@kubb/plugin-oas
Version:
OpenAPI Specification (OAS) plugin for Kubb, providing core functionality for parsing and processing OpenAPI/Swagger schemas for code generation.
408 lines (407 loc) • 14.8 kB
JavaScript
import "./chunk--u3MIqq1.js";
import { n as createReactGenerator$1, r as createGenerator$1, t as jsonGenerator } from "./generators-D7C3CXsN.js";
import { i as pascalCase, r as camelCase } from "./getFooter-Pw3tLCiV.js";
import { a as buildOperations, c as pLimit, i as buildOperation, n as withRequiredRequestBodySchema, o as buildSchema, r as SchemaGenerator } from "./requestBody-pRavthCw.js";
import { n as schemaKeywords, t as isKeyword } from "./SchemaMapper-CqMkO2T1.js";
import path from "node:path";
import { parseFromConfig, resolveServerUrl } from "@kubb/oas";
import { definePlugin, getMode } from "@kubb/core";
//#region src/createParser.ts
/**
* Creates a parser function that converts schema trees to output using the provided mapper and handlers
*
* This function provides a framework for building parsers by:
* 1. Checking for custom handlers for each keyword
* 2. Falling back to the mapper for simple keywords
* 3. Providing utilities for common operations (finding siblings, etc.)
*
* The generated parser is recursive and can handle nested schemas.
*
* **Type Safety**: Each handler receives a `tree` parameter where `tree.current` is automatically
* typed as the specific schema keyword type (e.g., `SchemaKeywordMapper['ref']` for the `ref` handler).
* This means you can access `tree.current.args` with full type safety without needing `isKeyword` checks,
* though such checks can still be used as runtime guards if desired.
*
* @template TOutput - The output type (e.g., string for Zod/Faker, ts.TypeNode for TypeScript)
* @template TOptions - The parser options type
* @param config - Configuration object containing mapper and handlers
* @returns A parse function that converts SchemaTree to TOutput
*
* @example
* ```ts
* // Create a simple string-based parser
* const parse = createParser({
* mapper: zodKeywordMapper,
* handlers: {
* // tree.current is typed as SchemaKeywordMapper['union']
* union(tree, options) {
* const items = tree.current.args // args is correctly typed as Schema[]
* .map(it => this.parse({ ...tree, current: it }, options))
* .filter(Boolean)
* return `z.union([${items.join(', ')}])`
* },
* // tree.current is typed as SchemaKeywordMapper['string']
* string(tree, options) {
* const minSchema = findSchemaKeyword(tree.siblings, 'min')
* const maxSchema = findSchemaKeyword(tree.siblings, 'max')
* return zodKeywordMapper.string(false, minSchema?.args, maxSchema?.args)
* },
* // tree.current is typed as SchemaKeywordMapper['ref']
* ref(tree, options) {
* // No need for isKeyword check - tree.current.args is already properly typed
* return `Ref: ${tree.current.args.name}`
* }
* }
* })
* ```
*/
function createParser(config) {
const { mapper, handlers } = config;
function parse(tree, options) {
const { current } = tree;
const handler = handlers[current.keyword];
if (handler) {
const context = { parse };
return handler.call(context, tree, options);
}
const value = mapper[current.keyword];
if (!value) return;
if (current.keyword in mapper) return value();
}
return parse;
}
/**
* Helper to find a schema keyword in siblings
* Useful in handlers when you need to find related schemas (e.g., min/max for string)
*
* @example
* ```ts
* const minSchema = findSchemaKeyword(tree.siblings, 'min')
* const maxSchema = findSchemaKeyword(tree.siblings, 'max')
* return zodKeywordMapper.string(false, minSchema?.args, maxSchema?.args)
* ```
*/
function findSchemaKeyword(siblings, keyword) {
return SchemaGenerator.find(siblings, schemaKeywords[keyword]);
}
//#endregion
//#region src/OperationGenerator.ts
var OperationGenerator = class {
#options;
#context;
constructor(options, context) {
this.#options = options;
this.#context = context;
}
get options() {
return this.#options;
}
set options(options) {
this.#options = {
...this.#options,
...options
};
}
get context() {
return this.#context;
}
#matchesPattern(operation, method, type, pattern) {
switch (type) {
case "tag": return operation.getTags().some((tag) => tag.name.match(pattern));
case "operationId": return !!operation.getOperationId({ friendlyCase: true }).match(pattern);
case "path": return !!operation.path.match(pattern);
case "method": return !!method.match(pattern);
case "contentType": return !!operation.getContentType().match(pattern);
default: return false;
}
}
getOptions(operation, method) {
const { override = [] } = this.context;
return override.find(({ pattern, type }) => this.#matchesPattern(operation, method, type, pattern))?.options || {};
}
#isExcluded(operation, method) {
const { exclude = [] } = this.context;
return exclude.some(({ pattern, type }) => this.#matchesPattern(operation, method, type, pattern));
}
#isIncluded(operation, method) {
const { include = [] } = this.context;
return include.some(({ pattern, type }) => this.#matchesPattern(operation, method, type, pattern));
}
getSchemas(operation, { resolveName = (name) => name } = {}) {
const operationId = operation.getOperationId({ friendlyCase: true });
const operationName = pascalCase(operationId);
const resolveKeys = (schema) => schema?.properties ? Object.keys(schema.properties) : void 0;
const pathParamsSchema = this.context.oas.getParametersSchema(operation, "path");
const queryParamsSchema = this.context.oas.getParametersSchema(operation, "query");
const headerParamsSchema = this.context.oas.getParametersSchema(operation, "header");
const requestSchema = this.context.oas.getRequestSchema(operation);
const statusCodes = operation.getResponseStatusCodes().map((statusCode) => {
const name = statusCode === "default" ? "error" : statusCode;
const schema = this.context.oas.getResponseSchema(operation, statusCode);
const keys = resolveKeys(schema);
return {
name: this.context.UNSTABLE_NAMING ? resolveName(pascalCase(`${operationId} status ${name}`)) : resolveName(pascalCase(`${operationId} ${name}`)),
description: operation.getResponseByStatusCode(statusCode)?.description,
schema,
operation,
operationName,
statusCode: name === "error" ? void 0 : Number(statusCode),
keys,
keysToOmit: keys?.filter((key) => (schema?.properties?.[key])?.writeOnly)
};
});
const successful = statusCodes.filter((item) => item.statusCode?.toString().startsWith("2"));
const errors = statusCodes.filter((item) => item.statusCode?.toString().startsWith("4") || item.statusCode?.toString().startsWith("5"));
const request = withRequiredRequestBodySchema(requestSchema ? {
name: this.context.UNSTABLE_NAMING ? resolveName(pascalCase(`${operationId} RequestData`)) : resolveName(pascalCase(`${operationId} ${operation.method === "get" ? "queryRequest" : "mutationRequest"}`)),
description: operation.schema.requestBody?.description,
operation,
operationName,
schema: requestSchema,
keys: resolveKeys(requestSchema),
keysToOmit: resolveKeys(requestSchema)?.filter((key) => (requestSchema.properties?.[key])?.readOnly)
} : void 0);
return {
pathParams: pathParamsSchema ? {
name: resolveName(pascalCase(`${operationId} PathParams`)),
operation,
operationName,
schema: pathParamsSchema,
keys: resolveKeys(pathParamsSchema)
} : void 0,
queryParams: queryParamsSchema ? {
name: resolveName(pascalCase(`${operationId} QueryParams`)),
operation,
operationName,
schema: queryParamsSchema,
keys: resolveKeys(queryParamsSchema) || []
} : void 0,
headerParams: headerParamsSchema ? {
name: resolveName(pascalCase(`${operationId} HeaderParams`)),
operation,
operationName,
schema: headerParamsSchema,
keys: resolveKeys(headerParamsSchema)
} : void 0,
request,
response: {
name: this.context.UNSTABLE_NAMING ? resolveName(pascalCase(`${operationId} ResponseData`)) : resolveName(pascalCase(`${operationId} ${operation.method === "get" ? "queryResponse" : "mutationResponse"}`)),
operation,
operationName,
schema: { oneOf: successful.map((item) => ({
...item.schema,
$ref: item.name
})) || void 0 }
},
responses: successful,
errors,
statusCodes
};
}
async getOperations() {
const { oas } = this.context;
const paths = oas.getPaths();
return Object.entries(paths).flatMap(([path, methods]) => Object.entries(methods).map((values) => {
const [method, operation] = values;
if (this.#isExcluded(operation, method)) return null;
if (this.context.include && !this.#isIncluded(operation, method)) return null;
return operation ? {
path,
method,
operation
} : null;
}).filter((x) => x !== null));
}
async build(...generators) {
const operations = await this.getOperations();
const generatorLimit = pLimit(3);
const operationLimit = pLimit(30);
this.context.events?.emit("debug", {
date: /* @__PURE__ */ new Date(),
logs: [`Building ${operations.length} operations`, ` • Generators: ${generators.length}`]
});
const writeTasks = generators.map((generator) => generatorLimit(async () => {
if (generator.version === "2") return [];
const v1Generator = generator;
const operationTasks = operations.map(({ operation, method }) => operationLimit(async () => {
const options = this.getOptions(operation, method);
if (v1Generator.type === "react") {
await buildOperation(operation, {
config: this.context.pluginManager.config,
fabric: this.context.fabric,
Component: v1Generator.Operation,
generator: this,
plugin: {
...this.context.plugin,
options: {
...this.options,
...options
}
}
});
return [];
}
return await v1Generator.operation?.({
generator: this,
config: this.context.pluginManager.config,
operation,
plugin: {
...this.context.plugin,
options: {
...this.options,
...options
}
}
}) ?? [];
}));
const opResultsFlat = (await Promise.all(operationTasks)).flat();
if (v1Generator.type === "react") {
await buildOperations(operations.map((op) => op.operation), {
fabric: this.context.fabric,
config: this.context.pluginManager.config,
Component: v1Generator.Operations,
generator: this,
plugin: this.context.plugin
});
return [];
}
const operationsResult = await v1Generator.operations?.({
generator: this,
config: this.context.pluginManager.config,
operations: operations.map((op) => op.operation),
plugin: this.context.plugin
});
return [...opResultsFlat, ...operationsResult ?? []];
}));
return (await Promise.all(writeTasks)).flat();
}
};
//#endregion
//#region src/plugin.ts
const pluginOasName = "plugin-oas";
const pluginOas = definePlugin((options) => {
const { output = { path: "schemas" }, group, validate = true, generators = [jsonGenerator], serverIndex, serverVariables, contentType, oasClass, discriminator = "strict", collisionDetection = false } = options;
const getOas = async ({ validate, config, events }) => {
const oas = await parseFromConfig(config, oasClass);
oas.setOptions({
contentType,
discriminator,
collisionDetection
});
try {
if (validate) await oas.validate();
} catch (er) {
const caughtError = er;
const errorTimestamp = /* @__PURE__ */ new Date();
const error = new Error("OAS Validation failed", { cause: caughtError });
events.emit("info", caughtError.message);
events.emit("debug", {
date: errorTimestamp,
logs: [`✗ ${error.message}`, caughtError.message]
});
}
return oas;
};
return {
name: pluginOasName,
options: {
output,
validate,
discriminator,
...options
},
inject() {
const config = this.config;
const events = this.events;
let oas;
return {
async getOas({ validate = false } = {}) {
if (!oas) oas = await getOas({
config,
events,
validate
});
return oas;
},
async getBaseURL() {
const oas = await getOas({
config,
events,
validate: false
});
if (serverIndex === void 0) return;
const server = oas.api.servers?.at(serverIndex);
if (!server?.url) return;
return resolveServerUrl(server, serverVariables);
}
};
},
resolvePath(baseName, pathMode, options) {
const root = path.resolve(this.config.root, this.config.output.path);
if ((pathMode ?? getMode(path.resolve(root, output.path))) === "single")
/**
* when output is a file then we will always append to the same file(output file), see fileManager.addOrAppend
* Other plugins then need to call addOrAppend instead of just add from the fileManager class
*/
return path.resolve(root, output.path);
if (group && (options?.group?.path || options?.group?.tag)) {
const groupName = group?.name ? group.name : (ctx) => {
if (group?.type === "path") return `${ctx.group.split("/")[1]}`;
return `${camelCase(ctx.group)}Controller`;
};
return path.resolve(root, output.path, groupName({ group: group.type === "path" ? options.group.path : options.group.tag }), baseName);
}
return path.resolve(root, output.path, baseName);
},
async install() {
const oas = await this.getOas({ validate });
if (!output) return;
await oas.dereference();
const schemaFiles = await new SchemaGenerator({
unknownType: "unknown",
emptySchemaType: "unknown",
dateType: "date",
transformers: {},
...this.plugin.options
}, {
fabric: this.fabric,
oas,
pluginManager: this.pluginManager,
events: this.events,
plugin: this.plugin,
contentType,
include: void 0,
override: void 0,
mode: "split",
output: output.path
}).build(...generators);
await this.upsertFile(...schemaFiles);
const operationFiles = await new OperationGenerator(this.plugin.options, {
fabric: this.fabric,
oas,
pluginManager: this.pluginManager,
events: this.events,
plugin: this.plugin,
contentType,
exclude: void 0,
include: void 0,
override: void 0,
mode: "split"
}).build(...generators);
await this.upsertFile(...operationFiles);
}
};
});
//#endregion
//#region src/index.ts
/**
* @deprecated use `import { createGenerator } from '@kubb/plugin-oas/generators'`
*/
const createGenerator = createGenerator$1;
/**
* @deprecated use `import { createReactGenerator } from '@kubb/plugin-oas/generators'`
*/
const createReactGenerator = createReactGenerator$1;
//#endregion
export { OperationGenerator, SchemaGenerator, buildOperation, buildOperations, buildSchema, createGenerator, createParser, createReactGenerator, findSchemaKeyword, isKeyword, pluginOas, pluginOasName, schemaKeywords };
//# sourceMappingURL=index.js.map