1525 lines · 46.4 KB · TypeScript
RawDownload
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.