swaggler
Version:
Swaggler helps you smuggle your existing API requests into structured, well-documented specs with ease
397 lines (396 loc) • 16.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenAPIGenerator = void 0;
const SwagglerException_1 = require("../errors/SwagglerException");
const yaml = __importStar(require("js-yaml"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const utils_1 = require("../utils");
class OpenAPIGenerator {
static getOpenAPIType(value) {
if (Array.isArray(value))
return "array";
if (value === null)
return "object";
if (typeof value === "object")
return "object";
if (typeof value === "number")
return Number.isInteger(value) ? "integer" : "number";
if (typeof value === "boolean")
return "boolean";
return "string";
}
static generateSchema(data, prefix, depth = 0, schemas = {}) {
// Prevent infinite recursion for circular references
if (depth > 100) {
return {
type: "object",
properties: {},
};
}
if (Array.isArray(data)) {
const itemSchemaName = `${prefix}Item`;
if (data.length > 0) {
const itemSchema = this.generateSchema(data[0], itemSchemaName, depth + 1, schemas);
schemas[itemSchemaName] = itemSchema;
return {
type: "array",
items: {
$ref: `#/components/schemas/${itemSchemaName}`,
},
};
}
return {
type: "array",
items: {
type: "object",
properties: {},
required: [],
},
};
}
if (typeof data === "object" && data !== null) {
const schema = {
type: "object",
properties: {},
};
Object.entries(data).forEach(([key, value]) => {
if (value === null) {
schema.properties[key] = {
type: "string",
nullable: true,
};
}
else if (Array.isArray(value)) {
const itemSchemaName = `${prefix}${key.charAt(0).toUpperCase() + key.slice(1)}Item`;
if (value.length > 0) {
const itemSchema = this.generateSchema(value[0], itemSchemaName, depth + 1, schemas);
schemas[itemSchemaName] = itemSchema;
schema.properties[key] = {
type: "array",
items: {
$ref: `#/components/schemas/${itemSchemaName}`,
},
};
}
else {
schema.properties[key] = {
type: "array",
items: {
type: "object",
properties: {},
},
};
}
}
else if (typeof value === "object") {
const nestedSchemaName = `${prefix}${key.charAt(0).toUpperCase() + key.slice(1)}`;
const nestedSchema = this.generateSchema(value, nestedSchemaName, depth + 1, schemas);
schemas[nestedSchemaName] = nestedSchema;
schema.properties[key] = {
$ref: `#/components/schemas/${nestedSchemaName}`,
};
}
else {
schema.properties[key] = {
type: this.getOpenAPIType(value),
example: value,
};
}
});
return schema;
}
return {
type: this.getOpenAPIType(data),
example: data,
properties: {},
};
}
static generateOperationId(method, path) {
const cleanPath = path.replace(/[{}]/g, "").replace(/\//g, "_");
return `${method.toLowerCase()}_${cleanPath}`.replace(/^_+|_+$/g, "");
}
static generateParameters(curl, pathName, urlTemplate) {
const parameters = [];
// Add path parameters
const pathParams = urlTemplate
? urlTemplate.match(/{([^}]+)}/g)?.map((param) => param.slice(1, -1)) ||
[]
: pathName.match(/{([^}]+)}/g)?.map((param) => param.slice(1, -1)) || [];
pathParams.forEach((param) => {
parameters.push({
name: param,
in: "path",
required: true,
schema: {
type: "string",
},
});
});
// Add query parameters
Object.entries(curl.queryParams).forEach(([key, value]) => {
// Skip if this key is already a path parameter
if (pathParams.includes(key)) {
return;
}
let example = value;
let schemaType = typeof value;
// Try to parse JSON strings
if (typeof value === "string") {
try {
const parsed = JSON.parse(value);
example = parsed;
schemaType = Array.isArray(parsed)
? "array"
: typeof parsed === "object"
? "object"
: schemaType;
}
catch {
// If parsing fails, keep the original string
}
}
const parameter = {
name: key,
in: "query",
required: false,
schema: {
type: schemaType,
},
example: example,
};
// Add items schema for array type
if (schemaType === "array") {
parameter.schema.items = {
type: "string",
};
}
// Add properties schema for object type
if (schemaType === "object") {
parameter.schema.properties = {};
}
parameters.push(parameter);
});
// Add header parameters
Object.entries(curl.headers).forEach(([key, value]) => {
parameters.push({
name: key,
in: "header",
required: false,
schema: {
type: "string",
},
example: value,
});
});
return parameters;
}
static parseFormData(formData) {
const result = {};
const params = new URLSearchParams(formData);
params.forEach((value, key) => {
result[key] = value;
});
return result;
}
static getContentType(contentType) {
if (!contentType)
return "application/json";
// Ensure we're using the standard content type for form data
if (contentType === "application/x-www-form-urlencoded") {
return "application/x-www-form-urlencoded";
}
return contentType;
}
static generate(curl, response, options = {}) {
const pathName = options.urlTemplate ||
curl.url.split("?")[0].replace(/^https?:\/\/[^\/]+/, "");
const operationId = options.operationName || this.generateOperationId(curl.method, pathName);
const schemaPrefix = (0, utils_1.capitalizeFirstLetter)(operationId);
// Create a schemas object to store all generated schemas
const schemas = {};
// Generate response schema with depth tracking
const responseSchema = this.generateSchema(response, schemaPrefix, 0, schemas);
// Generate request schema if there's data
let requestSchema = null;
if (curl.data) {
if (curl.contentType === "application/x-www-form-urlencoded") {
const formData = this.parseFormData(curl.data);
requestSchema = this.generateSchema(formData, `${schemaPrefix}Request`, 0, schemas);
}
else {
requestSchema = this.generateSchema(curl.data, `${schemaPrefix}Request`, 0, schemas);
}
}
// Extract path parameters to filter them from query parameters
const pathParams = pathName.match(/{([^}]+)}/g)?.map((param) => param.slice(1, -1)) || [];
// Filter out path parameters from query parameters
const filteredQueryParams = { ...curl.queryParams };
pathParams.forEach((param) => {
delete filteredQueryParams[param];
});
// Generate query parameter schema
const querySchema = this.generateSchema(filteredQueryParams, `${schemaPrefix}Query`, 0, schemas);
const contentType = this.getContentType(curl.contentType);
return {
openapi: "3.0.0",
info: {
title: "API Documentation",
version: "1.0.0",
},
paths: {
[pathName]: {
[curl.method.toLowerCase()]: {
operationId,
summary: options?.summary ||
(options?.operationName
? (0, utils_1.capitalizeFirstLetter)(options.operationName)
: `${curl.method} ${pathName}`),
tags: options.tags ? options.tags.map(utils_1.capitalizeFirstLetter) : [],
parameters: this.generateParameters(curl, pathName, options.urlTemplate),
requestBody: requestSchema
? {
required: true,
content: {
[contentType]: {
schema: {
$ref: `#/components/schemas/${schemaPrefix}Request`,
},
},
},
}
: undefined,
responses: {
"200": {
description: "Successful response",
content: {
"application/json": {
schema: {
$ref: `#/components/schemas/${schemaPrefix}Response`,
},
},
},
},
},
},
},
},
components: {
schemas: {
[`${schemaPrefix}Response`]: responseSchema,
...(requestSchema
? { [`${schemaPrefix}Request`]: requestSchema }
: {}),
...(Object.keys(filteredQueryParams).length > 0
? { [`${schemaPrefix}Query`]: querySchema }
: {}),
...schemas, // Include all nested schemas
},
},
};
}
static saveToFile(openapi, options) {
const yamlContent = yaml.dump(openapi, {
indent: 2,
lineWidth: -1,
noRefs: true,
});
// Use the output path if provided, otherwise use append path or default swagger.yaml
const outputPath = options.outputPath || options.appendPath || "swagger.yaml";
// Create directory if it doesn't exist
const dir = path.dirname(outputPath);
if (dir && !fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(outputPath, yamlContent);
}
static findDuplicateOperationId(openapi, operationId) {
for (const path in openapi.paths) {
for (const method in openapi.paths[path]) {
if (openapi.paths[path][method].operationId === operationId) {
return { exists: true, path, method };
}
}
}
return { exists: false };
}
static mergeWithExisting(openapi, appendPath) {
try {
const existingContent = fs.readFileSync(appendPath, "utf8");
const existingOpenAPI = yaml.load(existingContent);
// Check for duplicate operation IDs and throw error if found
for (const path in openapi.paths) {
for (const method in openapi.paths[path]) {
const operation = openapi.paths[path][method];
if (operation.operationId) {
const duplicate = this.findDuplicateOperationId(existingOpenAPI, operation.operationId);
if (duplicate.exists) {
throw new SwagglerException_1.SwagglerException("Operation ID conflict detected", "DUPLICATE_OPERATION_ID", {
operationId: operation.operationId,
existingPath: duplicate.path,
existingMethod: duplicate.method,
newPath: path,
newMethod: method,
});
}
}
}
}
// Merge paths
openapi.paths = {
...existingOpenAPI.paths,
...openapi.paths,
};
// Merge schemas
openapi.components.schemas = {
...existingOpenAPI.components?.schemas,
...openapi.components.schemas,
};
console.log(`Appended to existing swagger file: ${appendPath}`);
}
catch (error) {
if (error instanceof SwagglerException_1.SwagglerException) {
throw error;
}
if (error instanceof Error) {
throw new SwagglerException_1.SwagglerException("Failed to merge with existing OpenAPI specification", "MERGE_ERROR", { cause: error.message });
}
console.warn(`Warning: Could not read existing swagger file: ${error}`);
}
return openapi;
}
}
exports.OpenAPIGenerator = OpenAPIGenerator;