UNPKG

@simpleapps-com/augur-api

Version:

TypeScript client library for Augur microservices API endpoints

456 lines 15.7 kB
import { validate, parse } from '@readme/openapi-parser'; export class OpenApiSpecParser { constructor() { this.api = null; this.specPath = null; this.parsedAt = null; } /** * Load and parse OpenAPI specification * * @param source File path, URL, or spec object * @returns Parsed OpenAPI Document * @throws Error if specification cannot be parsed * * @example * ```typescript * const parser = new OpenApiSpecParser(); * await parser.loadSpec('./openapi/vmi.json'); * ``` */ async loadSpec(source) { try { // First try to parse (more lenient) this.api = (await parse(source)); this.specPath = typeof source === 'string' ? source : null; this.parsedAt = new Date(); return this.api; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to load OpenAPI spec: ${message}`); } } /** * Validate OpenAPI specification against schema * * @param source File path, URL, or spec object * @returns Validation result with errors and warnings */ async validateSpec(source) { try { const sourceToValidate = source || this.specPath || this.api; if (!sourceToValidate) { throw new Error('No specification to validate'); } const result = await validate(sourceToValidate); return { isValid: result.valid, errors: [], warnings: result.warnings.map(w => w.message || 'Warning'), }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to validate OpenAPI spec: ${message}`); } } /** * Get all API paths with their methods and metadata * * Replaces: jq -r '.paths | to_entries[] | "\(.key): \(.value | keys | join(", "))"' * * @returns Array of path information objects */ getAllPaths() { if (!this.api?.paths) return []; return Object.entries(this.api.paths).map(([path, pathItem]) => { const methods = this.extractMethods(pathItem); const pathParams = this.extractPathParameters(path); return { path, methods, parameters: pathParams, operationIds: this.extractOperationIds(pathItem), hasPathParams: pathParams.length > 0, pathParams, }; }); } /** * Get formatted path list (exact jq replacement) * * @returns Array of formatted strings like "path: GET, POST" */ getAllPathsFormatted() { return this.getAllPaths().map(p => `${p.path}: ${p.methods.join(', ')}`); } /** * Get total number of paths * * Replaces: jq '.paths | keys | length' * * @returns Number of API paths */ getPathCount() { return Object.keys(this.api?.paths || {}).length; } /** * Get total number of endpoints (path + method combinations) * * @returns Total number of API endpoints */ getEndpointCount() { return this.getAllPaths().reduce((sum, path) => sum + path.methods.length, 0); } /** * Get OpenAPI specification info * * Replaces: jq '.info' * * @returns OpenAPI info object or null */ getSpecInfo() { return this.api?.info || null; } /** * Get server information * * @returns Array of server definitions */ getServers() { return this.api?.servers || []; } /** * Get detailed information about a specific endpoint * * @param path API path (e.g., "/users/{id}") * @param method HTTP method (e.g., "GET") * @returns Operation details or null if not found */ getPathDetails(path, method) { const pathItem = this.api?.paths?.[path]; const operation = pathItem?.[method.toLowerCase()]; if (!operation || typeof operation !== 'object') return null; const pathParams = pathItem?.parameters || []; const opParams = operation.parameters || []; return { operationId: operation.operationId, summary: operation.summary, description: operation.description, parameters: [...pathParams, ...opParams], requestBody: operation.requestBody, responses: operation.responses || {}, tags: operation.tags || [], deprecated: operation.deprecated || false, method: method.toUpperCase(), path, }; } /** * Get all operation details for all endpoints * * @returns Array of all operation details */ getAllOperations() { const operations = []; const paths = this.getAllPaths(); paths.forEach(pathInfo => { pathInfo.methods.forEach(method => { const details = this.getPathDetails(pathInfo.path, method); if (details) { operations.push(details); } }); }); return operations; } /** * Find deprecated endpoints * * Replaces complex jq deprecated endpoint queries * * @returns Array of deprecated endpoint descriptions */ getDeprecatedPaths() { if (!this.api?.paths) return []; const deprecated = []; Object.entries(this.api.paths).forEach(([path, pathItem]) => { Object.entries(pathItem).forEach(([method, operation]) => { if (typeof operation === 'object' && operation && operation?.deprecated) { deprecated.push(`${method.toUpperCase()} ${path}`); } }); }); return deprecated; } /** * Search for endpoints by functionality * * @param searchTerm Term to search for in paths, summaries, descriptions, etc. * @returns Array of search results ranked by relevance */ findEndpoints(searchTerm) { if (!this.api?.paths) return []; const results = []; const term = searchTerm.toLowerCase(); Object.entries(this.api.paths).forEach(([path, pathItem]) => { Object.entries(pathItem).forEach(([method, operation]) => { if (typeof operation !== 'object' || !operation) return; const opRecord = operation; let score = 0; const reasons = []; // Score path name if (path.toLowerCase().includes(term)) { score += 10; reasons.push('path match'); } // Score summary const summary = opRecord.summary; if (summary?.toLowerCase().includes(term)) { score += 8; reasons.push('summary match'); } // Score description const description = opRecord.description; if (description?.toLowerCase().includes(term)) { score += 6; reasons.push('description match'); } // Score operation ID const operationId = opRecord.operationId; if (operationId?.toLowerCase().includes(term)) { score += 7; reasons.push('operationId match'); } // Score tags const tags = opRecord.tags; if (tags?.some((tag) => tag.toLowerCase().includes(term))) { score += 5; reasons.push('tag match'); } if (score > 0) { const details = this.getPathDetails(path, method); if (details) { results.push({ path, method: method.toUpperCase(), score, matchReason: reasons.join(', '), operation: details, }); } } }); }); return results.sort((a, b) => b.score - a.score); } /** * Get component schemas * * @returns Object containing all component schemas */ getComponentSchemas() { const schemas = this.api?.components?.schemas; return schemas || {}; } /** * Get complexity metrics for the API * * @returns Detailed complexity analysis */ getComplexityMetrics() { const paths = this.getAllPaths(); const operations = this.getAllOperations(); const totalEndpoints = operations.length; const pathsWithParameters = paths.filter(p => p.hasPathParams).length; const uniqueParameters = [...new Set(paths.flatMap(p => p.pathParams))]; const deprecated = this.getDeprecatedPaths(); // Method distribution const methodDistribution = {}; operations.forEach(op => { methodDistribution[op.method] = (methodDistribution[op.method] || 0) + 1; }); return { totalPaths: paths.length, totalEndpoints, pathsWithParameters, uniqueParameterCount: uniqueParameters.length, averageMethodsPerPath: totalEndpoints / (paths.length || 1), complexityScore: this.calculateComplexityScore(paths), deprecatedCount: deprecated.length, methodDistribution, }; } /** * Generate client structure information for all endpoints * This analyzes how each OpenAPI path should map to client method structure * * @returns Array of client structure mappings */ generateClientStructure() { const operations = this.getAllOperations(); return operations.map(operation => { const clientPath = this.pathToClientStructure(operation.path); const parameters = this.analyzeParameters(operation.parameters); const responseSchema = this.extractResponseSchema(operation.responses); return { path: operation.path, clientPath, method: operation.method, operationId: operation.operationId, parameters, responseSchema, }; }); } /** * Validate that the API specification follows expected patterns * * @returns Validation result with any issues found */ validateSpecification() { const errors = []; const warnings = []; if (!this.api) { errors.push('No API specification loaded'); return { isValid: false, errors, warnings }; } // Check required fields if (!this.api.info?.title) { errors.push('Missing required field: info.title'); } if (!this.api.info?.version) { errors.push('Missing required field: info.version'); } // Check paths const paths = this.getAllPaths(); if (paths.length === 0) { warnings.push('No API paths defined'); } // Check for operations without operationId const operations = this.getAllOperations(); const missingOperationIds = operations.filter(op => !op.operationId); if (missingOperationIds.length > 0) { warnings.push(`${missingOperationIds.length} operations missing operationId`); } // Check for deprecated operations const deprecated = this.getDeprecatedPaths(); if (deprecated.length > 0) { warnings.push(`${deprecated.length} deprecated endpoints found`); } return { isValid: errors.length === 0, errors, warnings, }; } /** * Get parsing metadata * * @returns Information about when the spec was parsed */ getParsingInfo() { return { specPath: this.specPath, parsedAt: this.parsedAt, api: !!this.api, }; } // Private helper methods extractMethods(pathItem) { return Object.keys(pathItem).filter(key => ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'trace'].includes(key)); } extractPathParameters(path) { const matches = path.match(/\{([^}]+)\}/g); return matches ? matches.map(match => match.slice(1, -1)) : []; } extractOperationIds(pathItem) { const methods = this.extractMethods(pathItem); return methods .map(method => { const operation = pathItem[method]; return operation?.operationId; }) .filter((id) => Boolean(id)); } calculateComplexityScore(paths) { let score = 0; paths.forEach(path => { // Base score for each path score += 1; // Additional score for parameters score += path.parameters.length * 0.5; // Additional score for multiple methods score += (path.methods.length - 1) * 0.3; // Additional score for complex paths (many segments) const segments = path.path.split('/').filter(s => s).length; score += Math.max(0, segments - 2) * 0.2; }); return score; } pathToClientStructure(path) { // Convert "/inv-mast/{invMastUid}/alternate-code" // to ["invMast", "alternateCode"] return path .split('/') .filter(segment => segment && !segment.startsWith('{')) .map(segment => this.toCamelCase(segment)); } toCamelCase(str) { return str.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase()); } analyzeParameters(parameters) { const result = { path: [], query: [], header: [], }; parameters.forEach(param => { if (param.in === 'path') { result.path.push(param.name); } else if (param.in === 'query') { result.query.push(param.name); } else if (param.in === 'header') { result.header.push(param.name); } }); return result; } extractResponseSchema(responses) { // Look for 200 response schema const successResponse = responses['200'] || responses['201']; if (successResponse?.content?.['application/json']) { const jsonContent = successResponse.content['application/json']; return jsonContent?.schema || null; } return null; } } /** * Factory function for easy usage * * @returns New OpenApiSpecParser instance */ export const createOpenApiParser = () => new OpenApiSpecParser(); /** * Utility function to parse OpenAPI spec from file path * * @param filePath Path to OpenAPI specification file * @returns Configured parser instance */ export const parseOpenApiSpec = async (filePath) => { const parser = createOpenApiParser(); await parser.loadSpec(filePath); return parser; }; //# sourceMappingURL=OpenApiSpecParser.js.map