UNPKG

rest-chronicle

Version:
293 lines (284 loc) 10.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _util = require("util"); var _fsExtra = _interopRequireDefault(require("fs-extra")); var _dotProp = _interopRequireDefault(require("dot-prop")); var _myrmidon = require("myrmidon"); var _constants = require("../constants"); var _utils = require("./utils"); var _Base = _interopRequireDefault(require("./Base")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /* eslint-disable no-param-reassign */ function isJSON(value) { if (typeof value !== 'string') return false; if (!value.trim().startsWith('{') && !value.trim().startsWith('[')) return false; try { JSON.parse(value); return true; } catch { return false; } } function actionsSorter(a, b) { if (a.response.status.code !== b.response.status.code) { return a.response.status.code - b.response.status.code; } return a.meta.createdAt - b.meta.createdAt; } class SwaggerReporter extends _Base.default { constructor(file, { hash } = {}) { super(file); _defineProperty(this, "mergeArray", true); if (hash) this.getHash = hash; } _renderHeaders(headers) { if (!headers) return []; return Object.entries(headers).filter(([name]) => name.toLowerCase() !== 'content-type').map(([name, value]) => ({ name, in: 'header', schema: { type: (0, _utils.detectType)(value) }, example: value })); } _renderBody(body) { var _body$constructor, _body$constructor2; if (isJSON(body)) { return this._renderBody(JSON.parse(body)); } if (body === null) { return { type: 'null', nullable: true, example: null }; } if (Buffer.isBuffer(body)) { return { type: 'string', format: 'binary', example: (0, _util.inspect)(body) }; } if (Array.isArray(body)) { return { type: 'array', items: body.length > 0 ? this._renderBody(body[0]) : {}, example: body }; } if (body && (((_body$constructor = body.constructor) === null || _body$constructor === void 0 ? void 0 : _body$constructor.name) === 'File' || ((_body$constructor2 = body.constructor) === null || _body$constructor2 === void 0 ? void 0 : _body$constructor2.name) === 'Blob')) { return { type: 'string', format: 'binary' }; } const type = (0, _utils.detectType)(body); if (type === 'object') { const properties = {}; for (const [key, value] of Object.entries(body)) { properties[key] = this._renderBody(value); } return { type: 'object', properties, example: body }; } const schema = { type, example: body }; if (type === 'number' && body >= 0 && body <= 1) { schema.minimum = 0; schema.maximum = 1; } return schema; } /** * Helper to merge two schemas (mostly for Object properties) * Keeps the first type found if they differ. */ _mergeSchemas(target, source) { if (!target) return source; if (!source) return target; // If types mismatch, we can't cleanly merge, stick with target but maybe relax it? // For now, we assume consistent types or just return target. if (target.type !== source.type) return target; if (target.type === 'object' && source.properties) { target.properties = target.properties || {}; for (const [key, val] of Object.entries(source.properties)) { if (!target.properties[key]) { // New property found in source, add it target.properties[key] = val; } else { // Property exists, recursively merge target.properties[key] = this._mergeSchemas(target.properties[key], val); } } } return target; } // eslint-disable-next-line sonarjs/cognitive-complexity _renderAction(actionInput) { const actions = (0, _myrmidon.toArray)(actionInput).sort(actionsSorter); if (!actions || actions.length === 0) return {}; const isSingle = actions.length === 1; // const baseAction = getBaseAction(actions); // const { group, title } = baseAction.context; // 1. Merge Parameters const paramsMap = new Map(); actions.forEach(action => { const headers = this._renderHeaders(action.request.headers); headers.forEach(param => { const key = `${param.name}:${param.in}`; if (!paramsMap.has(key)) paramsMap.set(key, param); }); }); // 2. Process Request Body let requestBody; const requestContentMap = {}; actions.forEach(action => { if (!action.request.body) return; const contentType = action.request.info.type || 'application/json'; if (!requestContentMap[contentType]) { requestContentMap[contentType] = { schemas: [], examples: {} }; } const generatedSchema = this._renderBody(action.request.body); requestContentMap[contentType].schemas.push(generatedSchema); let exampleKey = action.context.title; let counter = 1; while (requestContentMap[contentType].examples[exampleKey]) { exampleKey = `${action.context.title} (${counter++})`; } requestContentMap[contentType].examples[exampleKey] = { summary: action.context.title, value: generatedSchema.example }; }); if (Object.keys(requestContentMap).length > 0) { requestBody = { content: {} }; for (const [contentType, data] of Object.entries(requestContentMap)) { const finalSchema = data.schemas.reduce((acc, s) => this._mergeSchemas(acc, s), data.schemas[0]); requestBody.content[contentType] = { schema: finalSchema }; if (isSingle) { // Keep old structure: example inside schema finalSchema.example = Object.values(data.examples)[0].value; } else { // Use plural examples map for multiple actions delete finalSchema.example; requestBody.content[contentType].examples = data.examples; } } } // 3. Process Responses const responses = {}; const responseGroups = {}; actions.forEach(action => { const code = action.response.status.code; if (!responseGroups[code]) responseGroups[code] = []; responseGroups[code].push(action); }); for (const [code, groupActions] of Object.entries(responseGroups)) { const isSingleResponse = groupActions.length === 1; const responseContentMap = {}; groupActions.forEach(action => { const contentType = action.response.info.type || 'application/json'; if (!responseContentMap[contentType]) { responseContentMap[contentType] = { schemas: [], examples: {} }; } const generatedSchema = this._renderBody(action.response.body); responseContentMap[contentType].schemas.push(generatedSchema); let exampleKey = action.context.title; let counter = 1; while (responseContentMap[contentType].examples[exampleKey]) { exampleKey = `${action.context.title} (${counter++})`; } responseContentMap[contentType].examples[exampleKey] = { summary: action.context.title, value: generatedSchema.example }; }); responses[code] = { description: groupActions[0].context.title, content: {} }; for (const [contentType, data] of Object.entries(responseContentMap)) { const finalSchema = data.schemas.reduce((acc, s) => this._mergeSchemas(acc, s), data.schemas[0]); responses[code].content[contentType] = { schema: finalSchema }; if (isSingleResponse) { // Keep old structure finalSchema.example = Object.values(data.examples)[0].value; } else { // New structure for multiple examples delete finalSchema.example; responses[code].content[contentType].examples = data.examples; } } } return { // eslint-disable-next-line unicorn/no-array-callback-reference tags: actions.map(a => a.context.group).filter(_myrmidon.uniqueIdenticFilter), description: actions.map(a => a.context.title).join('. '), parameters: [...paramsMap.values()], requestBody, responses }; } _generate(groups, map, actions) { const paths = {}; const origins = [...new Set(actions === null || actions === void 0 ? void 0 : actions.map(a => a.request.origin))]; for (const [path, methods] of Object.entries(groups)) { for (const [method, actionIds] of Object.entries(methods)) { const methodName = method.toLowerCase(); // Get ALL actions for this Path + Method combination const groupActions = actionIds.map(id => map.get(id)); // Generate a single definition merging all actions _dotProp.default.set(paths, `${path}.${methodName}`, this._renderAction(groupActions)); } } const content = { openapi: '3.0.0', info: { version: '1.0.0', title: 'Swagger Report' }, servers: origins.map(url => ({ url })), paths }; return JSON.stringify(content, null, _constants.DEFAULT_JSON_OFFSET); } async write(actions) { const { groups, map } = this._build(actions, { groupBy: ['request.path', 'request.method'] }); const content = this._generate(groups, map, actions); await _fsExtra.default.writeFile(this.file, content); } } exports.default = SwaggerReporter;