UNPKG

@tryloop/oats

Version:

🌾 OATS - OpenAPI TypeScript Sync. The missing link between your OpenAPI specs and TypeScript applications. Automatically watch, generate, and sync TypeScript clients from your API definitions.

257 lines • 8.35 kB
/** * OATS Swagger/OpenAPI Change Detection * * Intelligent detection of meaningful changes in API specifications * * @module @oatsjs/core/swagger-diff */ import crypto from 'crypto'; /** * Detects meaningful changes in OpenAPI/Swagger specifications */ export class SwaggerChangeDetector { lastState = null; lastSpec = null; currentState = null; /** * Check if API specification has significant changes */ hasSignificantChanges(currentSpec) { const currentState = this.calculateSpecHash(currentSpec); this.currentState = currentState; if (this.lastState === null) { // First time, consider it a change this.lastState = currentState; this.lastSpec = currentSpec; return true; } if (this.lastState === currentState) { // No changes at all return false; } // Analyze the actual changes const changes = this.detectChanges(this.lastSpec, currentSpec); // Update state this.lastState = currentState; this.lastSpec = currentSpec; // Consider changes significant if they affect the API contract return this.areChangesSignificant(changes); } /** * Get the current spec hash */ getCurrentHash() { return this.currentState; } /** * Get detailed change analysis */ analyzeChanges(previousSpec, currentSpec) { const changes = this.detectChanges(previousSpec, currentSpec); const summary = { major: changes.filter((c) => c.severity === 'major').length, minor: changes.filter((c) => c.severity === 'minor').length, patch: changes.filter((c) => c.severity === 'patch').length, }; return { hasChanges: changes.length > 0, changes, summary, }; } /** * Calculate hash of API specification */ calculateSpecHash(spec) { // Extract only the meaningful parts for hashing const relevantSpec = this.extractRelevantParts(spec); // Serialize with sorted keys for consistent hashing const serialized = JSON.stringify(relevantSpec, null, 2); return crypto.createHash('sha256').update(serialized).digest('hex'); } /** * Extract parts of spec that matter for API contract */ extractRelevantParts(spec) { if (!spec) { return {}; } const relevant = {}; // Core spec info if (spec.info) { relevant.info = { version: spec.info.version, title: spec.info.title, }; } // Paths (endpoints) if (spec.paths) { relevant.paths = this.normalizePaths(spec.paths); } // Components/definitions (schemas) if (spec.components?.schemas) { relevant.schemas = spec.components.schemas; } else if (spec.definitions) { relevant.schemas = spec.definitions; } // Security definitions if (spec.components?.securitySchemes) { relevant.security = spec.components.securitySchemes; } else if (spec.securityDefinitions) { relevant.security = spec.securityDefinitions; } return relevant; } /** * Normalize paths for comparison */ normalizePaths(paths) { const normalized = {}; for (const [path, pathItem] of Object.entries(paths)) { normalized[path] = {}; for (const [method, operation] of Object.entries(pathItem)) { if (typeof operation === 'object' && operation !== null) { const op = operation; normalized[path][method] = { operationId: op.operationId, summary: op.summary, parameters: op.parameters || [], requestBody: op.requestBody, responses: op.responses || {}, tags: op.tags || [], }; } } } return normalized; } /** * Detect specific changes between specs */ detectChanges(oldSpec, newSpec) { const changes = []; if (!oldSpec || !newSpec) { return changes; } // Extract and normalize relevant parts for comparison const oldRelevant = this.extractRelevantParts(oldSpec); const newRelevant = this.extractRelevantParts(newSpec); // Compare normalized paths changes.push(...this.compareObjects(oldRelevant.paths || {}, newRelevant.paths || {}, 'paths', this.classifyPathChange.bind(this))); // Compare schemas changes.push(...this.compareObjects(oldRelevant.schemas || {}, newRelevant.schemas || {}, 'schemas', this.classifySchemaChange.bind(this))); return changes; } /** * Compare two objects and detect changes */ compareObjects(oldObj, newObj, basePath, classifier) { const changes = []; const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]); for (const key of allKeys) { const path = `${basePath}.${key}`; const oldValue = oldObj[key]; const newValue = newObj[key]; if (oldValue === undefined && newValue !== undefined) { // Added const { description, severity } = classifier('added', path); changes.push({ type: 'added', path, description, severity, }); } else if (oldValue !== undefined && newValue === undefined) { // Removed const { description, severity } = classifier('removed', path); changes.push({ type: 'removed', path, description, severity, }); } else if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) { // Modified const { description, severity } = classifier('modified', path); changes.push({ type: 'modified', path, description, severity, }); } } return changes; } /** * Classify path changes */ classifyPathChange(changeType, path) { if (changeType === 'added') { return { description: `New endpoint added: ${path}`, severity: 'minor', }; } else if (changeType === 'removed') { return { description: `Endpoint removed: ${path}`, severity: 'major', }; } else { return { description: `Endpoint modified: ${path}`, severity: 'minor', }; } } /** * Classify schema changes */ classifySchemaChange(changeType, path) { if (changeType === 'added') { return { description: `New schema added: ${path}`, severity: 'minor', }; } else if (changeType === 'removed') { return { description: `Schema removed: ${path}`, severity: 'major', }; } else { return { description: `Schema modified: ${path}`, severity: 'minor', }; } } /** * Determine if changes are significant enough to trigger regeneration */ areChangesSignificant(changes) { // Any major or minor changes are significant return changes.some((change) => change.severity === 'major' || change.severity === 'minor'); } /** * Reset the detector state */ reset() { this.lastState = null; this.lastSpec = null; } /** * Get current state hash */ getCurrentState() { return this.lastState; } } //# sourceMappingURL=swagger-diff.js.map