UNPKG

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