Rhys Sullivantrim sizes71e33a97 months agoHistory
893 lines · 26.6 KB · TypeScript
RawDownload
import * as Data from "effect/Data";import * as Effect from "effect/Effect";import { pipe } from "effect/Function";import * as ParseResult from "effect/ParseResult";import * as Schema from "effect/Schema"; import { parseOpenApiDocument } from "./openapi-document";import { extractOpenApiManifestJsonWithWasm } from "./openapi-extractor-wasm";import {  OpenApiToolManifestSchema,  type OpenApiExtractedTool,  type OpenApiHeader,  type OpenApiSecurityRequirement,  type OpenApiServer,  type OpenApiJsonObject,  type OpenApiSpecInput,  type OpenApiToolManifest,} from "./openapi-types"; type OpenApiExtractionStage = "validate" | "extract"; export class OpenApiExtractionError extends Data.TaggedError("OpenApiExtractionError")<{  sourceName: string;  stage: OpenApiExtractionStage;  message: string;  details: string | null;}> {} const manifestFromJsonSchema = Schema.parseJson(OpenApiToolManifestSchema);const decodeManifestFromJson = Schema.decodeUnknown(manifestFromJsonSchema); const toExtractionError = (  sourceName: string,  stage: OpenApiExtractionStage,  cause: unknown,): OpenApiExtractionError =>  cause instanceof OpenApiExtractionError    ? cause    : new OpenApiExtractionError({        sourceName,        stage,        message: "OpenAPI extraction failed",        details: ParseResult.isParseError(cause)          ? ParseResult.TreeFormatter.formatErrorSync(cause)          : String(cause),      }); const normalizeOpenApiDocumentText = (  sourceName: string,  openApiSpec: OpenApiSpecInput,): Effect.Effect<string, OpenApiExtractionError> => {  if (typeof openApiSpec === "string") {    return Effect.succeed(openApiSpec);  }   return Effect.try({    try: () => JSON.stringify(openApiSpec),    catch: (cause) =>      new OpenApiExtractionError({        sourceName,        stage: "validate",        message: "Unable to serialize OpenAPI input",        details: String(cause),      }),  });}; 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 collectRefKeys = (  value: unknown,  refs: Set<string>,): void => {  if (Array.isArray(value)) {    for (const item of value) {      collectRefKeys(item, refs);    }    return;  }   if (value === null || typeof value !== "object") {    return;  }   const record = value as Record<string, unknown>;  if (typeof record.$ref === "string" && record.$ref.startsWith("#/")) {    refs.add(record.$ref);  }   for (const nested of Object.values(record)) {    collectRefKeys(nested, refs);  }}; 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;}; const responseStatusRank = (statusCode: string): number => {  if (/^2\d\d$/.test(statusCode)) {    return 0;  }   if (statusCode === "default") {    return 1;  }   return 2;}; 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 = (  content: unknown,): unknown | undefined => {  return preferredContentEntry(content)?.[1].schema;}; const contentEntriesFromContent = (  document: OpenApiJsonObject,  content: unknown,): ReadonlyArray<{  mediaType: string;  schema?: unknown;  examples?: Array<{    valueJson: string;    mediaType?: string;    label?: string;  }>;}> => {  const entries = 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: mediaRecord.schema } : {}),        ...(examples.length > 0 ? { examples } : {}),      };    });   return entries;}; const examplesFromValue = (  value: unknown,  input: {    label?: string;    mediaType?: string;  } = {},): Array<{  valueJson: string;  mediaType?: string;  label?: string;}> => {  const record = asObject(value);  const examples: Array<{    valueJson: string;    mediaType?: string;    label?: string;  }> = [];   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<{  valueJson: string;  mediaType?: string;  label?: string;}> => examplesFromValue(schema); const examplesFromMediaType = (  mediaType: string,  mediaTypeRecord: Record<string, unknown>,): Array<{  valueJson: string;  mediaType?: string;  label?: string;}> => {  const direct = examplesFromValue(mediaTypeRecord, { mediaType });  if (direct.length > 0) {    return direct;  }   return examplesFromSchema(mediaTypeRecord.schema).map((example) => ({    ...example,    mediaType,  }));}; const headerFromValue = (  document: OpenApiJsonObject,  name: string,  value: unknown,): OpenApiHeader | undefined => {  const header = asObject(resolveLocalRef(document, value));  if (Object.keys(header).length === 0) {    return undefined;  }   const content = contentEntriesFromContent(document, header.content);  const directExamples =    content.length > 0      ? []      : examplesFromValue(header);   return {    name,    ...(asTrimmedString(header.description)      ? { description: asTrimmedString(header.description) }      : {}),    ...(typeof header.required === "boolean" ? { required: header.required } : {}),    ...(typeof header.deprecated === "boolean" ? { deprecated: header.deprecated } : {}),    ...(header.schema !== undefined ? { schema: header.schema } : {}),    ...(content.length > 0 ? { content } : {}),    ...(asTrimmedString(header.style)      ? { style: asTrimmedString(header.style) }      : {}),    ...(typeof header.explode === "boolean" ? { explode: header.explode } : {}),    ...(directExamples.length > 0 ? { examples: directExamples } : {}),  };}; const headersFromValue = (  document: OpenApiJsonObject,  value: unknown,): ReadonlyArray<OpenApiHeader> => Object.entries(asObject(value))  .sort(([left], [right]) => left.localeCompare(right))  .flatMap(([name, headerValue]) => {    const header = headerFromValue(document, name, headerValue);    return header ? [header] : [];  }); const serversFromValue = (  value: unknown,): ReadonlyArray<OpenApiServer> =>  asArray(value)    .map((entry) => asObject(entry))    .flatMap((server) => {      const url = asTrimmedString(server.url);      if (!url) {        return [];      }       const variables = Object.fromEntries(        Object.entries(asObject(server.variables))          .sort(([left], [right]) => left.localeCompare(right))          .flatMap(([name, variableValue]) => {            const variableRecord = asObject(variableValue);            const defaultValue = asTrimmedString(variableRecord.default);            return defaultValue ? [[name, defaultValue] as const] : [];          }),      );       return [{        url,        ...(asTrimmedString(server.description)          ? { description: asTrimmedString(server.description) }          : {}),        ...(Object.keys(variables).length > 0 ? { variables } : {}),      }];    }); const pathItemForTool = (  document: OpenApiJsonObject,  tool: OpenApiExtractedTool,): Record<string, unknown> =>  asObject(asObject(document.paths)[tool.path]); const parameterKey = (input: {  location: string;  name: string;}): string => `${input.location}:${input.name}`; const parameterRecordMapForTool = (  document: OpenApiJsonObject,  tool: OpenApiExtractedTool,): ReadonlyMap<string, Record<string, unknown>> => {  const merged = new Map<string, Record<string, unknown>>();  const pathItem = pathItemForTool(document, tool);  const operation = operationForTool(document, tool);   for (const parameterValue of asArray(pathItem.parameters)) {    const parameter = asObject(resolveLocalRef(document, parameterValue));    const name = asTrimmedString(parameter.name);    const location = asTrimmedString(parameter.in);    if (!name || !location) {      continue;    }     merged.set(parameterKey({ location, name }), parameter);  }   for (const parameterValue of asArray(operation.parameters)) {    const parameter = asObject(resolveLocalRef(document, parameterValue));    const name = asTrimmedString(parameter.name);    const location = asTrimmedString(parameter.in);    if (!name || !location) {      continue;    }     merged.set(parameterKey({ location, name }), parameter);  }   return merged;}; const operationForTool = (  document: OpenApiJsonObject,  tool: OpenApiExtractedTool,): Record<string, unknown> =>  asObject(    asObject(asObject(document.paths)[tool.path])[tool.method],  ); const requestBodySchemaForTool = (  document: OpenApiJsonObject,  tool: OpenApiExtractedTool,): unknown | undefined => {  const operation = operationForTool(document, tool);  if (Object.keys(operation).length === 0) {    return undefined;  }   const requestBody = resolveLocalRef(document, operation.requestBody);  return contentSchemaFromOperationContent(asObject(requestBody).content);}; const requestBodyPayloadForTool = (  document: OpenApiJsonObject,  tool: OpenApiExtractedTool,): OpenApiExtractedTool["invocation"]["requestBody"] => {  const operation = operationForTool(document, tool);  if (Object.keys(operation).length === 0) {    return tool.invocation.requestBody;  }   const requestBody = asObject(resolveLocalRef(document, operation.requestBody));  if (Object.keys(requestBody).length === 0) {    return tool.invocation.requestBody;  }   const contents = contentEntriesFromContent(document, requestBody.content);  const contentTypes = contents.map((content) => content.mediaType);   return {    required:      typeof requestBody.required === "boolean"        ? requestBody.required        : (tool.invocation.requestBody?.required ?? false),    contentTypes,    ...(contents.length > 0 ? { contents } : {}),  };}; const responseSchemaForTool = (  document: OpenApiJsonObject,  tool: OpenApiExtractedTool,): unknown | undefined => {  const operation = operationForTool(document, tool);  if (Object.keys(operation).length === 0) {    return undefined;  }   const responseEntries = Object.entries(asObject(operation.responses));  const preferredResponses = responseEntries    .filter(([status]) => /^2\d\d$/.test(status))    .sort(([left], [right]) => left.localeCompare(right));  const fallbackResponses = responseEntries.filter(([status]) => status === "default");   for (const [, responseValue] of [...preferredResponses, ...fallbackResponses]) {    const response = resolveLocalRef(document, responseValue);    const schema = contentSchemaFromOperationContent(asObject(response).content);    if (schema !== undefined) {      return schema;    }  }   return undefined;}; const responseVariantsForTool = (  document: OpenApiJsonObject,  tool: OpenApiExtractedTool,): OpenApiExtractedTool["responses"] => {  const operation = operationForTool(document, tool);  if (Object.keys(operation).length === 0) {    return undefined;  }   const responseEntries = Object.entries(asObject(operation.responses))    .sort(([left], [right]) =>      responseStatusRank(left) - responseStatusRank(right) || left.localeCompare(right),    );   const responses = responseEntries.map(([statusCode, responseValue]) => {    const response = asObject(resolveLocalRef(document, responseValue));    const contents = contentEntriesFromContent(document, response.content);

Lines 1–500 of 893.