import * as Effect from "effect/Effect"; import { contentHash, stableHash } from "@executor/source-core"; import { parseOpenApiDocument } from "./document";import type { OpenApiExample, OpenApiExtractedTool, OpenApiHeader, OpenApiJsonObject, OpenApiMediaContent, OpenApiResponseVariant, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiServer, OpenApiToolDocumentation, OpenApiToolManifest, OpenApiToolParameter, OpenApiToolRequestBody,} from "./types";import { OPEN_API_HTTP_METHODS, type OpenApiHttpMethod } from "./types"; const asObject = (value: unknown): Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {}; const asArray = (value: unknown): ReadonlyArray<unknown> => Array.isArray(value) ? value : []; const asTrimmedString = (value: unknown): string | undefined => typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; const stableJsonValue = (value: unknown): unknown => { if (Array.isArray(value)) { return value.map((entry) => stableJsonValue(entry)); } if (value !== null && typeof value === "object") { return Object.fromEntries( Object.entries(value as Record<string, unknown>) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, entry]) => [key, stableJsonValue(entry)]), ); } return value;}; const stableJsonStringify = (value: unknown): string => JSON.stringify(stableJsonValue(value)); const sortedTrimmedStrings = (value: unknown): string[] => [...new Set( asArray(value).flatMap((entry) => { const stringValue = asTrimmedString(entry); return stringValue ? [stringValue] : []; }), )].sort((left, right) => left.localeCompare(right)); const normalizedSwagger2FlowName = ( value: string | undefined,): "implicit" | "password" | "clientCredentials" | "authorizationCode" | undefined => { switch (value) { case "implicit": return "implicit"; case "password": return "password"; case "application": return "clientCredentials"; case "accessCode": return "authorizationCode"; default: return undefined; }}; const resolvePointerSegment = (segment: string): string => segment.replaceAll("~1", "/").replaceAll("~0", "~"); const resolveLocalRef = ( document: OpenApiJsonObject, value: unknown, activeRefs: ReadonlySet<string> = new Set<string>(),): unknown => { const object = asObject(value); const ref = typeof object.$ref === "string" ? object.$ref : null; if (!ref || !ref.startsWith("#/") || activeRefs.has(ref)) { return value; } const resolved = ref .slice(2) .split("/") .reduce<unknown>((current, segment) => { if (current === undefined || current === null) { return undefined; } return asObject(current)[resolvePointerSegment(segment)]; }, document); if (resolved === undefined) { return value; } const nextActiveRefs = new Set(activeRefs); nextActiveRefs.add(ref); const resolvedObject = asObject( resolveLocalRef(document, resolved, nextActiveRefs), ); const { $ref: _ignoredRef, ...rest } = object; return Object.keys(rest).length > 0 ? { ...resolvedObject, ...rest } : resolvedObject;}; export type OpenApiManifestExtractionOptions = { documentUrl?: string; loadDocument?: (url: string) => Promise<string>;}; const defaultDocumentLoader = async (url: string): Promise<string> => { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed fetching OpenAPI document ${url} (${response.status} ${response.statusText})`); } return response.text();}; const refTargetFor = ( currentDocumentUrl: string | undefined, ref: string,): { documentUrl?: string; pointer: string } | undefined => { if (ref.startsWith("#")) { return { documentUrl: currentDocumentUrl, pointer: ref }; } if (!currentDocumentUrl) { return undefined; } const resolvedUrl = new URL(ref, currentDocumentUrl); const pointer = resolvedUrl.hash && resolvedUrl.hash.length > 0 ? resolvedUrl.hash : "#"; resolvedUrl.hash = ""; return { documentUrl: resolvedUrl.toString(), pointer, };}; const resolvePointerValue = ( document: OpenApiJsonObject, pointer: string,): unknown => { if (pointer === "#" || pointer.length === 0) { return document; } return pointer .slice(pointer.startsWith("#/") ? 2 : 0) .split("/") .reduce<unknown>((current, segment) => { if (current === undefined || current === null) { return undefined; } return asObject(current)[resolvePointerSegment(segment)]; }, document);}; const loadDereferencedOpenApiDocument = async (input: { document: OpenApiJsonObject; documentUrl?: string; loadDocument?: (url: string) => Promise<string>;}): Promise<OpenApiJsonObject> => { if (!input.documentUrl) { return input.document; } const loader = input.loadDocument ?? defaultDocumentLoader; const cache = new Map<string, Promise<OpenApiJsonObject>>(); cache.set(input.documentUrl, Promise.resolve(input.document)); const loadParsedDocument = async (documentUrl: string): Promise<OpenApiJsonObject> => { const cached = cache.get(documentUrl); if (cached) { return cached; } const next = loader(documentUrl).then((text) => parseOpenApiDocument(text)); cache.set(documentUrl, next); return next; }; const dereference = async (inputValue: { value: unknown; currentDocument: OpenApiJsonObject; currentDocumentUrl?: string; activeRefs: ReadonlySet<string>; }): Promise<unknown> => { const { value, currentDocument, currentDocumentUrl, activeRefs } = inputValue; if (Array.isArray(value)) { return Promise.all( value.map((entry) => dereference({ value: entry, currentDocument, currentDocumentUrl, activeRefs, }), ), ); } if (value === null || typeof value !== "object") { return value; } const object = value as Record<string, unknown>; const ref = asTrimmedString(object.$ref); if (ref) { const target = refTargetFor(currentDocumentUrl, ref); if (!target?.documentUrl && !ref.startsWith("#")) { return value; } const activeKey = `${target?.documentUrl ?? currentDocumentUrl ?? "root"}|${target?.pointer ?? ref}`; if (activeRefs.has(activeKey)) { return value; } const targetDocument = target?.documentUrl && target.documentUrl !== currentDocumentUrl ? await loadParsedDocument(target.documentUrl) : currentDocument; const targetValue = resolvePointerValue(targetDocument, target?.pointer ?? ref); if (targetValue === undefined) { return value; } const nextActiveRefs = new Set(activeRefs); nextActiveRefs.add(activeKey); const resolvedValue = await dereference({ value: targetValue, currentDocument: targetDocument, currentDocumentUrl: target?.documentUrl ?? currentDocumentUrl, activeRefs: nextActiveRefs, }); const siblingEntries = Object.fromEntries( await Promise.all( Object.entries(object) .filter(([key]) => key !== "$ref") .map(async ([key, entry]) => [ key, await dereference({ value: entry, currentDocument, currentDocumentUrl, activeRefs: nextActiveRefs, }), ]), ), ); const resolvedObject = asObject(resolvedValue); return Object.keys(siblingEntries).length > 0 ? { ...resolvedObject, ...siblingEntries } : resolvedObject; } return Object.fromEntries( await Promise.all( Object.entries(object).map(async ([key, entry]) => [ key, await dereference({ value: entry, currentDocument, currentDocumentUrl, activeRefs, }), ]), ), ); }; return dereference({ value: input.document, currentDocument: input.document, currentDocumentUrl: input.documentUrl, activeRefs: new Set<string>(), }) as Promise<OpenApiJsonObject>;}; const preferredContentEntry = ( content: unknown,): readonly [string, Record<string, unknown>] | undefined => { const entries = Object.entries(asObject(content)) .sort(([left], [right]) => left.localeCompare(right)) .map(([mediaType, value]) => [mediaType, asObject(value)] as const); return ( entries.find(([mediaType]) => mediaType === "application/json") ?? entries.find(([mediaType]) => mediaType.toLowerCase().includes("+json")) ?? entries.find(([mediaType]) => mediaType.toLowerCase().includes("json")) ?? entries[0] );}; const contentSchemaFromOperationContent = ( document: OpenApiJsonObject, content: unknown,): unknown | undefined => { const preferred = preferredContentEntry(content); return preferred?.[1].schema === undefined ? undefined : resolveLocalRef(document, preferred[1].schema);}; const examplesFromValue = ( value: unknown, input: { label?: string; mediaType?: string; } = {},): Array<OpenApiExample> => { const record = asObject(value); const examples: Array<OpenApiExample> = []; if (record.example !== undefined) { examples.push({ valueJson: stableJsonStringify(record.example), ...(input.mediaType ? { mediaType: input.mediaType } : {}), ...(input.label ? { label: input.label } : {}), }); } const exampleEntries = Object.entries(asObject(record.examples)).sort(([left], [right]) => left.localeCompare(right), ); for (const [key, exampleValue] of exampleEntries) { const exampleRecord = asObject(exampleValue); examples.push({ valueJson: stableJsonStringify( exampleRecord.value !== undefined ? exampleRecord.value : exampleValue, ), ...(input.mediaType ? { mediaType: input.mediaType } : {}), label: key, }); } return examples;}; const examplesFromSchema = (schema: unknown): Array<OpenApiExample> => examplesFromValue(schema); const examplesFromMediaType = ( mediaType: string, mediaTypeRecord: Record<string, unknown>,): Array<OpenApiExample> => { const direct = examplesFromValue(mediaTypeRecord, { mediaType }); if (direct.length > 0) { return direct; } return examplesFromSchema(mediaTypeRecord.schema).map((example) => ({ ...example, mediaType, }));}; const contentEntriesFromContent = ( document: OpenApiJsonObject, content: unknown,): ReadonlyArray<OpenApiMediaContent> => Object.entries(asObject(content)) .sort(([left], [right]) => left.localeCompare(right)) .map(([mediaType, mediaValue]) => { const mediaRecord = asObject(resolveLocalRef(document, mediaValue)); const examples = examplesFromMediaType(mediaType, mediaRecord); return { mediaType, ...(mediaRecord.schema !== undefined ? { schema: resolveLocalRef(document, mediaRecord.schema) } : {}), ...(examples.length > 0 ? { examples } : {}), }; }); const schemaFromSchemaLikeRecord = ( document: OpenApiJsonObject, value: unknown,): unknown | undefined => { const record = asObject(resolveLocalRef(document, value)); if (record.schema !== undefined) { return resolveLocalRef(document, record.schema); } const schema: Record<string, unknown> = {}; const type = asTrimmedString(record.type); if (type === "file") { schema.type = "string"; schema.format = "binary"; } else if (type) { schema.type = type; } for (const key of [ "title", "description", "format", "default", "nullable", "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf", "minLength", "maxLength", "pattern", "minItems", "maxItems", "uniqueItems", "minProperties", "maxProperties", "deprecated", ] as const) { if (record[key] !== undefined) { schema[key] = record[key]; } } const enumValues = asArray(record.enum); if (enumValues.length > 0) { schema.enum = enumValues; } const required = sortedTrimmedStrings(record.required); if (required.length > 0) { schema.required = required; } if (record.items !== undefined) { schema.items = resolveLocalRef(document, record.items); } if (record.additionalProperties !== undefined) { schema.additionalProperties = typeof record.additionalProperties === "boolean" ? record.additionalProperties : resolveLocalRef(document, record.additionalProperties); } const properties = Object.fromEntries( Object.entries(asObject(record.properties)) .sort(([left], [right]) => left.localeCompare(right)) .map(([propertyName, propertyValue]) => [ propertyName, resolveLocalRef(document, propertyValue), ]), ); if (Object.keys(properties).length > 0) { schema.properties = properties; } const patternProperties = Object.fromEntries( Object.entries(asObject(record.patternProperties)) .sort(([left], [right]) => left.localeCompare(right)) .map(([pattern, patternValue]) => [pattern, resolveLocalRef(document, patternValue)]), ); if (Object.keys(patternProperties).length > 0) { schema.patternProperties = patternProperties; } for (const key of ["allOf", "anyOf", "oneOf"] as const) { const entries = asArray(record[key]).map((entry) => resolveLocalRef(document, entry)); if (entries.length > 0) { schema[key] = entries; } } if (record.not !== undefined) { schema.not = resolveLocalRef(document, record.not); } return Object.keys(schema).length > 0 ? schema : undefined;}; const headerFromValue = ( document: OpenApiJsonObject, name: string, value: unknown,): OpenApiHeader | undefined => {Lines 1–500 of 1525. Load more
import * as Effect from "effect/Effect"; import { contentHash, stableHash } from "@executor/source-core"; import { parseOpenApiDocument } from "./document"; import type { OpenApiExample, OpenApiExtractedTool, OpenApiHeader, OpenApiJsonObject, OpenApiMediaContent, OpenApiResponseVariant, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiServer, OpenApiToolDocumentation, OpenApiToolManifest, OpenApiToolParameter, OpenApiToolRequestBody, } from "./types"; import { OPEN_API_HTTP_METHODS, type OpenApiHttpMethod } from "./types"; const asObject = (value: unknown): Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {}; const asArray = (value: unknown): ReadonlyArray<unknown> => Array.isArray(value) ? value : []; const asTrimmedString = (value: unknown): string | undefined => typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; const stableJsonValue = (value: unknown): unknown => { if (Array.isArray(value)) { return value.map((entry) => stableJsonValue(entry)); } if (value !== null && typeof value === "object") { return Object.fromEntries( Object.entries(value as Record<string, unknown>) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, entry]) => [key, stableJsonValue(entry)]), ); } return value; }; const stableJsonStringify = (value: unknown): string => JSON.stringify(stableJsonValue(value)); const sortedTrimmedStrings = (value: unknown): string[] => [...new Set( asArray(value).flatMap((entry) => { const stringValue = asTrimmedString(entry); return stringValue ? [stringValue] : []; }), )].sort((left, right) => left.localeCompare(right)); const normalizedSwagger2FlowName = ( value: string | undefined, ): "implicit" | "password" | "clientCredentials" | "authorizationCode" | undefined => { switch (value) { case "implicit": return "implicit"; case "password": return "password"; case "application": return "clientCredentials"; case "accessCode": return "authorizationCode"; default: return undefined; } }; const resolvePointerSegment = (segment: string): string => segment.replaceAll("~1", "/").replaceAll("~0", "~"); const resolveLocalRef = ( document: OpenApiJsonObject, value: unknown, activeRefs: ReadonlySet<string> = new Set<string>(), ): unknown => { const object = asObject(value); const ref = typeof object.$ref === "string" ? object.$ref : null; if (!ref || !ref.startsWith("#/") || activeRefs.has(ref)) { return value; } const resolved = ref .slice(2) .split("/") .reduce<unknown>((current, segment) => { if (current === undefined || current === null) { return undefined; } return asObject(current)[resolvePointerSegment(segment)]; }, document); if (resolved === undefined) { return value; } const nextActiveRefs = new Set(activeRefs); nextActiveRefs.add(ref); const resolvedObject = asObject( resolveLocalRef(document, resolved, nextActiveRefs), ); const { $ref: _ignoredRef, ...rest } = object; return Object.keys(rest).length > 0 ? { ...resolvedObject, ...rest } : resolvedObject; }; export type OpenApiManifestExtractionOptions = { documentUrl?: string; loadDocument?: (url: string) => Promise<string>; }; const defaultDocumentLoader = async (url: string): Promise<string> => { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed fetching OpenAPI document ${url} (${response.status} ${response.statusText})`); } return response.text(); }; const refTargetFor = ( currentDocumentUrl: string | undefined, ref: string, ): { documentUrl?: string; pointer: string } | undefined => { if (ref.startsWith("#")) { return { documentUrl: currentDocumentUrl, pointer: ref }; } if (!currentDocumentUrl) { return undefined; } const resolvedUrl = new URL(ref, currentDocumentUrl); const pointer = resolvedUrl.hash && resolvedUrl.hash.length > 0 ? resolvedUrl.hash : "#"; resolvedUrl.hash = ""; return { documentUrl: resolvedUrl.toString(), pointer, }; }; const resolvePointerValue = ( document: OpenApiJsonObject, pointer: string, ): unknown => { if (pointer === "#" || pointer.length === 0) { return document; } return pointer .slice(pointer.startsWith("#/") ? 2 : 0) .split("/") .reduce<unknown>((current, segment) => { if (current === undefined || current === null) { return undefined; } return asObject(current)[resolvePointerSegment(segment)]; }, document); }; const loadDereferencedOpenApiDocument = async (input: { document: OpenApiJsonObject; documentUrl?: string; loadDocument?: (url: string) => Promise<string>; }): Promise<OpenApiJsonObject> => { if (!input.documentUrl) { return input.document; } const loader = input.loadDocument ?? defaultDocumentLoader; const cache = new Map<string, Promise<OpenApiJsonObject>>(); cache.set(input.documentUrl, Promise.resolve(input.document)); const loadParsedDocument = async (documentUrl: string): Promise<OpenApiJsonObject> => { const cached = cache.get(documentUrl); if (cached) { return cached; } const next = loader(documentUrl).then((text) => parseOpenApiDocument(text)); cache.set(documentUrl, next); return next; }; const dereference = async (inputValue: { value: unknown; currentDocument: OpenApiJsonObject; currentDocumentUrl?: string; activeRefs: ReadonlySet<string>; }): Promise<unknown> => { const { value, currentDocument, currentDocumentUrl, activeRefs } = inputValue; if (Array.isArray(value)) { return Promise.all( value.map((entry) => dereference({ value: entry, currentDocument, currentDocumentUrl, activeRefs, }), ), ); } if (value === null || typeof value !== "object") { return value; } const object = value as Record<string, unknown>; const ref = asTrimmedString(object.$ref); if (ref) { const target = refTargetFor(currentDocumentUrl, ref); if (!target?.documentUrl && !ref.startsWith("#")) { return value; } const activeKey = `${target?.documentUrl ?? currentDocumentUrl ?? "root"}|${target?.pointer ?? ref}`; if (activeRefs.has(activeKey)) { return value; } const targetDocument = target?.documentUrl && target.documentUrl !== currentDocumentUrl ? await loadParsedDocument(target.documentUrl) : currentDocument; const targetValue = resolvePointerValue(targetDocument, target?.pointer ?? ref); if (targetValue === undefined) { return value; } const nextActiveRefs = new Set(activeRefs); nextActiveRefs.add(activeKey); const resolvedValue = await dereference({ value: targetValue, currentDocument: targetDocument, currentDocumentUrl: target?.documentUrl ?? currentDocumentUrl, activeRefs: nextActiveRefs, }); const siblingEntries = Object.fromEntries( await Promise.all( Object.entries(object) .filter(([key]) => key !== "$ref") .map(async ([key, entry]) => [ key, await dereference({ value: entry, currentDocument, currentDocumentUrl, activeRefs: nextActiveRefs, }), ]), ), ); const resolvedObject = asObject(resolvedValue); return Object.keys(siblingEntries).length > 0 ? { ...resolvedObject, ...siblingEntries } : resolvedObject; } return Object.fromEntries( await Promise.all( Object.entries(object).map(async ([key, entry]) => [ key, await dereference({ value: entry, currentDocument, currentDocumentUrl, activeRefs, }), ]), ), ); }; return dereference({ value: input.document, currentDocument: input.document, currentDocumentUrl: input.documentUrl, activeRefs: new Set<string>(), }) as Promise<OpenApiJsonObject>; }; const preferredContentEntry = ( content: unknown, ): readonly [string, Record<string, unknown>] | undefined => { const entries = Object.entries(asObject(content)) .sort(([left], [right]) => left.localeCompare(right)) .map(([mediaType, value]) => [mediaType, asObject(value)] as const); return ( entries.find(([mediaType]) => mediaType === "application/json") ?? entries.find(([mediaType]) => mediaType.toLowerCase().includes("+json")) ?? entries.find(([mediaType]) => mediaType.toLowerCase().includes("json")) ?? entries[0] ); }; const contentSchemaFromOperationContent = ( document: OpenApiJsonObject, content: unknown, ): unknown | undefined => { const preferred = preferredContentEntry(content); return preferred?.[1].schema === undefined ? undefined : resolveLocalRef(document, preferred[1].schema); }; const examplesFromValue = ( value: unknown, input: { label?: string; mediaType?: string; } = {}, ): Array<OpenApiExample> => { const record = asObject(value); const examples: Array<OpenApiExample> = []; if (record.example !== undefined) { examples.push({ valueJson: stableJsonStringify(record.example), ...(input.mediaType ? { mediaType: input.mediaType } : {}), ...(input.label ? { label: input.label } : {}), }); } const exampleEntries = Object.entries(asObject(record.examples)).sort(([left], [right]) => left.localeCompare(right), ); for (const [key, exampleValue] of exampleEntries) { const exampleRecord = asObject(exampleValue); examples.push({ valueJson: stableJsonStringify( exampleRecord.value !== undefined ? exampleRecord.value : exampleValue, ), ...(input.mediaType ? { mediaType: input.mediaType } : {}), label: key, }); } return examples; }; const examplesFromSchema = (schema: unknown): Array<OpenApiExample> => examplesFromValue(schema); const examplesFromMediaType = ( mediaType: string, mediaTypeRecord: Record<string, unknown>, ): Array<OpenApiExample> => { const direct = examplesFromValue(mediaTypeRecord, { mediaType }); if (direct.length > 0) { return direct; } return examplesFromSchema(mediaTypeRecord.schema).map((example) => ({ ...example, mediaType, })); }; const contentEntriesFromContent = ( document: OpenApiJsonObject, content: unknown, ): ReadonlyArray<OpenApiMediaContent> => Object.entries(asObject(content)) .sort(([left], [right]) => left.localeCompare(right)) .map(([mediaType, mediaValue]) => { const mediaRecord = asObject(resolveLocalRef(document, mediaValue)); const examples = examplesFromMediaType(mediaType, mediaRecord); return { mediaType, ...(mediaRecord.schema !== undefined ? { schema: resolveLocalRef(document, mediaRecord.schema) } : {}), ...(examples.length > 0 ? { examples } : {}), }; }); const schemaFromSchemaLikeRecord = ( document: OpenApiJsonObject, value: unknown, ): unknown | undefined => { const record = asObject(resolveLocalRef(document, value)); if (record.schema !== undefined) { return resolveLocalRef(document, record.schema); } const schema: Record<string, unknown> = {}; const type = asTrimmedString(record.type); if (type === "file") { schema.type = "string"; schema.format = "binary"; } else if (type) { schema.type = type; } for (const key of [ "title", "description", "format", "default", "nullable", "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf", "minLength", "maxLength", "pattern", "minItems", "maxItems", "uniqueItems", "minProperties", "maxProperties", "deprecated", ] as const) { if (record[key] !== undefined) { schema[key] = record[key]; } } const enumValues = asArray(record.enum); if (enumValues.length > 0) { schema.enum = enumValues; } const required = sortedTrimmedStrings(record.required); if (required.length > 0) { schema.required = required; } if (record.items !== undefined) { schema.items = resolveLocalRef(document, record.items); } if (record.additionalProperties !== undefined) { schema.additionalProperties = typeof record.additionalProperties === "boolean" ? record.additionalProperties : resolveLocalRef(document, record.additionalProperties); } const properties = Object.fromEntries( Object.entries(asObject(record.properties)) .sort(([left], [right]) => left.localeCompare(right)) .map(([propertyName, propertyValue]) => [ propertyName, resolveLocalRef(document, propertyValue), ]), ); if (Object.keys(properties).length > 0) { schema.properties = properties; } const patternProperties = Object.fromEntries( Object.entries(asObject(record.patternProperties)) .sort(([left], [right]) => left.localeCompare(right)) .map(([pattern, patternValue]) => [pattern, resolveLocalRef(document, patternValue)]), ); if (Object.keys(patternProperties).length > 0) { schema.patternProperties = patternProperties; } for (const key of ["allOf", "anyOf", "oneOf"] as const) { const entries = asArray(record[key]).map((entry) => resolveLocalRef(document, entry)); if (entries.length > 0) { schema[key] = entries; } } if (record.not !== undefined) { schema.not = resolveLocalRef(document, record.not); } return Object.keys(schema).length > 0 ? schema : undefined; }; const headerFromValue = ( document: OpenApiJsonObject, name: string, value: unknown, ): OpenApiHeader | undefined => {