@samchon/openapi
Version:
Universal OpenAPI to LLM function calling schemas. Transform any Swagger/OpenAPI document into type-safe schemas for OpenAI, Claude, Qwen, and more.
230 lines (224 loc) • 12.8 kB
JavaScript
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?.length) {
const summary = props.operation.summary.endsWith(".") ? props.operation.summary : props.operation.summary + ".";
if (!!description.length && !description.startsWith(props.operation.summary)) description = `${summary}\n\n${description}`;
}
description = description.split("\n").map(s => s.trim()).join("\n");
add("@param connection");
for (const p of props.parameters ?? []) {
const param = p.parameter();
if (param.description) {
const text = param.description;
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