ng-openapi-gen
Version:
An OpenAPI 3.0 and 3.1 codegen for Angular 16+
293 lines • 12.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Operation = void 0;
const lodash_1 = require("lodash");
const content_1 = require("./content");
const gen_utils_1 = require("./gen-utils");
const logger_1 = require("./logger");
const openapi_typings_1 = require("./openapi-typings");
const operation_variant_1 = require("./operation-variant");
const parameter_1 = require("./parameter");
const request_body_1 = require("./request-body");
const response_1 = require("./response");
const security_1 = require("./security");
/**
* An operation descriptor
*/
class Operation {
constructor(openApi, path, pathSpec, method, id, spec, options) {
this.openApi = openApi;
this.path = path;
this.pathSpec = pathSpec;
this.method = method;
this.id = id;
this.spec = spec;
this.options = options;
this.parameters = [];
this.parametersRequired = false;
this.security = [];
this.allResponses = [];
this.variants = [];
this.logger = new logger_1.Logger(options.silent);
this.path = this.path.replace(/\'/g, '\\\'');
this.tags = spec.tags || [];
this.pathVar = `${(0, lodash_1.upperFirst)(id)}Path`;
this.methodName = spec['x-operation-name'] || this.id;
// Add both the common and specific parameters
const allParams = [
...this.collectParameters(false, pathSpec.parameters),
...this.collectParameters(true, spec.parameters),
];
// Maybe there were duplicated parameters? In this case, let specific parameters replace the general ones
this.parameters = [];
allParams.forEach(param => {
let skip = false;
if (!param.specific) {
skip = !!allParams.find(p => p !== param && p.name === param.name && p.specific);
}
if (!skip) {
this.parameters.push(param);
}
});
if (this.parameters.find(p => p.required)) {
this.parametersRequired = true;
}
this.hasParameters = this.parameters.length > 0;
this.security = spec.security ? this.collectSecurity(spec.security) : this.collectSecurity(openApi.security);
let body = spec.requestBody;
if (body) {
if ((0, openapi_typings_1.isReferenceObject)(body)) {
body = (0, gen_utils_1.resolveRef)(this.openApi, body.$ref);
}
body = body;
this.requestBody = new request_body_1.RequestBody(body, this.collectContent(body.content), this.options);
if (body.required) {
this.parametersRequired = true;
}
}
const responses = this.collectResponses();
this.successResponse = responses.success;
this.allResponses = responses.all;
this.pathExpression = this.toPathExpression();
this.deprecated = !!spec.deprecated;
// Now calculate the variants: request body content x success response content
this.calculateVariants();
}
skipImport() {
// All models are imported
return false;
}
collectParameters(specific, params) {
const result = [];
if (params) {
for (let param of params) {
if ((0, openapi_typings_1.isReferenceObject)(param)) {
param = (0, gen_utils_1.resolveRef)(this.openApi, param.$ref);
}
param = param;
if (param.in === 'cookie') {
this.logger.warn(`Ignoring cookie parameter ${this.id}.${param.name} as cookie parameters cannot be sent in XmlHttpRequests.`);
}
else if (!this.paramIsExcluded(param)) {
const parameter = new parameter_1.Parameter(param, this.options, this.openApi);
parameter.specific = specific;
result.push(parameter);
}
}
}
return result;
}
collectSecurity(params) {
if (!params) {
return [];
}
return params.map((param) => {
return Object.keys(param).map(key => {
const scope = param[key];
const security = (0, gen_utils_1.resolveRef)(this.openApi, `#/components/securitySchemes/${key}`);
return new security_1.Security(key, security, scope);
});
});
}
paramIsExcluded(param) {
const excludedParameters = this.options.excludeParameters || [];
return excludedParameters.includes(param.name);
}
collectContent(desc) {
const result = [];
if (desc) {
for (const type of Object.keys(desc)) {
result.push(new content_1.Content(type, desc[type], this.options, this.openApi));
}
}
return result;
}
collectResponses() {
let successResponse = undefined;
const allResponses = [];
const responses = this.spec.responses || {};
const responseByType = new Map();
for (const statusCode of Object.keys(responses)) {
const response = this.getResponse(responses[statusCode], statusCode);
allResponses.push(response);
const statusInt = Number.parseInt(statusCode.trim(), 10);
if (statusInt >= 200 && statusInt < 300 && !responseByType.has('successResponse')) {
responseByType.set('successResponse', response);
}
else if (statusCode === 'default') {
responseByType.set('defaultResponse', response);
}
}
successResponse = responseByType.get('successResponse') ?? responseByType.get('defaultResponse');
return { success: successResponse, all: allResponses };
}
getResponse(responseObject, statusCode) {
let responseDesc = undefined;
if ((0, openapi_typings_1.isReferenceObject)(responseObject)) {
responseDesc = (0, gen_utils_1.resolveRef)(this.openApi, responseObject.$ref);
}
else {
responseDesc = responseObject;
}
const response = new response_1.Response(statusCode, responseDesc.description || '', this.collectContent(responseDesc.content), this.options);
return response;
}
/**
* Returns a path expression to be evaluated, for example:
* "/a/{var1}/b/{var2}/" returns "/a/${params.var1}/b/${params.var2}"
*/
toPathExpression() {
return (this.path || '').replace(/\{([^}]+)}/g, (_, pName) => {
const param = this.parameters.find(p => p.name === pName);
const paramName = param ? param.var : pName;
return '${params.' + paramName + '}';
});
}
contentsByMethodPart(hasContent) {
const map = new Map();
if (hasContent) {
const content = hasContent.content;
if (content && content.length > 0) {
for (const type of content) {
if (type && type.mediaType) {
const part = this.variantMethodPart(type);
if (map.has(part)) {
this.logger.warn(`Overwriting variant method part '${part}' for media type '${map.get(part)?.mediaType}' by media type '${type.mediaType}'.`);
}
map.set(part, type);
}
}
}
}
if (map.size === 0) {
map.set('', null);
}
else if (map.size === 1) {
const content = [...map.values()][0];
map.clear();
map.set('', content);
}
return map;
}
calculateVariants() {
// It is possible to have multiple content types which end up in the same method.
// For example: application/json, application/foo-bar+json, text/json ...
const requestVariants = this.contentsByMethodPart(this.requestBody);
const responseVariants = this.contentsByMethodPart(this.successResponse);
// Calculate total number of variants - if there's only one combination, no suffixes needed
const totalVariants = Math.max(1, requestVariants.size) * Math.max(1, responseVariants.size);
// Check if we have a potential ambiguity: both request and response have single content types
// that would result in the same method suffix, causing duplicate method names
const hasAmbiguity = totalVariants > 1 &&
requestVariants.size === 1 && responseVariants.size === 1 &&
[...requestVariants.keys()][0] === '' && [...responseVariants.keys()][0] === '' &&
[...requestVariants.values()][0] !== null && [...responseVariants.values()][0] !== null;
if (hasAmbiguity) {
// Additional check: are the media types actually the same?
const requestMediaType = [...requestVariants.values()][0]?.mediaType;
const responseMediaType = [...responseVariants.values()][0]?.mediaType;
if (requestMediaType && responseMediaType) {
// Calculate what the method parts would be for each
const requestPart = this.variantMethodPart([...requestVariants.values()][0]);
const responsePart = this.variantMethodPart([...responseVariants.values()][0]);
// Only recalculate with preserved suffixes if the method parts would be the same
if (requestPart === responsePart) {
requestVariants.clear();
responseVariants.clear();
if (this.requestBody?.content && this.requestBody.content.length > 0) {
for (const content of this.requestBody.content) {
if (content && content.mediaType) {
const part = this.variantMethodPart(content);
requestVariants.set(part, content);
}
}
}
else {
requestVariants.set('', null);
}
if (this.successResponse?.content && this.successResponse.content.length > 0) {
for (const content of this.successResponse.content) {
if (content && content.mediaType) {
const part = this.variantMethodPart(content);
responseVariants.set(part, content);
}
}
}
else {
responseVariants.set('', null);
}
}
}
}
requestVariants.forEach((requestContent, requestPart) => {
responseVariants.forEach((responseContent, responsePart) => {
const methodName = this.methodName + requestPart + responsePart;
this.variants.push(new operation_variant_1.OperationVariant(this, methodName, requestContent, responseContent, this.options));
});
});
}
/**
* Returns how the given content is represented on the method name
*/
variantMethodPart(content) {
if (content) {
const keep = this.keepFullResponseMediaType(content.mediaType);
let type = content.mediaType;
type = content.mediaType.replace(/\/\*/, '');
if (type === '*' || type === 'application/octet-stream') {
return '$Any';
}
if (keep !== 'full') {
type = (0, lodash_1.last)(type.split('/'));
if (keep !== 'tail') {
const plus = type.lastIndexOf('+');
if (plus >= 0) {
type = type.substring(plus + 1);
}
}
}
return this.options.skipJsonSuffix && type === 'json' ? '' : `$${(0, gen_utils_1.typeName)(type)}`;
}
else {
return '';
}
}
/**
* Returns hint, how the expected response type in the request method names should be abbreviated.
*/
keepFullResponseMediaType(mediaType) {
if (this.options.keepFullResponseMediaType === true) {
return 'full';
}
if (Array.isArray(this.options.keepFullResponseMediaType)) {
for (const check of this.options.keepFullResponseMediaType) {
if (check.mediaType === undefined || new RegExp(check.mediaType).test(mediaType)) {
return check.use ?? 'short';
}
}
}
return 'short';
}
}
exports.Operation = Operation;
//# sourceMappingURL=operation.js.map