UNPKG

fastify-openapi-glue

Version:

generate a fastify configuration from an openapi specification

139 lines (123 loc) 3.56 kB
// baseclass for the v2 and v3 parsers export const HttpOperations = new Set([ "delete", "get", "head", "patch", "post", "put", "options", ]); function escapeJsonPointer(str) { return str.replace(/~/g, "~0").replace(/\//g, "~1"); } export class ParserBase { constructor() { this.config = { generic: {}, routes: [], contentTypes: new Set() }; } makeOperationId(operation, path) { // make a nice camelCase operationID // e.g. get /user/{name} becomes getUserByName const firstUpper = (str) => str.substring(0, 1).toUpperCase() + str.substring(1); const by = (_matched, p1) => `By${firstUpper(p1)}`; const parts = path.split("/").slice(1); parts.unshift(operation); const opId = parts .map((item, i) => (i > 0 ? firstUpper(item) : item)) .join("") .replace(/{(\w+)}/g, by) .replace(/[^a-z]/gi, ""); return opId; } makeURL(path) { // fastify wants 'path/:param' instead of openapis 'path/{param}' return path.replace(/{(\w+)}/g, ":$1"); } processSchema(schemas, item) { // clone schema to avoid side effects const obj = structuredClone(schemas[item]); let refAdded = false; function inspectNode(obj, path, paths) { if (typeof obj === "object" && obj !== null) { if (paths.has(obj)) { return paths.get(obj); } const newPaths = new Map(paths); newPaths.set(obj, path); for (const [key, value] of Object.entries(obj)) { const $ref = inspectNode( value, `${path}/${escapeJsonPointer(key)}`, newPaths, ); if (typeof $ref === "string") { obj[key] = { $ref }; refAdded = true; } } } return undefined; } const paths = new Map(); inspectNode(obj, "#", paths); // AJV requires an $id attribute for references to work if (refAdded && typeof obj["$id"] === "undefined") { obj["$id"] = "http://example.com/fastifySchema"; } schemas[item] = obj; } removeRecursion(schemas) { for (const [key, subschema] of Object.entries(schemas)) { if (key === "response") { this.processResponseSchema(subschema); } else { // some schemas are in the form of "body->content->mimeType->schema" if (subschema.content) { for (const [_contentType, contentTypeObj] of Object.entries( subschema.content, )) { this.processSchema(contentTypeObj, "schema"); } } // all others are in the form of "query->schema" etc else { this.processSchema(schemas, key); } } } } processOperation(path, operation, operationSpec, genericSchema) { if (operationSpec["x-no-fastify-config"]) { return; } const route = { method: operation.toUpperCase(), url: this.makeURL(path), schema: this.makeSchema(genericSchema, operationSpec), openapiPath: path, operationId: operationSpec.operationId || this.makeOperationId(operation, path), openapiSource: operationSpec, security: operationSpec.security || this.spec.security, }; if (operationSpec["x-fastify-config"]) { route.config = operationSpec["x-fastify-config"]; } this.config.routes.push(route); } processPaths(paths) { for (const [path, pathSpec] of Object.entries(paths)) { const { parameters, ...operations } = pathSpec; const genericSchema = {}; if (typeof parameters === "object") { this.parseParameters(genericSchema, parameters); } for (const [operation, operationSpec] of Object.entries(operations)) { if (HttpOperations.has(operation)) { this.processOperation(path, operation, operationSpec, genericSchema); } } } } }