"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.genOpenAPI = exports.OpenAPIType = void 0; const plugin_framework_1 = require("@protobuf-ts/plugin-framework"); const yaml = __importStar(require("yaml")); const local_type_name_1 = require("../local-type-name"); const gateway_1 = require("./gateway"); var OpenAPIType; (function (OpenAPIType) { OpenAPIType[OpenAPIType["GATEWAY"] = 0] = "GATEWAY"; OpenAPIType[OpenAPIType["TWIRP"] = 1] = "TWIRP"; })(OpenAPIType = exports.OpenAPIType || (exports.OpenAPIType = {})); /** * Generate twirp compliant OpenAPI doc * @param ctx * @param files * @param type */ function genOpenAPI(ctx, files, type) { return __awaiter(this, void 0, void 0, function* () { const documents = []; files.forEach(file => { file.service.forEach((service) => { var _a, _b; const document = { openapi: "3.0.3", info: { title: `${service.name}`, version: "1.0.0", description: genDescription(ctx, service), }, paths: type === OpenAPIType.TWIRP ? genTwirpPaths(ctx, file, service) : genGatewayPaths(ctx, file, service), components: genComponents(ctx, service.method), }; const fileName = type === OpenAPIType.TWIRP ? `${(_a = service.name) === null || _a === void 0 ? void 0 : _a.toLowerCase()}.twirp.openapi.yaml` : `${(_b = service.name) === null || _b === void 0 ? void 0 : _b.toLowerCase()}.openapi.yaml`; documents.push({ fileName, content: yaml.stringify(document), }); }); }); return documents; }); } exports.genOpenAPI = genOpenAPI; /** * Generates OpenAPI Twirp URI paths * @param ctx * @param file * @param service */ function genTwirpPaths(ctx, file, service) { return service.method.reduce((paths, method) => { const description = genDescription(ctx, method); paths[`/${file.package ? file.package + "." : ""}${service.name}/${method.name}`] = { post: { summary: description, operationId: `${service.name}_${method.name}`, requestBody: { required: true, content: { "application/json": { schema: { $ref: genRef(ctx, method.inputType) } } } }, responses: { "200": { description: "OK", content: { "application/json": { schema: { $ref: genRef(ctx, method.outputType), } } } } } } }; return paths; }, {}); } /** * Generates OpenAPI Twrip Gateway URI paths * @param ctx * @param file * @param service */ function genGatewayPaths(ctx, file, service) { const registry = ctx.registry; /** * Build paths recursively * @param method * @param httpSpec * @param paths */ function buildPath(method, httpSpec, paths) { const httpMethod = gateway_1.getMethod(httpSpec); const description = genDescription(ctx, method); const pathItem = { [httpMethod]: { summary: description, operationId: `${service.name}_${method.name}`, } }; const inputMessage = registry.resolveTypeName(method.inputType); const outPutMessage = registry.resolveTypeName(method.outputType); // All methods except GET have body if (httpMethod !== gateway_1.Pattern.GET) { pathItem[httpMethod].requestBody = genGatewayBody(ctx, httpSpec, inputMessage); } // All methods might have params pathItem[httpMethod].parameters = genGatewayParams(ctx, httpSpec, inputMessage); pathItem[httpMethod].responses = genGatewayResponse(ctx, httpSpec, outPutMessage); paths[`${httpSpec[httpMethod]}`] = pathItem; if (httpSpec.additional_bindings) { buildPath(method, httpSpec.additional_bindings, paths); } } return service.method.reduce((paths, method) => { const options = ctx.interpreter.readOptions(method); if (!options || options && !options["google.api.http"]) { return paths; } const httpSpec = options["google.api.http"]; buildPath(method, httpSpec, paths); return paths; }, {}); } /** * Generate OpenAPI Gateway Response * @param ctx * @param httpOptions * @param message */ function genGatewayResponse(ctx, httpOptions, message) { let schema = {}; if (httpOptions.responseBody) { schema = { type: "object", properties: { [httpOptions.responseBody]: { $ref: `#/components/schemas/${message.name}` } } }; } else { schema = { $ref: `#/components/schemas/${message.name}` }; } return { "200": { description: "OK", content: { "application/json": { schema, } } } }; } /** * Generate OpenAPI Gateway Response * @param ctx * @param httpOptions * @param message */ function genGatewayBody(ctx, httpOptions, message) { const schema = {}; if (httpOptions.body === "*") { schema.$ref = `#/components/schemas/${message.name}`; } else if (httpOptions.body) { const subField = message.field.find(field => field.name === httpOptions.body); if (!subField) { throw new Error(`the body field ${httpOptions.body} cannot be mapped to message ${message.name}`); } schema.properties = { [httpOptions.body]: genField(ctx, subField), }; } return { required: true, content: { "application/json": { schema, } } }; } /** * Generates OpenAPI Gateway Parameters * @param ctx * @param httpOptions * @param message */ function genGatewayParams(ctx, httpOptions, message) { const httpMethod = gateway_1.getMethod(httpOptions); const params = parseUriParams(httpOptions[httpMethod]); const urlParams = message.field .filter((field) => params.find((param) => param === field.name)) .map((field) => { return { name: field.name, in: "path", required: true, schema: Object.assign({}, genField(ctx, field)) }; }); if (httpOptions.body === "*") { return urlParams; } const queryString = message.field .filter((field) => field.name !== httpOptions.body && !params.find(param => param === field.name)) .map((field) => { return { name: field.name, in: "query", schema: Object.assign({}, genField(ctx, field)) }; }); return [ ...queryString, ...urlParams, ]; } /** * Generates OpenAPI Components * @param ctx * @param methods */ function genComponents(ctx, methods) { const components = { schemas: {} }; methods.reduce((schemas, method) => { genSchema(ctx, schemas, method.inputType); genSchema(ctx, schemas, method.outputType); return schemas; }, components.schemas); return components; } /** * Generate OpenAPI Schemas * @param ctx * @param schemas * @param typeName */ function genSchema(ctx, schemas, typeName) { const registry = ctx.registry; const localName = localMessageName(ctx, typeName); if (!localName) { return; } const descriptor = registry.resolveTypeName(typeName); if (schemas[localName]) { return; } // Handle OneOf if (descriptor.field.some((field) => registry.isUserDeclaredOneof(field))) { schemas[localName] = genOneOfType(ctx, descriptor); descriptor.oneofDecl.forEach((oneOfField, index) => { const oneOfTyName = `${localName}_${capitalizeFirstLetter(oneOfField.name)}`; const oneOfFields = descriptor.field.filter(field => { return field.oneofIndex === index; }); schemas[oneOfTyName] = genOneOfTypeKind(ctx, descriptor, oneOfFields); }); } else { schemas[localName] = genType(ctx, descriptor); } descriptor.field.forEach((field) => { if (field.type !== plugin_framework_1.FieldDescriptorProto_Type.MESSAGE || !registry.isMapField(field)) { return; } if (registry.isMapField(field)) { const entry = registry.resolveTypeName(field.typeName); if (plugin_framework_1.DescriptorProto.is(entry)) { const valueField = entry.field.find(fd => fd.number === 2); if (!valueField) { return; } if (valueField.type !== plugin_framework_1.FieldDescriptorProto_Type.MESSAGE) { return; } field = valueField; } } else if (registry.isSyntheticElement(descriptor)) { return; } genSchema(ctx, schemas, field.typeName); }); } /** * Generate an OpenAPI type * @param ctx * @param message */ function genType(ctx, message) { const description = genDescription(ctx, message); return { properties: genMessageProperties(ctx, message), description, }; } /** * Generate a Protobuf to OpenAPI oneof type * @param ctx * @param message */ function genOneOfType(ctx, message) { const description = genDescription(ctx, message); const oneOf = { allOf: [ { type: "object", properties: genMessageProperties(ctx, message), }, ], description, }; message.oneofDecl.forEach((field) => { oneOf.allOf.push({ $ref: `#/components/schemas/${message.name}_${capitalizeFirstLetter(field.name)}` }); }); return oneOf; } /** * Generate one of type * @param ctx * @param message * @param oneOfFields */ function genOneOfTypeKind(ctx, message, oneOfFields) { return { oneOf: oneOfFields.map((oneOf) => { return { type: "object", properties: { [oneOf.name]: genField(ctx, oneOf), } }; }) }; } /** * Generate message properties * @param ctx * @param message */ function genMessageProperties(ctx, message) { const registry = ctx.registry; return message.field.reduce((fields, field) => { if (registry.isUserDeclaredOneof(field)) { return fields; } fields[field.name] = genField(ctx, field); return fields; }, {}); } /** * Generates OpenAPI $ref * @param ctx * @param name */ function genRef(ctx, name) { const messageType = localMessageName(ctx, name); return `#/components/schemas/${messageType}`; } /** * Generate field definition * @param ctx * @param field */ function genField(ctx, field) { let openApiType; const registry = ctx.registry; switch (field.type) { case plugin_framework_1.FieldDescriptorProto_Type.DOUBLE: case plugin_framework_1.FieldDescriptorProto_Type.FLOAT: case plugin_framework_1.FieldDescriptorProto_Type.BOOL: case plugin_framework_1.FieldDescriptorProto_Type.STRING: case plugin_framework_1.FieldDescriptorProto_Type.FIXED32: case plugin_framework_1.FieldDescriptorProto_Type.FIXED64: case plugin_framework_1.FieldDescriptorProto_Type.INT32: case plugin_framework_1.FieldDescriptorProto_Type.INT64: case plugin_framework_1.FieldDescriptorProto_Type.SFIXED32: case plugin_framework_1.FieldDescriptorProto_Type.SFIXED64: case plugin_framework_1.FieldDescriptorProto_Type.SINT32: case plugin_framework_1.FieldDescriptorProto_Type.SINT64: case plugin_framework_1.FieldDescriptorProto_Type.UINT32: case plugin_framework_1.FieldDescriptorProto_Type.UINT64: openApiType = { type: genScalar(field.type), }; break; case plugin_framework_1.FieldDescriptorProto_Type.BYTES: openApiType = { type: "array", items: { type: "integer", } }; break; case plugin_framework_1.FieldDescriptorProto_Type.ENUM: const enumType = registry.getEnumFieldEnum(field); openApiType = genEnum(enumType); break; case plugin_framework_1.FieldDescriptorProto_Type.MESSAGE: // Map type if (registry.isMapField(field)) { const mapTypeValue = registry.getMapValueType(field); if (typeof mapTypeValue === "number") { const scalar = mapTypeValue; openApiType = { type: "object", additionalProperties: { type: genScalar(scalar) } }; } else if (plugin_framework_1.EnumDescriptorProto.is(mapTypeValue)) { openApiType = { type: "object", additionalProperties: Object.assign({}, genEnum(mapTypeValue)) }; } else if (plugin_framework_1.DescriptorProto.is(mapTypeValue)) { openApiType = { type: "object", additionalProperties: { $ref: `#/components/schemas/${mapTypeValue.name}`, } }; } else { throw new Error("map value not supported"); } break; } openApiType = { $ref: genRef(ctx, field.typeName), }; break; default: throw new Error(`${field.name} of type ${field.type} not supported`); } const description = genDescription(ctx, field); if (field.label === plugin_framework_1.FieldDescriptorProto_Label.REPEATED && !registry.isMapField(field)) { return { type: "array", items: openApiType, description: description || "", }; } if (field.type !== plugin_framework_1.FieldDescriptorProto_Type.MESSAGE) { openApiType.description = description || ""; } return openApiType; } /** * Generates enum definition * @param enumType */ function genEnum(enumType) { return { type: 'string', enum: enumType.value.map((value) => { return value.name; }) }; } /** * Generate scalar * @param type */ function genScalar(type) { switch (type) { case plugin_framework_1.FieldDescriptorProto_Type.BOOL: return "boolean"; case plugin_framework_1.FieldDescriptorProto_Type.DOUBLE: case plugin_framework_1.FieldDescriptorProto_Type.FLOAT: return "number"; case plugin_framework_1.FieldDescriptorProto_Type.STRING: return "string"; case plugin_framework_1.FieldDescriptorProto_Type.FIXED32: case plugin_framework_1.FieldDescriptorProto_Type.FIXED64: case plugin_framework_1.FieldDescriptorProto_Type.INT32: case plugin_framework_1.FieldDescriptorProto_Type.INT64: case plugin_framework_1.FieldDescriptorProto_Type.SFIXED32: case plugin_framework_1.FieldDescriptorProto_Type.SFIXED64: case plugin_framework_1.FieldDescriptorProto_Type.SINT32: case plugin_framework_1.FieldDescriptorProto_Type.SINT64: case plugin_framework_1.FieldDescriptorProto_Type.UINT32: case plugin_framework_1.FieldDescriptorProto_Type.UINT64: return "integer"; default: throw new Error(`${type} is not a scalar value`); } } /** * Generates the description * @param ctx * @param descriptor */ function genDescription(ctx, descriptor) { const registry = ctx.registry; const source = registry.sourceCodeComments(descriptor); const description = source.leading || source.trailing || ""; return description.trim(); } /** * Format protobuf name * @param ctx * @param name */ function localMessageName(ctx, name) { const registry = ctx.registry; const symbols = ctx.symbols; const entry = symbols.find(registry.resolveTypeName(name)); if (!entry) { return ""; } return local_type_name_1.createLocalTypeName(entry.descriptor, registry); } function parseUriParams(uri) { return getMatches(uri, /{([a-zA-Z_0-9]+)}/g, 1); } function getMatches(str, regex, index = 1) { const matches = []; let match; while (match = regex.exec(str)) { matches.push(match[index]); } return matches; } function capitalizeFirstLetter(str) { return str.charAt(0).toUpperCase() + str.slice(1); }