import { Effect, Option } from "effect";import { ToolFileJsonSchema } from "@executor-js/sdk/core"; import { planToolPaths, type OperationPathInput, type PlannedToolPath } from "./definitions";import { OpenApiExtractionError } from "./errors";import type { ParsedDocument } from "./parse";import { parseEntry, parseHead, parseSmallComponents, type ByteRange, type KeepPathItem, type SpecStructure,} from "./split";import { declaredContents, DocResolver, isNdjsonMediaType, ndjsonArrayOutputSchema, preferredResponseContent, type OperationObject, type ParameterObject, type PathItemObject, type RequestBodyObject, type ResponseObject, type ServerObject,} from "./openapi-utils";import { EncodingObject, ExtractedOperation, ExtractionResult, type HttpMethod, MediaBinding, OperationBinding, OperationFileHint, OperationId, OperationParameter, OperationRequestBody, OperationResponseBody, type ParameterLocation, ServerInfo, ServerVariable,} from "./types"; // ---------------------------------------------------------------------------// Constants// --------------------------------------------------------------------------- const HTTP_METHODS: readonly HttpMethod[] = [ "get", "put", "post", "delete", "patch", "head", "options", "trace",]; const VALID_PARAM_LOCATIONS = new Set<string>(["path", "query", "header", "cookie"]); // ---------------------------------------------------------------------------// Parameter extraction// --------------------------------------------------------------------------- const extractParameters = ( pathItem: PathItemObject, operation: OperationObject, r: DocResolver,): OperationParameter[] => { const merged = new Map<string, ParameterObject>(); for (const raw of pathItem.parameters ?? []) { const p = r.resolve<ParameterObject>(raw); if (!p) continue; merged.set(`${p.in}:${p.name}`, p); } for (const raw of operation.parameters ?? []) { const p = r.resolve<ParameterObject>(raw); if (!p) continue; merged.set(`${p.in}:${p.name}`, p); } return [...merged.values()] .filter((p) => VALID_PARAM_LOCATIONS.has(p.in)) .map((p) => OperationParameter.make({ name: p.name, location: p.in as ParameterLocation, required: p.in === "path" ? true : p.required === true, schema: Option.fromNullishOr(p.schema), style: Option.fromNullishOr(p.style), explode: Option.fromNullishOr(p.explode), allowReserved: Option.fromNullishOr("allowReserved" in p ? p.allowReserved : undefined), description: Option.fromNullishOr(p.description), }), );}; // ---------------------------------------------------------------------------// Request body extraction// --------------------------------------------------------------------------- const buildEncodingRecord = ( encoding: Record<string, unknown> | undefined,): Record<string, EncodingObject> | undefined => { if (!encoding) return undefined; const out: Record<string, EncodingObject> = {}; for (const [prop, raw] of Object.entries(encoding)) { if (typeof raw !== "object" || raw === null) continue; const e = raw as { contentType?: string; style?: string; explode?: boolean; allowReserved?: boolean; }; out[prop] = EncodingObject.make({ contentType: Option.fromNullishOr(e.contentType), style: Option.fromNullishOr(e.style), explode: Option.fromNullishOr(e.explode), allowReserved: Option.fromNullishOr(e.allowReserved), }); } return Object.keys(out).length > 0 ? out : undefined;}; const extractRequestBody = ( operation: OperationObject, r: DocResolver,): OperationRequestBody | undefined => { if (!operation.requestBody) return undefined; const body = r.resolve<RequestBodyObject>(operation.requestBody); if (!body) return undefined; const contents = declaredContents(body.content).map(({ mediaType, media }) => MediaBinding.make({ contentType: mediaType, schema: Option.fromNullishOr(multipartFileInputSchema(media.schema, mediaType)), encoding: Option.fromNullishOr( buildEncodingRecord((media as { encoding?: Record<string, unknown> }).encoding), ), }), ); if (contents.length === 0) return undefined; // Default = first declared (spec author's preferred order). Callers can // override at invoke time with a `contentType` arg. const defaultContent = contents[0]!; return OperationRequestBody.make({ required: body.required === true, contentType: defaultContent.contentType, schema: defaultContent.schema, contents: Option.some(contents), });}; // ---------------------------------------------------------------------------// Response schema extraction// --------------------------------------------------------------------------- const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value); const stringType = (schema: Record<string, unknown>): boolean => schema.type === "string" || (Array.isArray(schema.type) && schema.type.includes("string")); const numericType = (schema: Record<string, unknown>): boolean => schema.type === "integer" || schema.type === "number" || (Array.isArray(schema.type) && (schema.type.includes("integer") || schema.type.includes("number"))); const normalizedMediaType = (mediaType: string): string => mediaType.split(";")[0]?.trim().toLowerCase() ?? ""; const isJsonMediaType = (mediaType: string): boolean => { const normalized = normalizedMediaType(mediaType); return ( normalized === "application/json" || normalized.includes("+json") || normalized.includes("json") );}; const binaryStringSchema = (schema: Record<string, unknown>): boolean => stringType(schema) && (schema.format === "binary" || schema.format === "byte"); const arrayType = (schema: Record<string, unknown>): boolean => schema.type === "array" || (Array.isArray(schema.type) && schema.type.includes("array")); const nullableType = (schema: Record<string, unknown>): boolean => Array.isArray(schema.type) && schema.type.includes("null"); const isMultipartMediaType = (mediaType: string): boolean => normalizedMediaType(mediaType) === "multipart/form-data"; /** * Replace one binary/byte string node with the tool-file schema, carrying the * spec author's own annotations across. A `type: ["string", "null"]` node stays * nullable as `anyOf: [<file>, { type: "null" }]`, since the tool-file schema is * an object and cannot express null through a type array. */const toolFileSchemaFor = (node: Record<string, unknown>): Record<string, unknown> => { const annotations: Record<string, unknown> = {}; if (typeof node.title === "string") annotations.title = node.title; if (typeof node.description === "string") annotations.description = node.description; return nullableType(node) ? { anyOf: [ToolFileJsonSchema, { type: "null" }], ...annotations } : { ...(ToolFileJsonSchema as Record<string, unknown>), ...annotations };}; /** * Rewrite one multipart property. Deliberately limited to the two shapes the * invoke-side form encoder can actually deliver: a property that IS a binary * string, and an array property whose direct items are binary strings. */const multipartFileProperty = (property: unknown): unknown => { if (!isRecord(property)) return property; if (binaryStringSchema(property)) return toolFileSchemaFor(property); const items = property.items; if (arrayType(property) && isRecord(items) && binaryStringSchema(items)) { return { ...property, items: toolFileSchemaFor(items) }; } return property;}; /** * Advertise `multipart/form-data` binary fields as tool files. * * The rewrite is scoped to the request schema's own `properties` map — never a * blind walk of every object key — so `default`, `example`, and vendor * extensions that happen to look like a binary string schema are left alone. * * Two shapes are NOT rewritten, because the invoke-side encoder cannot honor * them and advertising an input it would silently JSON-stringify is worse than * not advertising it at all: * - binary fields nested inside an object property (only top-level properties * and direct array items become form parts); * - a body schema, or a property, behind a `$ref`. Component schemas are * carried through unresolved by design — the streaming compile path never * materializes `components.schemas` — so there is nothing to inspect here. */const multipartFileInputSchema = (schema: unknown, mediaType: string): unknown => { if (!isMultipartMediaType(mediaType) || !isRecord(schema)) return schema; const properties = schema.properties; if (!isRecord(properties)) return schema; let changed = false; const out: Record<string, unknown> = {}; for (const [name, property] of Object.entries(properties)) { const next = multipartFileProperty(property); if (next !== property) changed = true; out[name] = next; } return changed ? { ...schema, properties: out } : schema;}; const base64EncodingFromDescription = (schema: Record<string, unknown>): "base64" | "base64url" => typeof schema.description === "string" && /base64url|base64-url|url[- ]safe/i.test(schema.description) ? "base64url" : "base64"; const detectFileHint = ( schema: unknown, mediaType: string, r: DocResolver,): OperationFileHint | undefined => { const resolved = isRecord(schema) ? r.resolve<Record<string, unknown>>(schema) : null; if (!resolved) return undefined; if (!isJsonMediaType(mediaType) && binaryStringSchema(resolved)) { return OperationFileHint.make({ kind: "binaryResponse", mimeType: Option.some(mediaType), dataField: Option.none(), sizeField: Option.none(), encoding: Option.none(), }); } if (!isJsonMediaType(mediaType)) return undefined; const properties = resolved.properties; if (!isRecord(properties)) return undefined; const data = properties.data; const dataSchema = isRecord(data) ? r.resolve<Record<string, unknown>>(data) : null; if (!dataSchema || !binaryStringSchema(dataSchema)) return undefined; const size = properties.size; const sizeSchema = isRecord(size) ? r.resolve<Record<string, unknown>>(size) : null; const sizeField = sizeSchema && numericType(sizeSchema) ? "size" : undefined; return OperationFileHint.make({ kind: "byteField", mimeType: Option.some("application/octet-stream"), dataField: Option.some("data"), sizeField: sizeField ? Option.some(sizeField) : Option.none(), encoding: Option.some(base64EncodingFromDescription(dataSchema)), });}; const extractResponseBody = ( operation: OperationObject, r: DocResolver,): OperationResponseBody | undefined => { if (!operation.responses) return undefined; // Success responses may use exact codes ("200"), the OpenAPI wildcard status // key ("2XX" — Microsoft Graph declares every success response this way), or // fall through to "default". Prefer exact codes, then the wildcard, then default. const entries = Object.entries(operation.responses); const preferred = [ ...entries.filter(([s]) => /^2\d\d$/.test(s)).sort(([a], [b]) => a.localeCompare(b)), ...entries.filter(([s]) => /^2xx$/i.test(s)), ...entries.filter(([s]) => s === "default"), ]; for (const [, ref] of preferred) { const resp = r.resolve<ResponseObject>(ref); if (!resp) continue; const content = preferredResponseContent(resp.content); if (content?.media.schema) { return OperationResponseBody.make({ contentType: content.mediaType, schema: Option.some(content.media.schema), fileHint: Option.fromNullishOr(detectFileHint(content.media.schema, content.mediaType, r)), }); } } return undefined;}; /** * Derive an operation's output schema from its response body. NDJSON bodies * (`application/stream+json` and friends) are spec'd per LINE but returned by * the invoke path as an array of parsed lines, so the advertised schema wraps * the line schema in an array; otherwise describe previews promise a single * object that invocations never return. Used by both the whole-tree extract * and the serve path's stored-binding rebuild so the two stay in lockstep. */export const outputSchemaFromResponseBody = ( responseBody: OperationResponseBody,): unknown | undefined => { const schema = Option.getOrUndefined(responseBody.schema); if (schema === undefined) return undefined; return isNdjsonMediaType(responseBody.contentType) ? ndjsonArrayOutputSchema(schema) : schema;}; // ---------------------------------------------------------------------------// Input schema builder// --------------------------------------------------------------------------- // Optional `server` input — host selection + server-URL variables. Undefined// when there's nothing to configure (a single server with no variables).const buildServerInputProperty = ( servers: readonly ServerInfo[],): Record<string, unknown> | undefined => { const variableDefs: Record<string, ServerVariable> = {}; for (const server of servers) { for (const [name, v] of Object.entries(Option.getOrUndefined(server.variables) ?? {})) { if (!(name in variableDefs)) variableDefs[name] = v; } } const hasMultiple = servers.length > 1; const variableNames = Object.keys(variableDefs); if (!hasMultiple && variableNames.length === 0) return undefined; const properties: Record<string, unknown> = {}; if (hasMultiple) { properties.url = { type: "string", enum: servers.map((server) => server.url), default: servers[0]!.url, description: "Which of the spec's servers to send the request to.", }; } if (variableNames.length > 0) { properties.variables = { type: "object", additionalProperties: false, properties: Object.fromEntries( Object.entries(variableDefs).map(([name, v]) => [ name, { type: "string", default: v.default, ...(Option.isSome(v.enum) ? { enum: v.enum.value } : {}), ...(Option.isSome(v.description) ? { description: v.description.value } : {}), }, ]), ), description: "Values for the server URL `{variables}`; spec defaults apply when omitted.", }; } return { type: "object", additionalProperties: false, properties, description: "Optional host selection and server-URL variables for this request.", };}; export const buildInputSchema = ( parameters: readonly OperationParameter[], requestBody: OperationRequestBody | undefined, servers: readonly ServerInfo[],): Record<string, unknown> | undefined => { const properties: Record<string, unknown> = {}; const required: string[] = []; let requiredBodyAlternatives: readonly { readonly required: readonly string[] }[] | undefined; for (const param of parameters) { properties[param.name] = Option.getOrElse(param.schema, () => ({ type: "string" })); if (param.required) required.push(param.name); } // A path/query parameter named `server` takes precedence over the host input. const serverProperty = buildServerInputProperty(servers); if (serverProperty && !("server" in properties)) properties.server = serverProperty; if (requestBody) { // When the spec declares multiple media types for this requestBody, // expose `contentType` so the model can pick. Default = first declared. // For mixed bodies, `body` schema tracks the default; the model is // responsible for supplying a body shape that matches whichever // contentType it picks. Octet-only operations use `bodyBase64` instead. const contents = Option.getOrUndefined(requestBody.contents); const defaultIsOctetStream = requestBody.contentType.split(";")[0]?.trim().toLowerCase() === "application/octet-stream"; const acceptsOctetStream = defaultIsOctetStream || contents?.some( (content) => content.contentType.split(";")[0]?.trim().toLowerCase() === "application/octet-stream", ) === true; const acceptsBody = !defaultIsOctetStream || contents?.some( (content) => content.contentType.split(";")[0]?.trim().toLowerCase() !== "application/octet-stream", ) === true; if (acceptsBody) { properties.body = Option.getOrElse(requestBody.schema, () => ({ type: "object" })); } if (acceptsOctetStream) { properties.bodyBase64 = { type: "string", contentEncoding: "base64", contentMediaType: "application/octet-stream", description: "Base64-encoded bytes for application/octet-stream request bodies. When contentType is omitted, this selects application/octet-stream.", }; } if (requestBody.required) { if (acceptsOctetStream && acceptsBody) { requiredBodyAlternatives = [{ required: ["body"] }, { required: ["bodyBase64"] }]; } else { required.push(acceptsOctetStream ? "bodyBase64" : "body"); } } if (contents && contents.length > 1) { properties.contentType = { type: "string", enum: contents.map((c) => c.contentType), default: requestBody.contentType, description: "Content-Type for the request body. Declared media types for this operation, in spec order.", }; } } if (Object.keys(properties).length === 0) return undefined; return { type: "object", properties, ...(required.length > 0 ? { required } : {}), ...(requiredBodyAlternatives ? { anyOf: requiredBodyAlternatives } : {}), additionalProperties: false, };}; // ---------------------------------------------------------------------------// Operation ID derivation// --------------------------------------------------------------------------- const deriveOperationId = ( method: HttpMethod, pathTemplate: string, operation: OperationObject,): string => operation.operationId ?? (`${method}_${pathTemplate.replace(/[^a-zA-Z0-9]+/g, "_")}`.replace(/^_+|_+$/g, "") ||Lines 1–500 of 1123. Load more
import { Effect, Option } from "effect"; import { ToolFileJsonSchema } from "@executor-js/sdk/core"; import { planToolPaths, type OperationPathInput, type PlannedToolPath } from "./definitions"; import { OpenApiExtractionError } from "./errors"; import type { ParsedDocument } from "./parse"; import { parseEntry, parseHead, parseSmallComponents, type ByteRange, type KeepPathItem, type SpecStructure, } from "./split"; import { declaredContents, DocResolver, isNdjsonMediaType, ndjsonArrayOutputSchema, preferredResponseContent, type OperationObject, type ParameterObject, type PathItemObject, type RequestBodyObject, type ResponseObject, type ServerObject, } from "./openapi-utils"; import { EncodingObject, ExtractedOperation, ExtractionResult, type HttpMethod, MediaBinding, OperationBinding, OperationFileHint, OperationId, OperationParameter, OperationRequestBody, OperationResponseBody, type ParameterLocation, ServerInfo, ServerVariable, } from "./types"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const HTTP_METHODS: readonly HttpMethod[] = [ "get", "put", "post", "delete", "patch", "head", "options", "trace", ]; const VALID_PARAM_LOCATIONS = new Set<string>(["path", "query", "header", "cookie"]); // --------------------------------------------------------------------------- // Parameter extraction // --------------------------------------------------------------------------- const extractParameters = ( pathItem: PathItemObject, operation: OperationObject, r: DocResolver, ): OperationParameter[] => { const merged = new Map<string, ParameterObject>(); for (const raw of pathItem.parameters ?? []) { const p = r.resolve<ParameterObject>(raw); if (!p) continue; merged.set(`${p.in}:${p.name}`, p); } for (const raw of operation.parameters ?? []) { const p = r.resolve<ParameterObject>(raw); if (!p) continue; merged.set(`${p.in}:${p.name}`, p); } return [...merged.values()] .filter((p) => VALID_PARAM_LOCATIONS.has(p.in)) .map((p) => OperationParameter.make({ name: p.name, location: p.in as ParameterLocation, required: p.in === "path" ? true : p.required === true, schema: Option.fromNullishOr(p.schema), style: Option.fromNullishOr(p.style), explode: Option.fromNullishOr(p.explode), allowReserved: Option.fromNullishOr("allowReserved" in p ? p.allowReserved : undefined), description: Option.fromNullishOr(p.description), }), ); }; // --------------------------------------------------------------------------- // Request body extraction // --------------------------------------------------------------------------- const buildEncodingRecord = ( encoding: Record<string, unknown> | undefined, ): Record<string, EncodingObject> | undefined => { if (!encoding) return undefined; const out: Record<string, EncodingObject> = {}; for (const [prop, raw] of Object.entries(encoding)) { if (typeof raw !== "object" || raw === null) continue; const e = raw as { contentType?: string; style?: string; explode?: boolean; allowReserved?: boolean; }; out[prop] = EncodingObject.make({ contentType: Option.fromNullishOr(e.contentType), style: Option.fromNullishOr(e.style), explode: Option.fromNullishOr(e.explode), allowReserved: Option.fromNullishOr(e.allowReserved), }); } return Object.keys(out).length > 0 ? out : undefined; }; const extractRequestBody = ( operation: OperationObject, r: DocResolver, ): OperationRequestBody | undefined => { if (!operation.requestBody) return undefined; const body = r.resolve<RequestBodyObject>(operation.requestBody); if (!body) return undefined; const contents = declaredContents(body.content).map(({ mediaType, media }) => MediaBinding.make({ contentType: mediaType, schema: Option.fromNullishOr(multipartFileInputSchema(media.schema, mediaType)), encoding: Option.fromNullishOr( buildEncodingRecord((media as { encoding?: Record<string, unknown> }).encoding), ), }), ); if (contents.length === 0) return undefined; // Default = first declared (spec author's preferred order). Callers can // override at invoke time with a `contentType` arg. const defaultContent = contents[0]!; return OperationRequestBody.make({ required: body.required === true, contentType: defaultContent.contentType, schema: defaultContent.schema, contents: Option.some(contents), }); }; // --------------------------------------------------------------------------- // Response schema extraction // --------------------------------------------------------------------------- const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value); const stringType = (schema: Record<string, unknown>): boolean => schema.type === "string" || (Array.isArray(schema.type) && schema.type.includes("string")); const numericType = (schema: Record<string, unknown>): boolean => schema.type === "integer" || schema.type === "number" || (Array.isArray(schema.type) && (schema.type.includes("integer") || schema.type.includes("number"))); const normalizedMediaType = (mediaType: string): string => mediaType.split(";")[0]?.trim().toLowerCase() ?? ""; const isJsonMediaType = (mediaType: string): boolean => { const normalized = normalizedMediaType(mediaType); return ( normalized === "application/json" || normalized.includes("+json") || normalized.includes("json") ); }; const binaryStringSchema = (schema: Record<string, unknown>): boolean => stringType(schema) && (schema.format === "binary" || schema.format === "byte"); const arrayType = (schema: Record<string, unknown>): boolean => schema.type === "array" || (Array.isArray(schema.type) && schema.type.includes("array")); const nullableType = (schema: Record<string, unknown>): boolean => Array.isArray(schema.type) && schema.type.includes("null"); const isMultipartMediaType = (mediaType: string): boolean => normalizedMediaType(mediaType) === "multipart/form-data"; /** * Replace one binary/byte string node with the tool-file schema, carrying the * spec author's own annotations across. A `type: ["string", "null"]` node stays * nullable as `anyOf: [<file>, { type: "null" }]`, since the tool-file schema is * an object and cannot express null through a type array. */ const toolFileSchemaFor = (node: Record<string, unknown>): Record<string, unknown> => { const annotations: Record<string, unknown> = {}; if (typeof node.title === "string") annotations.title = node.title; if (typeof node.description === "string") annotations.description = node.description; return nullableType(node) ? { anyOf: [ToolFileJsonSchema, { type: "null" }], ...annotations } : { ...(ToolFileJsonSchema as Record<string, unknown>), ...annotations }; }; /** * Rewrite one multipart property. Deliberately limited to the two shapes the * invoke-side form encoder can actually deliver: a property that IS a binary * string, and an array property whose direct items are binary strings. */ const multipartFileProperty = (property: unknown): unknown => { if (!isRecord(property)) return property; if (binaryStringSchema(property)) return toolFileSchemaFor(property); const items = property.items; if (arrayType(property) && isRecord(items) && binaryStringSchema(items)) { return { ...property, items: toolFileSchemaFor(items) }; } return property; }; /** * Advertise `multipart/form-data` binary fields as tool files. * * The rewrite is scoped to the request schema's own `properties` map — never a * blind walk of every object key — so `default`, `example`, and vendor * extensions that happen to look like a binary string schema are left alone. * * Two shapes are NOT rewritten, because the invoke-side encoder cannot honor * them and advertising an input it would silently JSON-stringify is worse than * not advertising it at all: * - binary fields nested inside an object property (only top-level properties * and direct array items become form parts); * - a body schema, or a property, behind a `$ref`. Component schemas are * carried through unresolved by design — the streaming compile path never * materializes `components.schemas` — so there is nothing to inspect here. */ const multipartFileInputSchema = (schema: unknown, mediaType: string): unknown => { if (!isMultipartMediaType(mediaType) || !isRecord(schema)) return schema; const properties = schema.properties; if (!isRecord(properties)) return schema; let changed = false; const out: Record<string, unknown> = {}; for (const [name, property] of Object.entries(properties)) { const next = multipartFileProperty(property); if (next !== property) changed = true; out[name] = next; } return changed ? { ...schema, properties: out } : schema; }; const base64EncodingFromDescription = (schema: Record<string, unknown>): "base64" | "base64url" => typeof schema.description === "string" && /base64url|base64-url|url[- ]safe/i.test(schema.description) ? "base64url" : "base64"; const detectFileHint = ( schema: unknown, mediaType: string, r: DocResolver, ): OperationFileHint | undefined => { const resolved = isRecord(schema) ? r.resolve<Record<string, unknown>>(schema) : null; if (!resolved) return undefined; if (!isJsonMediaType(mediaType) && binaryStringSchema(resolved)) { return OperationFileHint.make({ kind: "binaryResponse", mimeType: Option.some(mediaType), dataField: Option.none(), sizeField: Option.none(), encoding: Option.none(), }); } if (!isJsonMediaType(mediaType)) return undefined; const properties = resolved.properties; if (!isRecord(properties)) return undefined; const data = properties.data; const dataSchema = isRecord(data) ? r.resolve<Record<string, unknown>>(data) : null; if (!dataSchema || !binaryStringSchema(dataSchema)) return undefined; const size = properties.size; const sizeSchema = isRecord(size) ? r.resolve<Record<string, unknown>>(size) : null; const sizeField = sizeSchema && numericType(sizeSchema) ? "size" : undefined; return OperationFileHint.make({ kind: "byteField", mimeType: Option.some("application/octet-stream"), dataField: Option.some("data"), sizeField: sizeField ? Option.some(sizeField) : Option.none(), encoding: Option.some(base64EncodingFromDescription(dataSchema)), }); }; const extractResponseBody = ( operation: OperationObject, r: DocResolver, ): OperationResponseBody | undefined => { if (!operation.responses) return undefined; // Success responses may use exact codes ("200"), the OpenAPI wildcard status // key ("2XX" — Microsoft Graph declares every success response this way), or // fall through to "default". Prefer exact codes, then the wildcard, then default. const entries = Object.entries(operation.responses); const preferred = [ ...entries.filter(([s]) => /^2\d\d$/.test(s)).sort(([a], [b]) => a.localeCompare(b)), ...entries.filter(([s]) => /^2xx$/i.test(s)), ...entries.filter(([s]) => s === "default"), ]; for (const [, ref] of preferred) { const resp = r.resolve<ResponseObject>(ref); if (!resp) continue; const content = preferredResponseContent(resp.content); if (content?.media.schema) { return OperationResponseBody.make({ contentType: content.mediaType, schema: Option.some(content.media.schema), fileHint: Option.fromNullishOr(detectFileHint(content.media.schema, content.mediaType, r)), }); } } return undefined; }; /** * Derive an operation's output schema from its response body. NDJSON bodies * (`application/stream+json` and friends) are spec'd per LINE but returned by * the invoke path as an array of parsed lines, so the advertised schema wraps * the line schema in an array; otherwise describe previews promise a single * object that invocations never return. Used by both the whole-tree extract * and the serve path's stored-binding rebuild so the two stay in lockstep. */ export const outputSchemaFromResponseBody = ( responseBody: OperationResponseBody, ): unknown | undefined => { const schema = Option.getOrUndefined(responseBody.schema); if (schema === undefined) return undefined; return isNdjsonMediaType(responseBody.contentType) ? ndjsonArrayOutputSchema(schema) : schema; }; // --------------------------------------------------------------------------- // Input schema builder // --------------------------------------------------------------------------- // Optional `server` input — host selection + server-URL variables. Undefined // when there's nothing to configure (a single server with no variables). const buildServerInputProperty = ( servers: readonly ServerInfo[], ): Record<string, unknown> | undefined => { const variableDefs: Record<string, ServerVariable> = {}; for (const server of servers) { for (const [name, v] of Object.entries(Option.getOrUndefined(server.variables) ?? {})) { if (!(name in variableDefs)) variableDefs[name] = v; } } const hasMultiple = servers.length > 1; const variableNames = Object.keys(variableDefs); if (!hasMultiple && variableNames.length === 0) return undefined; const properties: Record<string, unknown> = {}; if (hasMultiple) { properties.url = { type: "string", enum: servers.map((server) => server.url), default: servers[0]!.url, description: "Which of the spec's servers to send the request to.", }; } if (variableNames.length > 0) { properties.variables = { type: "object", additionalProperties: false, properties: Object.fromEntries( Object.entries(variableDefs).map(([name, v]) => [ name, { type: "string", default: v.default, ...(Option.isSome(v.enum) ? { enum: v.enum.value } : {}), ...(Option.isSome(v.description) ? { description: v.description.value } : {}), }, ]), ), description: "Values for the server URL `{variables}`; spec defaults apply when omitted.", }; } return { type: "object", additionalProperties: false, properties, description: "Optional host selection and server-URL variables for this request.", }; }; export const buildInputSchema = ( parameters: readonly OperationParameter[], requestBody: OperationRequestBody | undefined, servers: readonly ServerInfo[], ): Record<string, unknown> | undefined => { const properties: Record<string, unknown> = {}; const required: string[] = []; let requiredBodyAlternatives: readonly { readonly required: readonly string[] }[] | undefined; for (const param of parameters) { properties[param.name] = Option.getOrElse(param.schema, () => ({ type: "string" })); if (param.required) required.push(param.name); } // A path/query parameter named `server` takes precedence over the host input. const serverProperty = buildServerInputProperty(servers); if (serverProperty && !("server" in properties)) properties.server = serverProperty; if (requestBody) { // When the spec declares multiple media types for this requestBody, // expose `contentType` so the model can pick. Default = first declared. // For mixed bodies, `body` schema tracks the default; the model is // responsible for supplying a body shape that matches whichever // contentType it picks. Octet-only operations use `bodyBase64` instead. const contents = Option.getOrUndefined(requestBody.contents); const defaultIsOctetStream = requestBody.contentType.split(";")[0]?.trim().toLowerCase() === "application/octet-stream"; const acceptsOctetStream = defaultIsOctetStream || contents?.some( (content) => content.contentType.split(";")[0]?.trim().toLowerCase() === "application/octet-stream", ) === true; const acceptsBody = !defaultIsOctetStream || contents?.some( (content) => content.contentType.split(";")[0]?.trim().toLowerCase() !== "application/octet-stream", ) === true; if (acceptsBody) { properties.body = Option.getOrElse(requestBody.schema, () => ({ type: "object" })); } if (acceptsOctetStream) { properties.bodyBase64 = { type: "string", contentEncoding: "base64", contentMediaType: "application/octet-stream", description: "Base64-encoded bytes for application/octet-stream request bodies. When contentType is omitted, this selects application/octet-stream.", }; } if (requestBody.required) { if (acceptsOctetStream && acceptsBody) { requiredBodyAlternatives = [{ required: ["body"] }, { required: ["bodyBase64"] }]; } else { required.push(acceptsOctetStream ? "bodyBase64" : "body"); } } if (contents && contents.length > 1) { properties.contentType = { type: "string", enum: contents.map((c) => c.contentType), default: requestBody.contentType, description: "Content-Type for the request body. Declared media types for this operation, in spec order.", }; } } if (Object.keys(properties).length === 0) return undefined; return { type: "object", properties, ...(required.length > 0 ? { required } : {}), ...(requiredBodyAlternatives ? { anyOf: requiredBodyAlternatives } : {}), additionalProperties: false, }; }; // --------------------------------------------------------------------------- // Operation ID derivation // --------------------------------------------------------------------------- const deriveOperationId = ( method: HttpMethod, pathTemplate: string, operation: OperationObject, ): string => operation.operationId ?? (`${method}_${pathTemplate.replace(/[^a-zA-Z0-9]+/g, "_")}`.replace(/^_+|_+$/g, "") ||