@ply-ct/ply
Version:
REST API Automated Testing
289 lines • 12.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Plyex = exports.defaultOptions = void 0;
const http_status_codes_1 = require("http-status-codes");
const nestjs_1 = require("./nestjs");
const apidocs_1 = require("./apidocs");
const ts_1 = require("../ts");
const code_1 = require("./code");
exports.defaultOptions = {
tsConfig: 'tsconfig.json',
sourcePatterns: ['src/**/*.ts'],
addMissingOperations: true,
overwrite: false,
includeUntagged: true,
samplesFromActual: false
};
class Plyex {
constructor(plugin, logger, options) {
this.plugin = plugin;
this.logger = logger;
this.jsDocReaders = new Map();
this.options = { ...exports.defaultOptions, ...(options || {}) };
this.ts = new ts_1.Ts(this.options.tsConfig, this.options.sourcePatterns);
}
getPluginEndpointMethods() {
if (this.plugin === 'nestjs') {
return new nestjs_1.NestJsPlugin(this.ts).getEndpointMethods();
}
else {
throw new Error(`Unsupported plyex plugin: ${this.plugin}`);
}
}
async augment(openApi, endpointMethods) {
if (!endpointMethods) {
endpointMethods = this.getPluginEndpointMethods();
}
if (this.options.addMissingOperations) {
for (const endpointMethod of endpointMethods) {
const openApiPath = (openApi.paths || {})[endpointMethod.path];
if (!openApiPath) {
if (!openApi.paths)
openApi.paths = {};
openApi.paths[endpointMethod.path] = {
[endpointMethod.method]: this.operation(endpointMethod)
};
}
else if (!openApiPath[endpointMethod.method]) {
openApiPath[endpointMethod.method] = this.operation(endpointMethod);
}
}
}
const augmented = await this.doAugment(openApi, endpointMethods);
return augmented;
}
async doAugment(openApi, endpointMethods) {
const deepCopy = JSON.parse(JSON.stringify(openApi));
const openApiPaths = deepCopy.paths || {};
for (const path of Object.keys(openApiPaths)) {
const openApiPath = openApiPaths[path];
for (const method of Object.keys(openApiPath)) {
const openApiMethod = method;
const operation = openApiPath[openApiMethod];
const endpointMethod = endpointMethods.find((epm) => {
return epm.path === path && epm.method === openApiMethod;
});
if (endpointMethod) {
let jsDocReader = this.jsDocReaders.get(endpointMethod.file);
if (!jsDocReader) {
const sourceFile = this.ts.program.getSourceFile(endpointMethod.file);
if (!sourceFile) {
throw new Error(`Source file (${endpointMethod.file}) not found for ${this.epm(endpointMethod)}`);
}
jsDocReader = new apidocs_1.JsDocReader(this.ts, sourceFile);
this.jsDocReaders.set(endpointMethod.file, jsDocReader);
}
const plyEndpointMeta = await jsDocReader.getPlyEndpointMeta(endpointMethod, 'ply', this.options.includeUntagged, this.options.samplesFromActual);
if (plyEndpointMeta) {
if (!operation.summary || this.options.overwrite) {
if (plyEndpointMeta.summaries.length > 1 &&
endpointMethod.lastSegmentOptional) {
operation.summary = plyEndpointMeta.summaries[1];
}
else {
operation.summary = plyEndpointMeta.summaries[0];
}
}
if (plyEndpointMeta.operationId &&
(!operation.operationId || this.options.overwrite)) {
operation.operationId = plyEndpointMeta.operationId;
}
if (plyEndpointMeta.description &&
(!operation.description || this.options.overwrite)) {
operation.description = plyEndpointMeta.description;
}
this.examples(endpointMethod, operation, plyEndpointMeta);
}
}
}
}
return deepCopy;
}
operation(endpointMethod) {
this.logger.info(`Adding OpenAPI operation: ${endpointMethod.path}.${endpointMethod.method}`);
return { summary: '' };
}
examples(endpointMethod, operation, plyEndpointMeta) {
var _a, _b;
if ((_a = plyEndpointMeta.examples) === null || _a === void 0 ? void 0 : _a.request) {
if (!operation.requestBody) {
operation.requestBody = { description: '', content: {}, required: true };
}
const reqContent = operation.requestBody.content;
let jsonPayload = reqContent['application/json'];
if (!jsonPayload) {
jsonPayload = reqContent['application/json'] = { schema: {} };
}
if (jsonPayload) {
if (!operation.requestBody.description) {
const schemaType = this.schemaType(jsonPayload);
if (schemaType)
operation.requestBody.description = this.typeName(schemaType);
}
if (!jsonPayload.example || this.options.overwrite) {
jsonPayload.example = this.example(endpointMethod, plyEndpointMeta.examples.request);
}
}
}
if ((_b = plyEndpointMeta.examples) === null || _b === void 0 ? void 0 : _b.responses) {
if (!operation.responses)
operation.responses = {};
for (const code of Object.keys(plyEndpointMeta.examples.responses)) {
const respExamples = plyEndpointMeta.examples.responses[code];
if (respExamples.length > 0) {
const example = this.example(endpointMethod, endpointMethod.lastSegmentOptional && respExamples.length > 1
? respExamples[1]
: respExamples[0], true);
if (!operation.responses[code])
operation.responses[code] = { description: '' };
let respContent = operation.responses[code].content;
if (!respContent && typeof example === 'object') {
respContent = operation.responses[code].content = {
'application/json': { schema: {} }
};
}
if (respContent) {
let jsonPayload = respContent['application/json'];
if (!jsonPayload) {
jsonPayload = respContent['application/json'] = { schema: {} };
}
if (jsonPayload) {
if (!operation.responses[code].description) {
const intCode = parseInt(code);
if (!isNaN(intCode)) {
operation.responses[code].description =
(0, http_status_codes_1.getReasonPhrase)(intCode);
}
if (!operation.responses[code].description) {
operation.responses[code].description = '' + code;
}
}
if (!jsonPayload.example || this.options.overwrite) {
jsonPayload.example = example;
}
if (jsonPayload.example) {
if (code === '200' || code === '201') {
const schemaType = this.schemaType(jsonPayload) || 'Unknown';
this.codeSamples(operation, endpointMethod, schemaType, jsonPayload.example);
}
}
}
}
}
}
}
}
schemaType(jsonPayload) {
var _a;
const schema = jsonPayload.schema;
let ref = schema.$ref;
if (schema.type === 'array' && ((_a = schema.items) === null || _a === void 0 ? void 0 : _a.$ref)) {
ref = schema.items.$ref;
}
if (ref) {
let type = ref.substring(ref.lastIndexOf('/') + 1);
if (schema.type === 'array')
type = `[${type}]`;
return type;
}
return '';
}
codeSamples(operation, endpointMethod, schemaType, example) {
if (operation['x-codeSamples']) {
return; // already added
}
const pathSegments = endpointMethod.path.substring(1).split('/');
const pathChunks = pathSegments.reduce((chunks, seg) => {
const chunk = chunks[chunks.length - 1];
if (seg.startsWith('{') && seg.endsWith('}')) {
if (chunk.param)
chunks.push({ path: '/', param: false });
else
chunk.path += '/';
chunks.push({ path: seg.substring(1, seg.length - 1), param: true });
}
else if (chunk.param) {
chunks.push({ path: `/${seg}`, param: false });
}
else {
chunk.path += `/${seg}`;
}
return chunks;
}, [{ path: '', param: false }]);
const isArray = schemaType.startsWith('[') && schemaType.endsWith(']');
const type = isArray ? schemaType.substring(1, schemaType.length - 1) : schemaType;
const templateContext = {
chunks: pathChunks,
type,
array: isArray,
name: this.typeName(schemaType)
};
const last = pathSegments[pathSegments.length - 1];
if (last.startsWith('{') && last.endsWith('}') && example) {
const name = last.substring(1, last.length - 1);
if (example[name]) {
templateContext.item = { name, value: example[name] };
}
}
const samples = new code_1.CodeSamples(endpointMethod.method).getSamples(templateContext);
for (const lang of Object.keys(samples)) {
if (!operation['x-codeSamples'])
operation['x-codeSamples'] = [];
operation['x-codeSamples'].push({ lang, source: samples[lang] });
}
}
typeName(schemaType) {
if (!schemaType)
return '';
const isArray = schemaType.startsWith('[') && schemaType.endsWith(']');
const type = isArray ? schemaType.substring(1, schemaType.length - 1) : schemaType;
return isArray ? this.pluralize(this.uncapitalize(type)) : this.uncapitalize(type);
}
/**
* Crude pluralization -- override with jsdoc
*/
pluralize(sing) {
if (sing.endsWith('s') ||
sing.endsWith('sh') ||
sing.endsWith('ch') ||
sing.endsWith('x') ||
sing.endsWith('z')) {
return sing + 'es';
}
else if (sing.endsWith('y')) {
return sing.substring(0, sing.length - 1) + 'ies';
}
else {
return sing + 's';
}
}
capitalize(lower) {
return `${lower.charAt(0).toUpperCase()}${lower.substring(1)}`;
}
uncapitalize(cap) {
return `${cap.charAt(0).toLowerCase()}${cap.substring(1)}`;
}
epm(endpointMethod) {
return `${endpointMethod.path}.${endpointMethod.name} ${endpointMethod.method}`;
}
example(endpointMethod, example, isResponse = false) {
if (example.startsWith('{') || example.startsWith('[')) {
try {
return JSON.parse(this.cleanupJson(example));
}
catch (err) {
this.logger.error(`Error parsing JSON example ${isResponse ? 'response' : 'request'}: ${this.epm(endpointMethod)}`, err);
}
}
return example;
}
cleanupJson(json) {
const lines = [];
for (const line of json.split(/\r?\n/)) {
lines.push(line.replace(/(?<!")\$\{.+?}/g, (_num) => '123'));
}
return lines.join('\n');
}
}
exports.Plyex = Plyex;
//# sourceMappingURL=plyex.js.map