@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
JavaScript
;
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