@simpleapps-com/augur-api
Version:
TypeScript client library for Augur microservices API endpoints
456 lines • 15.7 kB
JavaScript
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