export default async()

in packages/type-safe-api/scripts/type-safe-api/parser/parse-openapi-spec.ts [94:209]


export default async (argv: string[]) => {
  const args = parse<Arguments>({
    specPath: { type: String, alias: "s" },
    smithyJsonPath: { type: String, optional: true },
    outputPath: { type: String, alias: "o" },
  }, { argv });

  const spec = (await SwaggerParser.bundle(args.specPath)) as any;

  if (args.smithyJsonPath) {
    // Read the operations out of the Smithy model
    const smithyModel = JSON.parse(
      fs.readFileSync(args.smithyJsonPath, "utf-8")
    );

    // Retrieve all services from the smithy model
    const services: SmithyServiceDetails[] = Object.entries(smithyModel.shapes).filter(([, shape]: [string, any]) =>
      shape.type === "service" && shape.traits).map(([id, shape]: [string, any]) => ({
        id,
        traits: shape.traits,
      }));

    // Apply all service-level traits as vendor extensions at the top level of the spec
    services.forEach((service) => {
      Object.entries(service.traits).forEach(([traitId, value]) => {
        const vendorExtension = getVendorExtensionFromTrait(traitId);
        spec[vendorExtension] = value;
      });
    });

    // Retrieve all operations from the smithy model
    const operations: SmithyOperationDetails[] = Object.entries(
      smithyModel.shapes
    )
      .filter(
        ([, shape]: [string, any]) =>
          shape.type === "operation" &&
          shape.traits &&
          SMITHY_HTTP_TRAIT_ID in shape.traits
      )
      .map(([id, shape]: [string, any]) => ({
        id,
        method: shape.traits[SMITHY_HTTP_TRAIT_ID].method?.toLowerCase(),
        path: shape.traits[SMITHY_HTTP_TRAIT_ID].uri,
        traits: shape.traits,
        input: smithyModel.shapes[shape.input?.target],
      }));

    // Apply all operation-level traits as vendor extensions to the relevant operation in the spec
    operations.forEach((operation) => {
      if (spec.paths?.[operation.path]?.[operation.method]) {
        Object.entries(operation.traits).forEach(([traitId, value]) => {
          const vendorExtension = getVendorExtensionFromTrait(traitId);

          let extensionValue = value;

          // The smithy paginated trait is written in terms of inputs which may have different names in openapi
          // so we must map them here
          if (vendorExtension === PAGINATED_VENDOR_EXTENSION) {
            extensionValue = Object.fromEntries(Object.entries(value as {[key: string]: string}).map(([traitProperty, memberName]) => {
              const member = operation.input?.members?.[memberName];
              const renamedMemberName = SMITHY_RENAME_TRAITS.map(trait => member?.traits?.[trait]).find(x => x) ?? memberName;
              return [traitProperty, renamedMemberName];
            }));
          }

          spec.paths[operation.path][operation.method][vendorExtension] = extensionValue;
        });
      }
    });
  }

  const invalidRequestParameters: InvalidRequestParameter[] = [];

  // Dereference a clone of the spec to test parameters
  const dereferencedSpec = await SwaggerParser.dereference(JSON.parse(JSON.stringify(spec)), {
    dereference: {
      // Circular references are valid, we just ignore them for the purpose of validation
      circular: "ignore",
    },
  });

  // Validate the request parameters
  Object.entries(dereferencedSpec.paths || {}).forEach(([p, pathOp]: [string, any]) => {
    Object.entries(pathOp ?? {}).forEach(([method, operation]: [string, any]) => {
      (operation?.parameters ?? []).forEach((parameter: any) => {
        // Check if the parameter is an allowed type
        if (VALID_REQUEST_PARAMETER_TYPES.has(parameter?.schema?.type)) {
          return;
        }
        // Check if the parameter is an array of the allowed type
        if ("array" === parameter?.schema?.type && VALID_REQUEST_PARAMETER_TYPES.has(parameter?.schema?.items?.type)) {
          return;
        }

        // Parameter is invalid
        invalidRequestParameters.push({
          method,
          path: p,
          operationId: parameter?.operationId,
          parameterName: parameter?.name,
        });
      });
    });
  });

  if (invalidRequestParameters.length > 0) {
    const parameterErrors = invalidRequestParameters.map((p) => `${p.operationId ?? `${p.method} ${p.path}`}: ${p.parameterName}`).join('\n');
    console.error(`Request parameters must be of type ${[...VALID_REQUEST_PARAMETER_TYPES].join(', ')} or arrays of these. Found invalid parameters:\n${parameterErrors}`);
    process.exit(1);
  }

  writeFile(args.outputPath, JSON.stringify(spec, null, 2), {
    readonly: true,
  });
};