@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
997 lines (857 loc) • 33.3 kB
JavaScript
/**
* OpenAPI/Swagger Parser Utility
* This script extracts and organizes endpoint information from OpenAPI spec files
*
* Usage:
* node openapi-parser.js <openapi-file.json> --summary
* node openapi-parser.js <openapi-file.json> --endpoints
* // ... other commands
* node openapi-parser.js <openapi-file.json> --find-property <prop-name>
* node openapi-parser.js <openapi-file.json> --find-property <prop-name> --in-schema <SchemaName>
*/
import fs from 'fs/promises';
class OpenAPIParser {
constructor(spec) {
this.spec = spec;
this.version = spec.openapi || spec.swagger || '2.0';
// Global cache for resolved schemas with better tracking
this._resolvedSchemas = null;
this._globalCache = new Map();
this._refCache = new Map();
// Global recursion tracking
this._globalVisited = new WeakSet();
this._refStack = new Set();
}
/**
* Get all endpoints grouped by tag or path
*/
getAllEndpoints() {
const endpoints = [];
const paths = this.spec.paths || {};
for (const [pathKey, pathItem] of Object.entries(paths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if (['get', 'post', 'put', 'delete', 'patch', 'options', 'head'].includes(method)) {
endpoints.push({
path: pathKey,
method: method.toUpperCase(),
operationId: operation.operationId,
summary: operation.summary,
description: operation.description,
tags: operation.tags || [],
deprecated: operation.deprecated || false
});
}
}
}
return endpoints;
}
/**
* Get detailed information about a specific endpoint
*/
getEndpointDetails(method, path) {
const pathItem = this.spec.paths?.[path];
if (!pathItem) return null;
const operation = pathItem[method.toLowerCase()];
if (!operation) return null;
return {
path,
method: method.toUpperCase(),
operationId: operation.operationId,
summary: operation.summary,
description: operation.description,
tags: operation.tags || [],
parameters: this.extractParameters(operation, pathItem),
requestBody: this.extractRequestBody(operation),
responses: this.extractResponses(operation),
security: operation.security || this.spec.security || [],
deprecated: operation.deprecated || false
};
}
/**
* Extract parameters (path, query, header, cookie)
*/
extractParameters(operation, pathItem) {
const parameters = [
...(pathItem.parameters || []),
...(operation.parameters || [])
];
return parameters.map(param => {
// Handle $ref
if (param.$ref) {
param = this.resolveRef(param.$ref);
}
return {
name: param.name,
in: param.in,
description: param.description,
required: param.required || false,
deprecated: param.deprecated || false,
schema: param.schema || { type: param.type },
example: param.example,
examples: param.examples,
style: param.style,
explode: param.explode
};
});
}
/**
* Extract request body information
*/
extractRequestBody(operation) {
if (!operation.requestBody) return null;
let requestBody = operation.requestBody;
// Handle $ref
if (requestBody.$ref) {
requestBody = this.resolveRef(requestBody.$ref);
}
const content = {};
for (const [mediaType, mediaTypeObj] of Object.entries(requestBody.content || {})) {
content[mediaType] = {
schema: this.resolveSchema(mediaTypeObj.schema),
examples: mediaTypeObj.examples,
example: mediaTypeObj.example
};
}
return {
description: requestBody.description,
required: requestBody.required || false,
content
};
}
/**
* Extract response information
*/
extractResponses(operation) {
const responses = {};
for (const [statusCode, response] of Object.entries(operation.responses || {})) {
let resolvedResponse = response;
// Handle $ref
if (response.$ref) {
resolvedResponse = this.resolveRef(response.$ref);
}
const content = {};
for (const [mediaType, mediaTypeObj] of Object.entries(resolvedResponse.content || {})) {
content[mediaType] = {
schema: this.resolveSchema(mediaTypeObj.schema),
examples: mediaTypeObj.examples,
example: mediaTypeObj.example
};
}
responses[statusCode] = {
description: resolvedResponse.description,
headers: resolvedResponse.headers,
content
};
}
return responses;
}
/**
* Resolve schema references with robust circular reference protection
*/
resolveSchema(schema, context = { depth: 0, visited: new Set(), path: '' }) {
// Hard limits
const MAX_DEPTH = 10;
const MAX_ITERATIONS = 1000;
if (!schema || context.depth > MAX_DEPTH) {
console.warn(`Schema resolution stopped: depth=${context.depth}, path=${context.path}`);
return { type: 'object', description: '[Depth limit reached]' };
}
// Create a stable identifier for this schema instance
const schemaKey = this._createStableSchemaKey(schema, context.path);
// Check if we've already processed this exact schema in this context
if (context.visited.has(schemaKey)) {
return { type: 'object', description: '[Circular reference detected]', $ref: schema.$ref };
}
// Check global cache
if (this._globalCache.has(schemaKey)) {
return this._globalCache.get(schemaKey);
}
// Add to visited before processing to prevent infinite recursion
context.visited.add(schemaKey);
try {
let resolvedSchema = schema;
// Handle $ref with enhanced protection
if (schema.$ref) {
if (this._refStack.has(schema.$ref)) {
console.warn(`Circular $ref detected: ${schema.$ref}`);
return { type: 'object', description: '[Circular $ref]', $ref: schema.$ref };
}
this._refStack.add(schema.$ref);
try {
resolvedSchema = this.resolveRef(schema.$ref);
} catch (error) {
console.warn(`Failed to resolve $ref: ${schema.$ref} - ${error.message}`);
return { type: 'object', $ref: schema.$ref, error: 'Failed to resolve' };
} finally {
this._refStack.delete(schema.$ref);
}
}
// Build the resolved schema safely
const result = {
type: resolvedSchema.type,
format: resolvedSchema.format,
description: resolvedSchema.description,
example: resolvedSchema.example,
default: resolvedSchema.default,
enum: resolvedSchema.enum,
nullable: resolvedSchema.nullable,
deprecated: resolvedSchema.deprecated
};
// Cache early to prevent re-processing
this._globalCache.set(schemaKey, result);
// Handle different schema types with depth tracking
const newContext = {
depth: context.depth + 1,
visited: new Set(context.visited),
path: context.path
};
if (resolvedSchema.type === 'object' && resolvedSchema.properties) {
result.properties = {};
result.required = resolvedSchema.required || [];
const propertyKeys = Object.keys(resolvedSchema.properties).slice(0, 50); // Limit properties
for (const propName of propertyKeys) {
const propSchema = resolvedSchema.properties[propName];
const propPath = context.path ? `${context.path}.${propName}` : propName;
result.properties[propName] = this.resolveSchema(propSchema, {
...newContext,
path: propPath
});
}
if (resolvedSchema.additionalProperties !== undefined) {
if (typeof resolvedSchema.additionalProperties === 'object') {
result.additionalProperties = this.resolveSchema(resolvedSchema.additionalProperties, {
...newContext,
path: `${context.path}.<additional>`
});
} else {
result.additionalProperties = resolvedSchema.additionalProperties;
}
}
} else if (resolvedSchema.type === 'array' && resolvedSchema.items) {
result.items = this.resolveSchema(resolvedSchema.items, {
...newContext,
path: `${context.path}[]`
});
result.minItems = resolvedSchema.minItems;
result.maxItems = resolvedSchema.maxItems;
result.uniqueItems = resolvedSchema.uniqueItems;
}
// Handle string constraints
if (resolvedSchema.type === 'string') {
result.minLength = resolvedSchema.minLength;
result.maxLength = resolvedSchema.maxLength;
result.pattern = resolvedSchema.pattern;
}
// Handle number constraints
if (resolvedSchema.type === 'number' || resolvedSchema.type === 'integer') {
result.minimum = resolvedSchema.minimum;
result.maximum = resolvedSchema.maximum;
result.exclusiveMinimum = resolvedSchema.exclusiveMinimum;
result.exclusiveMaximum = resolvedSchema.exclusiveMaximum;
result.multipleOf = resolvedSchema.multipleOf;
}
// Handle combined schemas with strict limits
if (context.depth < MAX_DEPTH - 2) {
if (resolvedSchema.allOf && resolvedSchema.allOf.length <= 10) {
result.allOf = resolvedSchema.allOf.map((s, i) =>
this.resolveSchema(s, { ...newContext, path: `${context.path}.allOf[${i}]` })
);
}
if (resolvedSchema.oneOf && resolvedSchema.oneOf.length <= 10) {
result.oneOf = resolvedSchema.oneOf.map((s, i) =>
this.resolveSchema(s, { ...newContext, path: `${context.path}.oneOf[${i}]` })
);
}
if (resolvedSchema.anyOf && resolvedSchema.anyOf.length <= 10) {
result.anyOf = resolvedSchema.anyOf.map((s, i) =>
this.resolveSchema(s, { ...newContext, path: `${context.path}.anyOf[${i}]` })
);
}
}
return result;
} catch (error) {
console.warn(`Error resolving schema at path ${context.path}:`, error.message);
return { type: 'object', error: error.message };
}
}
/**
* Create a stable, unique key for schema caching
*/
_createStableSchemaKey(schema, path) {
if (!schema) return 'null';
// For $ref, use the ref itself as the primary key
if (schema.$ref) {
return `ref:${schema.$ref}:${path}`;
}
// Create a deterministic fingerprint
const parts = [
schema.type || 'any',
schema.title || '',
schema.format || '',
schema.description ? schema.description.substring(0, 50) : '',
Object.keys(schema.properties || {}).length.toString(),
schema.items ? 'hasItems' : '',
path
];
return parts.join('|');
}
/**
* Resolve $ref references with better error handling
*/
resolveRef(ref) {
// Check cache first
if (this._refCache.has(ref)) {
return this._refCache.get(ref);
}
const parts = ref.split('/');
let current = this.spec;
try {
for (let i = 1; i < parts.length; i++) {
if (!current || typeof current !== 'object') {
throw new Error(`Invalid reference path at segment: ${parts[i]}`);
}
current = current[parts[i]];
if (current === undefined) {
throw new Error(`Reference path not found at segment: ${parts[i]}`);
}
}
// Cache the result
this._refCache.set(ref, current);
return current;
} catch (error) {
throw new Error(`Cannot resolve reference: ${ref} - ${error.message}`);
}
}
/**
* Get all schemas with improved caching
*/
getAllSchemas() {
if (this._resolvedSchemas) {
return this._resolvedSchemas;
}
const schemas = this.spec.components?.schemas || this.spec.definitions || {};
const result = {};
// Process schemas with limits
const schemaNames = Object.keys(schemas).slice(0, 100); // Limit total schemas
for (const name of schemaNames) {
try {
result[name] = this.resolveSchema(schemas[name], {
depth: 0,
visited: new Set(),
path: name
});
} catch (error) {
console.warn(`Failed to resolve schema ${name}:`, error.message);
result[name] = { type: 'object', error: error.message };
}
}
this._resolvedSchemas = result;
return result;
}
/**
* Search endpoints by keyword
*/
searchEndpoints(keyword) {
const lowerKeyword = keyword.toLowerCase();
const endpoints = this.getAllEndpoints();
return endpoints.filter(endpoint => {
const searchText = `${endpoint.path} ${endpoint.method} ${endpoint.operationId || ''} ${endpoint.summary || ''} ${endpoint.description || ''} ${endpoint.tags.join(' ')}`.toLowerCase();
return searchText.includes(lowerKeyword);
});
}
/**
* Get endpoints by tag
*/
getEndpointsByTag(tag) {
const endpoints = this.getAllEndpoints();
return endpoints.filter(endpoint => endpoint.tags.includes(tag));
}
/**
* Get a summary of the API
*/
getAPISummary() {
const endpoints = this.getAllEndpoints();
const tags = new Set();
const methods = {};
endpoints.forEach(endpoint => {
endpoint.tags.forEach(tag => tags.add(tag));
methods[endpoint.method] = (methods[endpoint.method] || 0) + 1;
});
return {
title: this.spec.info?.title,
version: this.spec.info?.version,
description: this.spec.info?.description,
servers: this.spec.servers || [{ url: this.spec.host ? `${this.spec.schemes?.[0] || 'http'}://${this.spec.host}${this.spec.basePath || ''}` : 'N/A' }],
totalEndpoints: endpoints.length,
tags: Array.from(tags),
methodCounts: methods,
security: this.spec.security || [],
externalDocs: this.spec.externalDocs
};
}
/**
* Robust property finder with better recursion protection
*/
_findPropertyRecursive(currentSchema, propertyName, currentPath, results, context = { depth: 0, visited: new Set(), pathStack: new Set() }) {
// Hard limits
const MAX_DEPTH = 15;
const MAX_RESULTS = 100;
const MAX_PATHS = 50;
if (context.depth > MAX_DEPTH || results.length > MAX_RESULTS || context.pathStack.size > MAX_PATHS) {
return;
}
if (!currentSchema || typeof currentSchema !== 'object') return;
// Create unique identifier for this search context
const contextKey = `${currentPath}:${context.depth}:${propertyName}`;
if (context.visited.has(contextKey)) {
return;
}
if (context.pathStack.has(currentPath)) {
return; // Circular path detected
}
context.visited.add(contextKey);
context.pathStack.add(currentPath);
try {
// Handle $ref resolution with protection
if (currentSchema.$ref) {
if (this._refStack.has(currentSchema.$ref)) {
return; // Circular reference
}
this._refStack.add(currentSchema.$ref);
try {
const resolvedSchema = this.resolveRef(currentSchema.$ref);
this._findPropertyRecursive(resolvedSchema, propertyName, currentPath, results, {
depth: context.depth + 1,
visited: new Set(context.visited),
pathStack: new Set(context.pathStack)
});
} catch (error) {
console.warn(`Failed to resolve $ref in property search: ${currentSchema.$ref}`);
} finally {
this._refStack.delete(currentSchema.$ref);
}
return;
}
// Handle object properties with limits
if (currentSchema.type === 'object' && currentSchema.properties) {
const propertyKeys = Object.keys(currentSchema.properties).slice(0, 20); // Limit properties per object
for (const key of propertyKeys) {
const subSchema = currentSchema.properties[key];
const newPath = currentPath ? `${currentPath}.${key}` : key;
// Check for property match
if (key.toLowerCase() === propertyName.toLowerCase()) {
results.push({
path: newPath,
validation: this._simplifySchema(subSchema),
depth: context.depth
});
}
// Continue searching deeper with new context
if (this._shouldRecurseInto(subSchema) && context.depth < MAX_DEPTH - 2) {
this._findPropertyRecursive(subSchema, propertyName, newPath, results, {
depth: context.depth + 1,
visited: new Set(context.visited),
pathStack: new Set(context.pathStack)
});
}
}
}
// Handle array items with protection
if (currentSchema.type === 'array' && currentSchema.items && context.depth < MAX_DEPTH - 2) {
const itemsPath = currentPath ? `${currentPath}[]` : '[]';
if (this._shouldRecurseInto(currentSchema.items)) {
this._findPropertyRecursive(currentSchema.items, propertyName, itemsPath, results, {
depth: context.depth + 1,
visited: new Set(context.visited),
pathStack: new Set(context.pathStack)
});
}
}
// Handle combined schemas with strict limits
if (context.depth < MAX_DEPTH - 3) {
const combinedSchemas = [
...(currentSchema.allOf || []).slice(0, 5),
...(currentSchema.oneOf || []).slice(0, 5),
...(currentSchema.anyOf || []).slice(0, 5)
];
for (const schema of combinedSchemas) {
if (this._shouldRecurseInto(schema)) {
this._findPropertyRecursive(schema, propertyName, currentPath, results, {
depth: context.depth + 1,
visited: new Set(context.visited),
pathStack: new Set(context.pathStack)
});
}
}
}
// Handle additionalProperties with protection
if (currentSchema.additionalProperties &&
typeof currentSchema.additionalProperties === 'object' &&
context.depth < MAX_DEPTH - 2) {
const additionalPath = currentPath ? `${currentPath}.<additionalProperty>` : '<additionalProperty>';
if (this._shouldRecurseInto(currentSchema.additionalProperties)) {
this._findPropertyRecursive(currentSchema.additionalProperties, propertyName, additionalPath, results, {
depth: context.depth + 1,
visited: new Set(context.visited),
pathStack: new Set(context.pathStack)
});
}
}
} catch (error) {
console.warn(`Error in property search at path ${currentPath}:`, error.message);
} finally {
context.pathStack.delete(currentPath);
}
}
/**
* Enhanced recursion safety check
*/
_shouldRecurseInto(schema) {
if (!schema || typeof schema !== 'object') return false;
// Don't recurse into simple types
if (schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type)) {
// Unless they have special properties that might contain nested schemas
return !!(schema.properties || schema.items || schema.allOf || schema.oneOf || schema.anyOf);
}
// Check if there's something meaningful to recurse into
return !!(
schema.$ref ||
schema.properties ||
schema.items ||
schema.allOf ||
schema.oneOf ||
schema.anyOf ||
(schema.additionalProperties && typeof schema.additionalProperties === 'object')
);
}
/**
* Simplify schema for output
*/
_simplifySchema(schema) {
if (!schema) return null;
const simplified = {
type: schema.type,
format: schema.format,
description: schema.description ? schema.description.substring(0, 200) : undefined,
example: schema.example,
default: schema.default,
enum: schema.enum,
required: schema.required
};
// Add validation constraints based on type
if (schema.type === 'string') {
if (schema.minLength !== undefined) simplified.minLength = schema.minLength;
if (schema.maxLength !== undefined) simplified.maxLength = schema.maxLength;
if (schema.pattern) simplified.pattern = schema.pattern;
} else if (schema.type === 'number' || schema.type === 'integer') {
if (schema.minimum !== undefined) simplified.minimum = schema.minimum;
if (schema.maximum !== undefined) simplified.maximum = schema.maximum;
} else if (schema.type === 'array') {
if (schema.minItems !== undefined) simplified.minItems = schema.minItems;
if (schema.maxItems !== undefined) simplified.maxItems = schema.maxItems;
simplified.items = schema.items?.$ref || schema.items?.type || 'any';
}
// Include $ref if present
if (schema.$ref) simplified.$ref = schema.$ref;
// Remove undefined values
Object.keys(simplified).forEach(key => {
if (simplified[key] === undefined) {
delete simplified[key];
}
});
return simplified;
}
/**
* Find property with enhanced protection
*/
findProperty(propertyName, schemaName = null) {
// Clear global state
this._refStack.clear();
this._globalVisited = new WeakSet();
const rawSchemas = this.spec.components?.schemas || this.spec.definitions || {};
const results = [];
try {
if (schemaName) {
// Scoped search
const foundSchemaKey = Object.keys(rawSchemas).find(k => k.toLowerCase() === schemaName.toLowerCase());
if (!foundSchemaKey) {
return { error: `Schema '${schemaName}' not found.` };
}
const schemaToSearch = rawSchemas[foundSchemaKey];
this._findPropertyRecursive(schemaToSearch, propertyName, '', results);
return results.map(r => ({ ...r, path: `${foundSchemaKey}.${r.path}` }));
} else {
// Global search with limits
const globalResults = {};
const schemaKeys = Object.keys(rawSchemas).slice(0, 50); // Limit schemas to search
for (const key of schemaKeys) {
const schema = rawSchemas[key];
const schemaResults = [];
try {
this._findPropertyRecursive(schema, propertyName, '', schemaResults);
if (schemaResults.length > 0) {
globalResults[key] = schemaResults.map(r => ({ ...r, path: `${key}.${r.path}` }));
}
} catch (error) {
console.warn(`Error searching in schema ${key}:`, error.message);
}
// Reset state between schemas
this._refStack.clear();
}
return globalResults;
}
} catch (error) {
console.error('Property search failed:', error.message);
return { error: error.message };
}
}
/**
* Test recursion handling with better protection
*/
testRecursionHandling() {
const testResults = {
totalSchemas: 0,
complexSchemas: [],
deepestNesting: 0,
circularReferences: [],
errors: [],
cacheStats: {
globalCacheSize: this._globalCache.size,
refCacheSize: this._refCache.size
}
};
try {
const schemas = this.spec.components?.schemas || this.spec.definitions || {};
testResults.totalSchemas = Object.keys(schemas).length;
// Limit testing to prevent runaway processes
const schemasToTest = Object.entries(schemas).slice(0, 20);
for (const [name, schema] of schemasToTest) {
try {
// Clear state before each test
this._refStack.clear();
// Test resolution with timeout
const startTime = Date.now();
const resolved = this.resolveSchema(schema, {
depth: 0,
visited: new Set(),
path: name
});
const endTime = Date.now();
if (endTime - startTime > 5000) { // 5 second timeout
testResults.errors.push({
schema: name,
error: 'Resolution timeout'
});
continue;
}
// Measure complexity safely
const depth = this._measureSchemaDepth(schema, new Set(), 0, 10); // Max depth 10 for testing
if (depth > 5) {
testResults.complexSchemas.push({
name,
depth,
hasCircularRef: this._hasCircularReference(schema, [], new Set(), 10)
});
}
if (depth > testResults.deepestNesting) {
testResults.deepestNesting = depth;
}
// Check for circular references safely
if (this._hasCircularReference(schema, [], new Set(), 10)) {
testResults.circularReferences.push(name);
}
} catch (error) {
testResults.errors.push({
schema: name,
error: error.message
});
}
}
} catch (error) {
testResults.errors.push({
general: error.message
});
}
return testResults;
}
/**
* Measure schema depth with protection
*/
_measureSchemaDepth(schema, visited, depth, maxDepth) {
if (!schema || depth >= maxDepth) return depth;
const schemaKey = schema.$ref || JSON.stringify(schema).substring(0, 50);
if (visited.has(schemaKey)) return depth;
visited.add(schemaKey);
let maxFoundDepth = depth;
try {
if (schema.$ref) {
const resolved = this.resolveRef(schema.$ref);
maxFoundDepth = Math.max(maxFoundDepth, this._measureSchemaDepth(resolved, visited, depth + 1, maxDepth));
}
if (schema.properties) {
const props = Object.values(schema.properties).slice(0, 10); // Limit properties
for (const prop of props) {
maxFoundDepth = Math.max(maxFoundDepth, this._measureSchemaDepth(prop, visited, depth + 1, maxDepth));
}
}
if (schema.items) {
maxFoundDepth = Math.max(maxFoundDepth, this._measureSchemaDepth(schema.items, visited, depth + 1, maxDepth));
}
const combined = [
...(schema.allOf || []).slice(0, 5),
...(schema.oneOf || []).slice(0, 5),
...(schema.anyOf || []).slice(0, 5)
];
for (const subSchema of combined) {
maxFoundDepth = Math.max(maxFoundDepth, this._measureSchemaDepth(subSchema, visited, depth + 1, maxDepth));
}
} catch (error) {
// Ignore errors in depth measurement
}
return maxFoundDepth;
}
/**
* Check for circular references with protection
*/
_hasCircularReference(schema, path, visited, maxDepth) {
if (!schema || path.length >= maxDepth) return false;
const schemaId = schema.$ref || JSON.stringify(schema).substring(0, 50);
if (path.includes(schemaId)) return true;
if (visited.has(schemaId)) return false;
visited.add(schemaId);
const newPath = [...path, schemaId];
try {
if (schema.$ref) {
const resolved = this.resolveRef(schema.$ref);
if (this._hasCircularReference(resolved, newPath, visited, maxDepth)) return true;
}
if (schema.properties) {
const props = Object.values(schema.properties).slice(0, 10);
for (const prop of props) {
if (this._hasCircularReference(prop, newPath, visited, maxDepth)) return true;
}
}
if (schema.items && this._hasCircularReference(schema.items, newPath, visited, maxDepth)) return true;
const combined = [
...(schema.allOf || []).slice(0, 5),
...(schema.oneOf || []).slice(0, 5),
...(schema.anyOf || []).slice(0, 5)
];
for (const subSchema of combined) {
if (this._hasCircularReference(subSchema, newPath, visited, maxDepth)) return true;
}
} catch (error) {
// Ignore errors in circular reference detection
}
return false;
}
}
// CLI Handler
async function main() {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log(`
Usage:
node openapi-parser.js <openapi-file> [command] [options]
Commands:
--summary Get a high-level summary of the API.
--endpoints List all available endpoints.
--endpoint <METHOD> <PATH> Get details for a specific endpoint.
--schemas List all defined data schemas.
--search <keyword> Search endpoints by a keyword.
--tag <tag> Get endpoints associated with a specific tag.
--find-property <prop-name> Find a property globally across all schemas.
--find-property <prop-name> --in-schema <SchemaName>
Find a property only within a specific schema.
--test-recursion Test the parser for recursion handling and schema complexity.
Examples:
node openapi-parser.js api.json --summary
node openapi-parser.js api.json --find-property status
node openapi-parser.js api.json --find-property type --in-schema Experiment
node openapi-parser.js api.json --test-recursion
`);
process.exit(1);
}
try {
const filename = args[0];
const command = args[1];
const content = await fs.readFile(filename, 'utf8');
const spec = JSON.parse(content);
const parser = new OpenAPIParser(spec);
let result;
switch (command) {
case '--summary':
result = parser.getAPISummary();
break;
case '--endpoints':
result = parser.getAllEndpoints();
break;
case '--endpoint':
if (args.length < 4) {
console.error('Error: Please provide METHOD and PATH for --endpoint');
process.exit(1);
}
result = parser.getEndpointDetails(args[2], args[3]);
break;
case '--schemas':
result = parser.getAllSchemas();
break;
case '--search':
if (args.length < 3) {
console.error('Error: Please provide a search keyword for --search');
process.exit(1);
}
result = parser.searchEndpoints(args[2]);
break;
case '--tag':
if (args.length < 3) {
console.error('Error: Please provide a tag name for --tag');
process.exit(1);
}
result = parser.getEndpointsByTag(args[2]);
break;
case '--find-property': {
if (args.length < 3) {
console.error('Error: Please provide a property name for --find-property');
process.exit(1);
}
const propertyName = args[2];
const schemaFlagIndex = args.indexOf('--in-schema');
if (schemaFlagIndex !== -1 && args.length > schemaFlagIndex + 1) {
// Scoped search
const schemaName = args[schemaFlagIndex + 1];
result = parser.findProperty(propertyName, schemaName);
} else {
// Global search
result = parser.findProperty(propertyName);
}
break;
}
case '--test-recursion':
result = parser.testRecursionHandling();
break;
default:
console.error(`Unknown command: ${command}`);
process.exit(1);
}
console.log(JSON.stringify(result, null, 2));
} catch (error) {
// Provide more detailed error information
console.error('Error:', error.message);
if (error.stack && (error.stack.includes('Maximum call stack size exceeded') || error.message.includes('recursion'))) {
console.error('\nRecursion error detected! The parser has built-in protection, but this schema may be extremely complex.');
console.error('Try running with --test-recursion to identify problematic schemas.');
}
if (process.env.DEBUG) {
console.error('\nStack trace:', error.stack);
}
process.exit(1);
}
}
// Export for use as a module
export { OpenAPIParser };
// Run CLI if called directly
if (import.meta.url.endsWith(process.argv[1])) {
main();
}