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,
});
};