UNPKG

@samchon/openapi

Version:

OpenAPI definitions and converters for 'typia' and 'nestia'.

229 lines (223 loc) 12.9 kB
import { EndpointUtil } from "../../utils/EndpointUtil.mjs"; import { Escaper } from "../../utils/Escaper.mjs"; import { OpenApiTypeChecker } from "../../utils/OpenApiTypeChecker.mjs"; var HttpMigrateRouteComposer; (function(HttpMigrateRouteComposer) { HttpMigrateRouteComposer.compose = props => { const body = emplaceBodySchema("request")((schema => emplaceReference({ document: props.document, name: EndpointUtil.pascal(`I/Api/${props.path}`) + "." + EndpointUtil.pascal(`${props.method}/Body`), schema })))(props.operation.requestBody); const success = (() => { const body = emplaceBodySchema("response")((schema => emplaceReference({ document: props.document, name: EndpointUtil.pascal(`I/Api/${props.path}`) + "." + EndpointUtil.pascal(`${props.method}/Response`), schema })))(props.operation.responses?.["201"] ?? props.operation.responses?.["200"] ?? props.operation.responses?.default); return body ? { ...body, status: props.operation.responses?.["201"] ? "201" : props.operation.responses?.["200"] ? "200" : "default" } : body; })(); const failures = []; if (body === false) failures.push(`supports only "application/json", "application/x-www-form-urlencoded", "multipart/form-data" and "text/plain" content type in the request body.`); if (success === false) failures.push(`supports only "application/json", "application/x-www-form-urlencoded" and "text/plain" content type in the response body.`); const [headers, query] = [ "header", "query" ].map((type => { const parameters = (props.operation.parameters ?? []).filter((p => p.in === type)); if (parameters.length === 0) return null; const objects = parameters.map((p => OpenApiTypeChecker.isObject(p.schema) ? p.schema : OpenApiTypeChecker.isReference(p.schema) && OpenApiTypeChecker.isObject(props.document.components.schemas?.[p.schema.$ref.replace(`#/components/schemas/`, ``)] ?? {}) ? p.schema : null)).filter((s => !!s)); const primitives = parameters.filter((p => OpenApiTypeChecker.isBoolean(p.schema) || OpenApiTypeChecker.isInteger(p.schema) || OpenApiTypeChecker.isNumber(p.schema) || OpenApiTypeChecker.isString(p.schema) || OpenApiTypeChecker.isArray(p.schema) || OpenApiTypeChecker.isTuple(p.schema))); const out = elem => ({ ...elem, name: type, key: type, title: () => elem.title, description: () => elem.description, example: () => elem.example, examples: () => elem.examples }); if (objects.length === 1 && primitives.length === 0) return out(parameters[0]); else if (objects.length > 1) { failures.push(`${type} typed parameters must be only one object type`); return false; } const dto = objects[0] ? OpenApiTypeChecker.isObject(objects[0]) ? objects[0] : (props.document.components.schemas ?? {})[objects[0].$ref.replace(`#/components/schemas/`, ``)] : null; const entire = [ ...objects.map((o => OpenApiTypeChecker.isObject(o) ? o : props.document.components.schemas?.[o.$ref.replace(`#/components/schemas/`, ``)])), { type: "object", properties: Object.fromEntries([ ...primitives.map((p => [ p.name, { ...p.schema, description: p.schema.description ?? p.description } ])), ...dto ? Object.entries(dto.properties ?? {}) : [] ]), required: [ ...new Set([ ...primitives.filter((p => p.required)).map((p => p.name)), ...dto?.required ?? [] ]) ] } ]; return parameters.length === 0 ? null : out({ schema: emplaceReference({ document: props.document, name: EndpointUtil.pascal(`I/Api/${props.path}`) + "." + EndpointUtil.pascal(`${props.method}/${type}`), schema: { type: "object", properties: Object.fromEntries([ ...new Map(entire.map((o => Object.entries(o.properties ?? {}).map((([name, schema]) => [ name, { ...schema, description: schema.description ?? schema.description } ])))).flat()) ]), required: [ ...new Set(entire.map((o => o.required ?? [])).flat()) ] } }) }); })); const parameterNames = EndpointUtil.splitWithNormalization(props.emendedPath).filter((str => str[0] === ":")).map((str => str.substring(1))); const pathParameters = (props.operation.parameters ?? []).filter((p => p.in === "path")); if (parameterNames.length !== pathParameters.length) if (pathParameters.length < parameterNames.length && pathParameters.every((p => p.name !== undefined && parameterNames.includes(p.name)))) { for (const name of parameterNames) if (pathParameters.find((p => p.name === name)) === undefined) pathParameters.push({ name, in: "path", schema: { type: "string" } }); pathParameters.sort(((a, b) => parameterNames.indexOf(a.name) - parameterNames.indexOf(b.name))); props.operation.parameters = [ ...pathParameters, ...(props.operation.parameters ?? []).filter((p => p.in !== "path")) ]; } else failures.push("number of path parameters are not matched with its full path."); if (failures.length) return failures; const parameters = (props.operation.parameters ?? []).filter((p => p.in === "path")).map(((p, i) => ({ name: parameterNames[i], key: (() => { let key = EndpointUtil.normalize(parameterNames[i]); if (Escaper.variable(key)) return key; while (true) { key = "_" + key; if (!parameterNames.some((s => s === key))) return key; } })(), schema: p.schema, parameter: () => p }))); return { method: props.method, path: props.path, emendedPath: props.emendedPath, accessor: [ "@lazy" ], parameters: (props.operation.parameters ?? []).filter((p => p.in === "path")).map(((p, i) => ({ name: parameterNames[i], key: (() => { let key = EndpointUtil.normalize(parameterNames[i]); if (Escaper.variable(key)) return key; while (true) { key = "_" + key; if (!parameterNames.some((s => s === key))) return key; } })(), schema: p.schema, parameter: () => p }))), headers: headers || null, query: query || null, body: body || null, success: success || null, exceptions: Object.fromEntries(Object.entries(props.operation.responses ?? {}).filter((([key]) => key !== "200" && key !== "201" && key !== "default")).map((([status, response]) => [ status, { schema: response.content?.["application/json"]?.schema ?? {}, response: () => response, media: () => response.content?.["application/json"] ?? {} } ]))), comment: () => writeRouteComment({ operation: props.operation, parameters, body: body || null }), operation: () => props.operation }; }; const writeRouteComment = props => { const commentTags = []; const add = text => { if (commentTags.every((line => line !== text))) commentTags.push(text); }; let description = props.operation.description ?? ""; if (props.operation.summary) { const emended = props.operation.summary.endsWith(".") ? props.operation.summary : props.operation.summary + "."; if (!!description.length && !description.startsWith(props.operation.summary)) description = `${emended}\n${description}`; } description = description.split("\n").map((s => s.trim())).join("\n"); for (const p of props.parameters ?? []) { const param = p.parameter(); if (param.description || param.title) { const text = param.description ?? param.title; add(`@param ${p.name} ${writeIndented(text, p.name.length + 8)}`); } } if (props.body?.description()?.length) add(`@param body ${writeIndented(props.body.description(), 12)}`); for (const security of props.operation.security ?? []) for (const [name, scopes] of Object.entries(security)) add(`@security ${[ name, ...scopes ].join("")}`); if (props.operation.tags) props.operation.tags.forEach((name => add(`@tag ${name}`))); if (props.operation.deprecated) add("@deprecated"); description = description.length ? commentTags.length ? `${description}\n\n${commentTags.join("\n")}` : description : commentTags.join("\n"); description = description.split("*/").join("*\\/"); return description; }; const writeIndented = (text, spaces) => text.split("\n").map((s => s.trim())).map(((s, i) => i === 0 ? s : `${" ".repeat(spaces)}${s}`)).join("\n"); const emplaceBodySchema = from => emplacer => meta => { if (!meta?.content) return null; const entries = Object.entries(meta.content).filter((([_, v]) => !!v)); const json = entries.find((e => meta["x-nestia-encrypted"] === true ? e[0].includes("text/plain") || e[0].includes("application/json") : e[0].includes("application/json") || e[0].includes("*/*"))); if (json) { const {schema} = json[1]; return schema || from === "response" ? { type: "application/json", name: "body", key: "body", schema: schema ? isNotObjectLiteral(schema) ? schema : emplacer(schema) : {}, description: () => meta.description, media: () => json[1], "x-nestia-encrypted": meta["x-nestia-encrypted"] } : null; } const query = entries.find((e => e[0].includes("application/x-www-form-urlencoded"))); if (query) { const {schema} = query[1]; return schema || from === "response" ? { type: "application/x-www-form-urlencoded", name: "body", key: "body", schema: schema ? isNotObjectLiteral(schema) ? schema : emplacer(schema) : {}, description: () => meta.description, media: () => query[1] } : null; } const text = entries.find((e => e[0].includes("text/plain"))); if (text) return { type: "text/plain", name: "body", key: "body", schema: { type: "string" }, description: () => meta.description, media: () => text[1] }; if (from === "request") { const multipart = entries.find((e => e[0].includes("multipart/form-data"))); if (multipart) { const {schema} = multipart[1]; return { type: "multipart/form-data", name: "body", key: "body", schema: schema ? isNotObjectLiteral(schema) ? schema : emplacer(schema) : {}, description: () => meta.description, media: () => multipart[1] }; } } return false; }; const emplaceReference = props => { var _a; (_a = props.document.components).schemas ?? (_a.schemas = {}); props.document.components.schemas[props.name] = props.schema; return { $ref: `#/components/schemas/${props.name}` }; }; const isNotObjectLiteral = schema => OpenApiTypeChecker.isReference(schema) || OpenApiTypeChecker.isBoolean(schema) || OpenApiTypeChecker.isNumber(schema) || OpenApiTypeChecker.isString(schema) || OpenApiTypeChecker.isUnknown(schema) || OpenApiTypeChecker.isOneOf(schema) && schema.oneOf.every(isNotObjectLiteral) || OpenApiTypeChecker.isArray(schema) && isNotObjectLiteral(schema.items); })(HttpMigrateRouteComposer || (HttpMigrateRouteComposer = {})); export { HttpMigrateRouteComposer }; //# sourceMappingURL=HttpMigrateRouteComposer.mjs.map