@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.
397 lines (391 loc) • 21.1 kB
JavaScript
import { AccessorUtil } from "../AccessorUtil.mjs";
import { MapUtil } from "../MapUtil.mjs";
import { JsonDescriptionUtil } from "./JsonDescriptionUtil.mjs";
var OpenApiTypeCheckerBase;
(function(OpenApiTypeCheckerBase) {
OpenApiTypeCheckerBase.isNull = schema => schema.type === "null";
OpenApiTypeCheckerBase.isUnknown = schema => schema.type === undefined && !OpenApiTypeCheckerBase.isConstant(schema) && !OpenApiTypeCheckerBase.isOneOf(schema) && !OpenApiTypeCheckerBase.isReference(schema);
OpenApiTypeCheckerBase.isConstant = schema => schema.const !== undefined;
OpenApiTypeCheckerBase.isBoolean = schema => schema.type === "boolean";
OpenApiTypeCheckerBase.isInteger = schema => schema.type === "integer";
OpenApiTypeCheckerBase.isNumber = schema => schema.type === "number";
OpenApiTypeCheckerBase.isString = schema => schema.type === "string";
OpenApiTypeCheckerBase.isArray = schema => schema.type === "array" && schema.items !== undefined;
OpenApiTypeCheckerBase.isTuple = schema => schema.type === "array" && schema.prefixItems !== undefined;
OpenApiTypeCheckerBase.isObject = schema => schema.type === "object";
OpenApiTypeCheckerBase.isReference = schema => schema.$ref !== undefined;
OpenApiTypeCheckerBase.isOneOf = schema => schema.oneOf !== undefined;
OpenApiTypeCheckerBase.isRecursiveReference = props => {
if (OpenApiTypeCheckerBase.isReference(props.schema) === false) return false;
const current = props.schema.$ref.split(props.prefix)[1];
let counter = 0;
OpenApiTypeCheckerBase.visit({
prefix: props.prefix,
components: props.components,
schema: props.schema,
closure: schema => {
if (OpenApiTypeCheckerBase.isReference(schema)) {
const next = schema.$ref.split(props.prefix)[1];
if (current === next) ++counter;
}
}
});
return counter > 1;
};
OpenApiTypeCheckerBase.unreference = props => {
const reasons = [];
const result = unreferenceSchema({
prefix: props.prefix,
refAccessor: props.refAccessor ?? `$input.${props.prefix.substring(2).split("/").filter(s => !!s.length).join(".")}`,
accessor: props.accessor ?? "$input.schema",
components: props.components,
schema: props.schema,
reasons
});
if (result === null) return {
success: false,
error: {
method: props.method,
message: `failed to unreference due to unable to find.`,
reasons
}
};
return {
success: true,
value: result
};
};
OpenApiTypeCheckerBase.escape = props => {
const reasons = [];
const result = escapeSchema({
...props,
reasons,
visited: new Map,
accessor: props.accessor ?? "$input.schema",
refAccessor: props.refAccessor ?? AccessorUtil.reference(props.prefix)
}) || null;
if (result === null) return {
success: false,
error: {
method: props.method,
message: `failed to escape some reference type(s) due to unable to find${Number(props.recursive) === 0 ? " or recursive relationship" : ""}.`,
reasons
}
};
return {
success: true,
value: result
};
};
OpenApiTypeCheckerBase.visit = props => {
const already = new Set;
const refAccessor = props.refAccessor ?? `$input.${AccessorUtil.reference(props.prefix)}`;
const next = (schema, accessor) => {
props.closure(schema, accessor);
if (OpenApiTypeCheckerBase.isReference(schema)) {
const key = schema.$ref.split(props.prefix).pop();
if (already.has(key) === true) return;
already.add(key);
const found = props.components.schemas?.[key];
if (found !== undefined) next(found, `${refAccessor}[${JSON.stringify(key)}]`);
} else if (OpenApiTypeCheckerBase.isOneOf(schema)) schema.oneOf.forEach((s, i) => next(s, `${accessor}.oneOf[${i}]`)); else if (OpenApiTypeCheckerBase.isObject(schema)) {
for (const [key, value] of Object.entries(schema.properties ?? {})) next(value, `${accessor}.properties[${JSON.stringify(key)}]`);
if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) next(schema.additionalProperties, `${accessor}.additionalProperties`);
} else if (OpenApiTypeCheckerBase.isArray(schema)) next(schema.items, `${accessor}.items`); else if (OpenApiTypeCheckerBase.isTuple(schema)) {
(schema.prefixItems ?? []).forEach((s, i) => next(s, `${accessor}.prefixItems[${i}]`));
if (typeof schema.additionalItems === "object" && schema.additionalItems !== null) next(schema.additionalItems, `${accessor}.additionalItems`);
}
};
next(props.schema, props.accessor ?? "$input.schema");
};
OpenApiTypeCheckerBase.covers = props => coverStation({
prefix: props.prefix,
components: props.components,
x: props.x,
y: props.y,
visited: new Map
});
const unreferenceSchema = props => {
if (OpenApiTypeCheckerBase.isReference(props.schema) === false) return props.schema;
const key = props.schema.$ref.split(props.prefix).pop();
const found = props.components.schemas?.[key];
if (found === undefined) {
props.reasons.push({
schema: props.schema,
accessor: props.accessor,
message: `unable to find reference type ${JSON.stringify(key)}.`
});
return null;
} else if (OpenApiTypeCheckerBase.isReference(found) === false) return found; else if (props.first === key) {
props.reasons.push({
schema: props.schema,
accessor: props.accessor,
message: `recursive reference type ${JSON.stringify(key)}.`
});
return null;
}
return unreferenceSchema({
...props,
accessor: `${props.refAccessor}[${JSON.stringify(key)}]`,
first: key
});
};
const escapeSchema = props => {
if (OpenApiTypeCheckerBase.isReference(props.schema)) {
const key = props.schema.$ref.split(props.prefix)[1];
const target = props.components.schemas?.[key];
if (target === undefined) {
props.reasons.push({
schema: props.schema,
accessor: props.accessor,
message: `unable to find reference type ${JSON.stringify(key)}.`
});
return null;
} else if (props.visited.has(key) === true) {
if (props.recursive === false) return null;
const depth = props.visited.get(key);
if (depth > props.recursive) {
if (props.recursive === 0) {
props.reasons.push({
schema: props.schema,
accessor: props.accessor,
message: `recursive reference type ${JSON.stringify(key)}.`
});
return null;
}
return undefined;
}
props.visited.set(key, depth + 1);
const res = escapeSchema({
...props,
schema: target,
accessor: `${props.refAccessor}[${JSON.stringify(key)}]`
});
return res ? {
...res,
description: JsonDescriptionUtil.cascade({
prefix: props.prefix,
components: props.components,
schema: props.schema,
escape: true
})
} : res;
} else {
const res = escapeSchema({
...props,
schema: target,
accessor: `${props.refAccessor}[${JSON.stringify(key)}]`,
visited: new Map([ ...props.visited, [ key, 1 ] ])
});
return res ? {
...res,
description: JsonDescriptionUtil.cascade({
prefix: props.prefix,
components: props.components,
schema: props.schema,
escape: true
})
} : res;
}
} else if (OpenApiTypeCheckerBase.isOneOf(props.schema)) {
const elements = props.schema.oneOf.map((s, i) => escapeSchema({
...props,
schema: s,
accessor: `${props.accessor}.oneOf[${i}]`
}));
if (elements.some(v => v === null)) return null;
const filtered = elements.filter(v => v !== undefined);
if (filtered.length === 0) return undefined;
return {
...props.schema,
oneOf: filtered.map(v => flatSchema({
prefix: props.prefix,
components: props.components,
schema: v
})).flat()
};
} else if (OpenApiTypeCheckerBase.isObject(props.schema)) {
const object = props.schema;
const properties = Object.entries(object.properties ?? {}).map(([k, s]) => [ k, escapeSchema({
...props,
schema: s,
visited: props.visited,
accessor: `${props.accessor}.properties[${JSON.stringify(k)}]`
}) ]);
const additionalProperties = object.additionalProperties ? typeof object.additionalProperties === "object" && object.additionalProperties !== null ? escapeSchema({
...props,
schema: object.additionalProperties,
accessor: `${props.accessor}.additionalProperties`
}) : object.additionalProperties : false;
if (properties.some(([_k, v]) => v === null) || additionalProperties === null) return null; else if (properties.some(([k, v]) => v === undefined && object.required?.includes(k) === true) === true) return undefined;
return {
...object,
properties: Object.fromEntries(properties.filter(([_k, v]) => v !== undefined)),
additionalProperties: additionalProperties ?? false,
required: object.required?.filter(k => properties.some(([key, value]) => key === k && value !== undefined)) ?? []
};
} else if (OpenApiTypeCheckerBase.isTuple(props.schema)) {
const elements = props.schema.prefixItems.map((s, i) => escapeSchema({
...props,
schema: s,
accessor: `${props.accessor}.prefixItems[${i}]`
}));
const additionalItems = props.schema.additionalItems ? typeof props.schema.additionalItems === "object" && props.schema.additionalItems !== null ? escapeSchema({
...props,
schema: props.schema.additionalItems,
accessor: `${props.accessor}.additionalItems`
}) : props.schema.additionalItems : false;
if (elements.some(v => v === null) || additionalItems === null) return null; else if (elements.some(v => v === undefined)) return undefined;
return {
...props.schema,
prefixItems: elements,
additionalItems: additionalItems ?? false
};
} else if (OpenApiTypeCheckerBase.isArray(props.schema)) {
const items = escapeSchema({
...props,
schema: props.schema.items,
accessor: `${props.accessor}.items`
});
if (items === null) return null; else if (items === undefined) return {
...props.schema,
minItems: undefined,
maxItems: 0,
items: {}
};
return {
...props.schema,
items
};
}
return props.schema;
};
const coverStation = p => {
const cache = p.visited.get(p.x)?.get(p.y);
if (cache !== undefined) return cache;
const nested = MapUtil.take(p.visited)(p.x)(() => new Map);
nested.set(p.y, true);
const result = coverSchema(p);
nested.set(p.y, result);
return result;
};
const coverSchema = p => {
if (p.x === p.y) return true; else if (OpenApiTypeCheckerBase.isReference(p.x) && OpenApiTypeCheckerBase.isReference(p.y) && p.x.$ref === p.y.$ref) return true;
const alpha = flatSchema({
prefix: p.prefix,
components: p.components,
schema: p.x
});
const beta = flatSchema({
prefix: p.prefix,
components: p.components,
schema: p.y
});
if (alpha.some(x => OpenApiTypeCheckerBase.isUnknown(x))) return true; else if (beta.some(x => OpenApiTypeCheckerBase.isUnknown(x))) return false;
return beta.every(b => alpha.some(a => coverEscapedSchema({
prefix: p.prefix,
components: p.components,
visited: p.visited,
x: a,
y: b
})));
};
const coverEscapedSchema = p => {
if (p.x === p.y) return true; else if (OpenApiTypeCheckerBase.isUnknown(p.x)) return true; else if (OpenApiTypeCheckerBase.isUnknown(p.y)) return false; else if (OpenApiTypeCheckerBase.isNull(p.x)) return OpenApiTypeCheckerBase.isNull(p.y); else if (OpenApiTypeCheckerBase.isConstant(p.x)) return OpenApiTypeCheckerBase.isConstant(p.y) && p.x.const === p.y.const; else if (OpenApiTypeCheckerBase.isBoolean(p.x)) return OpenApiTypeCheckerBase.isBoolean(p.y) || OpenApiTypeCheckerBase.isConstant(p.y) && typeof p.y.const === "boolean"; else if (OpenApiTypeCheckerBase.isInteger(p.x)) return (OpenApiTypeCheckerBase.isInteger(p.y) || OpenApiTypeCheckerBase.isConstant(p.y)) && OpenApiTypeCheckerBase.coverInteger(p.x, p.y); else if (OpenApiTypeCheckerBase.isNumber(p.x)) return (OpenApiTypeCheckerBase.isConstant(p.y) || OpenApiTypeCheckerBase.isInteger(p.y) || OpenApiTypeCheckerBase.isNumber(p.y)) && OpenApiTypeCheckerBase.coverNumber(p.x, p.y); else if (OpenApiTypeCheckerBase.isString(p.x)) return (OpenApiTypeCheckerBase.isConstant(p.y) || OpenApiTypeCheckerBase.isString(p.y)) && OpenApiTypeCheckerBase.coverString(p.x, p.y); else if (OpenApiTypeCheckerBase.isArray(p.x)) return (OpenApiTypeCheckerBase.isArray(p.y) || OpenApiTypeCheckerBase.isTuple(p.y)) && coverArray({
prefix: p.prefix,
components: p.components,
visited: p.visited,
x: p.x,
y: p.y
}); else if (OpenApiTypeCheckerBase.isObject(p.x)) return OpenApiTypeCheckerBase.isObject(p.y) && coverObject({
prefix: p.prefix,
components: p.components,
visited: p.visited,
x: p.x,
y: p.y
}); else if (OpenApiTypeCheckerBase.isReference(p.x)) return OpenApiTypeCheckerBase.isReference(p.y) && p.x.$ref === p.y.$ref;
return false;
};
const coverArray = p => {
if (OpenApiTypeCheckerBase.isTuple(p.y)) return p.y.prefixItems.every(v => coverStation({
prefix: p.prefix,
components: p.components,
visited: p.visited,
x: p.x.items,
y: v
})) && (p.y.additionalItems === undefined || typeof p.y.additionalItems === "object" && coverStation({
prefix: p.prefix,
components: p.components,
visited: p.visited,
x: p.x.items,
y: p.y.additionalItems
})); else if (!(p.x.minItems === undefined || p.y.minItems !== undefined && p.x.minItems <= p.y.minItems)) return false; else if (!(p.x.maxItems === undefined || p.y.maxItems !== undefined && p.x.maxItems >= p.y.maxItems)) return false;
return coverStation({
prefix: p.prefix,
components: p.components,
visited: p.visited,
x: p.x.items,
y: p.y.items
});
};
const coverObject = p => {
if (!p.x.additionalProperties && !!p.y.additionalProperties) return false; else if (!!p.x.additionalProperties && !!p.y.additionalProperties && (typeof p.x.additionalProperties === "object" && p.y.additionalProperties === true || typeof p.x.additionalProperties === "object" && typeof p.y.additionalProperties === "object" && !coverStation({
prefix: p.prefix,
components: p.components,
visited: p.visited,
x: p.x.additionalProperties,
y: p.y.additionalProperties
}))) return false;
return Object.entries(p.y.properties ?? {}).every(([key, b]) => {
const a = p.x.properties?.[key];
if (a === undefined) return false; else if (p.x.required?.includes(key) === true && (p.y.required?.includes(key) ?? false) === false) return false;
return coverStation({
prefix: p.prefix,
components: p.components,
visited: p.visited,
x: a,
y: b
});
});
};
OpenApiTypeCheckerBase.coverInteger = (x, y) => {
if (OpenApiTypeCheckerBase.isConstant(y)) return typeof y.const === "number" && Number.isInteger(y.const);
return x.type === y.type && OpenApiTypeCheckerBase.coverNumericRange(x, y);
};
OpenApiTypeCheckerBase.coverNumber = (x, y) => {
if (OpenApiTypeCheckerBase.isConstant(y)) return typeof y.const === "number";
return (x.type === y.type || x.type === "number" && y.type === "integer") && OpenApiTypeCheckerBase.coverNumericRange(x, y);
};
OpenApiTypeCheckerBase.coverString = (x, y) => {
if (OpenApiTypeCheckerBase.isConstant(y)) return typeof y.const === "string";
return [ x.format === undefined || y.format !== undefined && coverFormat(x.format, y.format), x.pattern === undefined || x.pattern === y.pattern, x.minLength === undefined || y.minLength !== undefined && x.minLength <= y.minLength, x.maxLength === undefined || y.maxLength !== undefined && x.maxLength >= y.maxLength ].every(v => v);
};
const coverFormat = (x, y) => x === y || x === "idn-email" && y === "email" || x === "idn-hostname" && y === "hostname" || [ "uri", "iri" ].includes(x) && y === "url" || x === "iri" && y === "uri" || x === "iri-reference" && y === "uri-reference";
const flatSchema = props => {
const schema = escapeReferenceOfFlatSchema(props);
if (OpenApiTypeCheckerBase.isOneOf(schema)) return schema.oneOf.map(v => flatSchema({
prefix: props.prefix,
components: props.components,
schema: v
})).flat();
return [ schema ];
};
const escapeReferenceOfFlatSchema = props => {
if (OpenApiTypeCheckerBase.isReference(props.schema) === false) return props.schema;
const key = props.schema.$ref.replace(props.prefix, "");
const found = escapeReferenceOfFlatSchema({
prefix: props.prefix,
components: props.components,
schema: props.components.schemas?.[key] ?? {}
});
if (found === undefined) throw new Error(`Reference type not found: ${JSON.stringify(props.schema.$ref)}`);
return escapeReferenceOfFlatSchema({
prefix: props.prefix,
components: props.components,
schema: found
});
};
OpenApiTypeCheckerBase.coverNumericRange = (x, y) => [ x.minimum === undefined || y.minimum !== undefined && x.minimum <= y.minimum || y.exclusiveMinimum !== undefined && x.minimum < y.exclusiveMinimum, x.maximum === undefined || y.maximum !== undefined && x.maximum >= y.maximum || y.exclusiveMaximum !== undefined && x.maximum > y.exclusiveMaximum, x.exclusiveMinimum === undefined || y.minimum !== undefined && x.exclusiveMinimum <= y.minimum || y.exclusiveMinimum !== undefined && x.exclusiveMinimum <= y.exclusiveMinimum, x.exclusiveMaximum === undefined || y.maximum !== undefined && x.exclusiveMaximum >= y.maximum || y.exclusiveMaximum !== undefined && x.exclusiveMaximum >= y.exclusiveMaximum, x.multipleOf === undefined || y.multipleOf !== undefined && y.multipleOf / x.multipleOf === Math.floor(y.multipleOf / x.multipleOf) ].every(v => v);
})(OpenApiTypeCheckerBase || (OpenApiTypeCheckerBase = {}));
export { OpenApiTypeCheckerBase };
//# sourceMappingURL=OpenApiTypeCheckerBase.mjs.map