UNPKG

@spec2ts/openapi-client

Version:

Utility to convert OpenAPI v3 specifications to Typescript HTTP client using TypeScript native compiler

240 lines (239 loc) 9.08 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isMethod = exports.parseOperation = void 0; const ts = require("typescript"); const core = require("@spec2ts/core"); const core_parser_1 = require("@spec2ts/jsonschema/lib/core-parser"); const core_parser_2 = require("@spec2ts/openapi/lib/core-parser"); const util_1 = require("./util"); const methods = ["GET", "PUT", "POST", "DELETE", "OPTIONS", "HEAD", "PATCH", "TRACE"]; const contentTypes = { "*/*": "json", "application/json": "json", "application/x-www-form-urlencoded": "form", "multipart/form-data": "multipart", }; function parseOperation(path, item, method, operation, context) { const result = { name: getOperationVar(method, path, operation.operationId, context), paramsVars: {}, args: [], query: [], header: [], response: core.keywordType.void }; parseArgs(result, item, operation, context); parseResponses(result, path, method, operation, context); return result; } exports.parseOperation = parseOperation; function parseArgs(result, item, operation, context) { parseParameters(result, item, operation, context); if (operation.requestBody) { parseRequestBody(result, operation.requestBody, context); } result.args.push(core.createParameter("options", { type: ts.factory.createTypeReferenceNode("RequestOptions", undefined), questionToken: true, })); } function parseResponses(result, path, method, operation, context) { const operationName = (0, core_parser_2.getOperationName)(method, path, operation.operationId, context); result.response = getTypeFromResponses(operationName, operation.responses, context); result.responseJSON = isJSONResponse(operation.responses, context); result.responseVoid = result.response === core.keywordType.void; } function parseParameters(result, item, operation, context) { const parameters = fixDeepObjects([ ...resolveReferences(item.parameters, context), ...resolveReferences(operation.parameters, context), ]); result.query = parameters.filter((p) => p.in === "query"); result.header = parameters.filter((p) => p.in === "header"); const argNames = result.paramsVars = createParametersNames(parameters); let objectBindingParams = parameters; if (context.options.inlineRequired) { const [required, optional] = splitParameters(parameters); objectBindingParams = optional; required.forEach(p => { result.args.push(core.createParameter(argNames[p.name], { type: (0, core_parser_1.getTypeFromSchema)(p.schema, context), })); }); } if (!objectBindingParams.length) { return; } result.args.push(core.createParameter(core.createObjectBinding(objectBindingParams.map(({ name }) => ({ name: argNames[name] }))), { initializer: objectBindingParams.some(p => p.required) ? undefined : ts.factory.createObjectLiteralExpression(), type: ts.factory.createTypeLiteralNode(objectBindingParams.map((p) => core.createPropertySignature({ name: argNames[p.name], questionToken: !p.required, type: (0, core_parser_1.getTypeFromSchema)(p.schema, context) }))), })); } function parseRequestBody(result, requestBody, context) { const body = (0, core_parser_1.resolveReference)(requestBody, context); const [schema, mode] = getSchemaFromContent(body.content); const type = (0, core_parser_1.getTypeFromSchema)(schema, context); const bodyVar = result.bodyVar = (0, util_1.camelCase)(type.name || getReferenceName(schema) || "body"); result.bodyMode = mode; result.args.push(core.createParameter(bodyVar, { type })); } function createParametersNames(parameters) { const argNames = {}; parameters.forEach(({ name }) => { // strip leading namespaces, eg. foo.name -> name const stripped = (0, util_1.camelCase)(name.replace(/.+\./, "")); // keep the prefix if the stripped-down name is already taken argNames[name] = stripped in argNames ? (0, util_1.camelCase)(name) : stripped; }); return argNames; } function splitParameters(parameters) { const required = []; const optional = []; parameters.forEach(p => { if (p.required) required.push(p); else optional.push(p); }); return [required, optional]; } function getTypeFromResponses(operationName, res, context) { const codes = Object.keys(res); const types = []; codes.forEach(code => { const isOK = isOKResponse(code, codes.length); const type = getTypeFromResponse(res[code], context); if (!type) console.log(res[code]); if (ts.isTypeReferenceNode(type) || core.isKeywordTypeNode(type)) { isOK && types.push(type); } else { const name = (0, core_parser_2.getResponseName)(operationName, code, context); context.aliases.push(core.createTypeOrInterfaceDeclaration({ modifiers: [core.modifier.export], name, type })); if (isOK) { types.push(ts.factory.createTypeReferenceNode(name, undefined)); } } }); if (types.length === 1) { return types[0]; } return ts.factory.createUnionTypeNode(types); } function isJSONResponse(responses, context) { var _a, _b; const codes = Object.keys(responses); const resCode = codes.find(code => isOKResponse(code, codes.length)); if (!resCode) { return false; } const response = (0, core_parser_1.resolveReference)(responses[resCode], context); return (!!((_a = response === null || response === void 0 ? void 0 : response.content) === null || _a === void 0 ? void 0 : _a["application/json"]) || !!((_b = response === null || response === void 0 ? void 0 : response.content) === null || _b === void 0 ? void 0 : _b["*/*"])); } function isOKResponse(code, codesCount) { return codesCount === 1 || parseInt(code, 10) < 400; } //#endregion //#region Private function isMethod(method) { return methods.includes(method); } exports.isMethod = isMethod; //#endregion //#region Private function getTypeFromResponse(res, context) { res = (0, core_parser_1.resolveReference)(res, context); if (!(res === null || res === void 0 ? void 0 : res.content)) { return core.keywordType.void; } return (0, core_parser_1.getTypeFromSchema)(getSchemaFromContent(res.content)[0], context); } function getSchemaFromContent(content) { var _a; const res = Object.entries(contentTypes).find(([t]) => t in content); let schema; let mode; if (res) { const [contentType, contentMode] = res; mode = contentMode; schema = (_a = content === null || content === void 0 ? void 0 : content[contentType]) === null || _a === void 0 ? void 0 : _a.schema; } return [schema || { type: "string" }, mode]; } function resolveReferences(array, context) { var _a; return (_a = array === null || array === void 0 ? void 0 : array.map(ref => (0, core_parser_1.resolveReference)(ref, context))) !== null && _a !== void 0 ? _a : []; } function getReferenceName(obj) { if ((0, core_parser_1.isReference)(obj)) { return (0, util_1.camelCase)(obj.$ref.split("/").slice(-1)[0]); } } /** * Despite its name, OpenApi's `deepObject` serialization does not support * deeply nested objects. As a workaround we detect parameters that contain * square brackets and merge them into a single object. */ function fixDeepObjects(params) { const res = []; const merged = {}; params.forEach((p) => { const m = /^(.+?)\[(.*?)\]/.exec(p.name); if (!m) { res.push(p); return; } const [, name, prop] = m; let obj = merged[name]; if (!obj) { obj = merged[name] = { name, in: p.in, style: "deepObject", schema: { type: "object", properties: {}, }, }; res.push(obj); } obj.schema.properties[prop] = p.schema; }); return res; } function getOperationVar(verb, path, operationId, context) { const id = getOperationVarId(operationId); if (id) { return id; } return getPathVar(`${verb} ${path}`, context); } function getPathVar(path, context) { path = path.replace(/\{(.+?)\}/, "by $1").replace(/\{(.+?)\}/g, "and $1"); let name = (0, util_1.camelCase)(path); const count = (context.names[name] = (context.names[name] || 0) + 1); if (count > 1) { name += count; } return name; } function getOperationVarId(id) { if (!id) return; if (id.match(/[^\w\s]/)) return; id = (0, util_1.camelCase)(id); if (core.isValidIdentifier(id)) return id; } //#endregion