UNPKG

@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
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