mirror of
https://github.com/LukeHagar/varsity.git
synced 2025-12-06 04:22:00 +00:00
Initial commit
This commit is contained in:
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
177
README.md
Normal file
177
README.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Varsity
|
||||
|
||||
A comprehensive OpenAPI parsing and validation library built with TypeScript and Bun. Varsity provides both a command-line interface and a programmatic API for validating OpenAPI specifications across all versions (2.0, 3.0.x, 3.1.0).
|
||||
|
||||
## Features
|
||||
|
||||
- 🔍 **Multi-version Support**: Validates OpenAPI 2.0 (Swagger) and OpenAPI 3.0.x/3.1.0 specifications
|
||||
- 📊 **Comprehensive Reporting**: Generate reports in JSON, YAML, HTML, and Markdown formats
|
||||
- 🚀 **High Performance**: Built with Bun for fast execution
|
||||
- 🛠️ **Flexible Usage**: Use as a CLI tool or import as a library
|
||||
- ✅ **AJV Integration**: Robust validation using the industry-standard AJV library
|
||||
- 📝 **Detailed Error Reporting**: Clear error messages with path information
|
||||
- 🔧 **Extensible**: Support for custom validation rules and schemas
|
||||
- 🎯 **Type Safety**: Full TypeScript support with comprehensive OpenAPI type definitions from `oas-types`
|
||||
- 📋 **Comprehensive Schemas**: Uses official JSON schemas for accurate validation
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
#### Validate a specification
|
||||
```bash
|
||||
bun run src/cli.ts validate path/to/spec.json
|
||||
```
|
||||
|
||||
#### Parse without validation
|
||||
```bash
|
||||
bun run src/cli.ts parse path/to/spec.json
|
||||
```
|
||||
|
||||
#### Generate a report
|
||||
```bash
|
||||
bun run src/cli.ts report path/to/spec.json --format html --output report.html
|
||||
```
|
||||
|
||||
#### Batch validation
|
||||
```bash
|
||||
bun run src/cli.ts batch spec1.json spec2.json spec3.json
|
||||
```
|
||||
|
||||
#### Show supported versions
|
||||
```bash
|
||||
bun run src/cli.ts info
|
||||
```
|
||||
|
||||
### CLI Options
|
||||
|
||||
#### Validate Command
|
||||
- `-s, --strict`: Enable strict validation mode
|
||||
- `-e, --examples`: Validate examples in the specification
|
||||
- `-r, --references`: Validate all references
|
||||
- `-v, --verbose`: Show detailed output
|
||||
|
||||
#### Report Command
|
||||
- `-f, --format <format>`: Report format (json, yaml, html, markdown)
|
||||
- `-o, --output <file>`: Output file path
|
||||
- `-w, --warnings`: Include warnings in report
|
||||
- `-m, --metadata`: Include metadata in report
|
||||
|
||||
### Programmatic Usage
|
||||
|
||||
#### Functional Approach (Recommended)
|
||||
|
||||
```typescript
|
||||
import { validate, parse, generateValidationReport } from './src/varsity.js';
|
||||
|
||||
// Parse and validate
|
||||
const result = await validate('path/to/spec.json');
|
||||
|
||||
// Generate a report
|
||||
const report = await generateValidationReport('path/to/spec.json', {
|
||||
format: 'json',
|
||||
includeWarnings: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
|
||||
// Parse without validation
|
||||
const parsed = await parse('path/to/spec.json');
|
||||
```
|
||||
|
||||
#### Factory Pattern (For Configuration)
|
||||
|
||||
```typescript
|
||||
import { createVarsity } from './src/varsity.js';
|
||||
|
||||
const varsity = createVarsity({
|
||||
defaultVersion: '3.0.3',
|
||||
strictMode: false,
|
||||
customSchemas: {},
|
||||
reportFormats: ['json']
|
||||
});
|
||||
|
||||
// Use the configured instance
|
||||
const result = await varsity.validate('path/to/spec.json');
|
||||
const report = await varsity.generateReport('path/to/spec.json', {
|
||||
format: 'json',
|
||||
includeWarnings: true,
|
||||
includeMetadata: true
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core Functions
|
||||
|
||||
#### Direct Functions
|
||||
- `validate(source: string, options?: ValidationOptions, config?: VarsityConfig): Promise<ValidationResult>`
|
||||
- `parse(source: string): Promise<ParsedSpec>`
|
||||
- `generateValidationReport(source: string, reportOptions: ReportOptions, validationOptions?: ValidationOptions, config?: VarsityConfig): Promise<string>`
|
||||
- `saveValidationReport(source: string, reportOptions: ReportOptions, validationOptions?: ValidationOptions, config?: VarsityConfig): Promise<void>`
|
||||
- `validateMultiple(sources: string[], options?: ValidationOptions, config?: VarsityConfig): Promise<ValidationResult[]>`
|
||||
- `getSupportedVersions(): string[]`
|
||||
|
||||
#### Factory Function
|
||||
- `createVarsity(config?: VarsityConfig)`: Creates a configured instance with methods
|
||||
|
||||
#### Individual Module Functions
|
||||
- `parseOpenAPISpec(source: string): Promise<ParsedSpec>`
|
||||
- `validateBasicStructure(spec: any, version: OpenAPIVersion): boolean`
|
||||
- `validateOpenAPISpec(spec: any, version: OpenAPIVersion, options?: ValidationOptions): ValidationResult`
|
||||
- `generateReport(result: ValidationResult, options: ReportOptions): string`
|
||||
- `saveReport(content: string, outputPath: string): void`
|
||||
|
||||
### Types
|
||||
|
||||
- `ValidationResult`: Contains validation results with errors and warnings
|
||||
- `ParsedSpec`: Parsed specification with metadata
|
||||
- `ValidationOptions`: Configuration for validation behavior
|
||||
- `ReportOptions`: Configuration for report generation
|
||||
- `VarsityConfig`: Global configuration for the library
|
||||
- `OpenAPISpec`: Union type for all OpenAPI specification versions
|
||||
- `OpenAPIVersion`: Supported OpenAPI version strings
|
||||
|
||||
### Type Safety
|
||||
|
||||
Varsity leverages the comprehensive `oas-types` package for full TypeScript support:
|
||||
|
||||
```typescript
|
||||
import type { OpenAPI2, OpenAPI3, OpenAPI3_1 } from 'oas-types';
|
||||
|
||||
// All parsed specifications are properly typed
|
||||
const result = await validate('spec.json');
|
||||
// result.spec is typed as OpenAPISpec (OpenAPI2 | OpenAPI3 | OpenAPI3_1)
|
||||
|
||||
// Type guards for version-specific handling
|
||||
if (result.version === '2.0') {
|
||||
const swaggerSpec = result.spec as OpenAPI2;
|
||||
// swaggerSpec.swagger, swaggerSpec.info, etc. are fully typed
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
bun test
|
||||
```
|
||||
|
||||
### Building
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
### Linting
|
||||
```bash
|
||||
bun run lint
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
52
bun.lock
Normal file
52
bun.lock
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "varsity",
|
||||
"dependencies": {
|
||||
"@types/node": "^24.6.1",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"commander": "^14.0.1",
|
||||
"oas-types": "^1.0.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="],
|
||||
|
||||
"@types/node": ["@types/node@24.6.1", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.17", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA=="],
|
||||
|
||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
|
||||
|
||||
"commander": ["commander@14.0.1", "", {}, "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"oas-types": ["oas-types@1.0.6", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-86K+t8TGsBBetPaN4+9LY4pifAC/aguyylihhkbHa40nK8J5chpHYWhfmmFVT2QQS6aw+pCdjFM8fAaY352mZA=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
|
||||
}
|
||||
}
|
||||
31
index.ts
Normal file
31
index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// Main functional exports
|
||||
export {
|
||||
validate,
|
||||
parse,
|
||||
generateValidationReport,
|
||||
saveValidationReport,
|
||||
validateMultiple,
|
||||
getSupportedVersions,
|
||||
createVarsity,
|
||||
// Individual module exports
|
||||
parseOpenAPISpec,
|
||||
validateBasicStructure,
|
||||
validateOpenAPISpec,
|
||||
generateReport,
|
||||
saveReport,
|
||||
} from "./src/varsity.js";
|
||||
|
||||
// Type exports
|
||||
export type {
|
||||
ParsedSpec,
|
||||
ValidationResult,
|
||||
ValidationError,
|
||||
ValidationOptions,
|
||||
ReportOptions,
|
||||
VarsityConfig,
|
||||
OpenAPIVersion,
|
||||
CLIResult,
|
||||
} from "./src/types.js";
|
||||
|
||||
// Default export - functional instance
|
||||
export { default } from "./src/varsity.js";
|
||||
41
package.json
Normal file
41
package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "varsity",
|
||||
"version": "1.0.0",
|
||||
"description": "Comprehensive OpenAPI parsing and validation library",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"bin": {
|
||||
"varsity": "./src/cli.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "bun run src/cli.ts",
|
||||
"dev": "bun run --watch src/cli.ts",
|
||||
"test": "bun test",
|
||||
"build": "bun build index.ts --outdir dist --target bun",
|
||||
"lint": "bun run --bun tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"openapi",
|
||||
"swagger",
|
||||
"validation",
|
||||
"parser",
|
||||
"api",
|
||||
"specification"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^24.6.1",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"commander": "^14.0.1",
|
||||
"oas-types": "^1.0.6"
|
||||
}
|
||||
}
|
||||
324
src/cli.ts
Normal file
324
src/cli.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env bun
|
||||
import { Command } from "commander";
|
||||
import {
|
||||
validate,
|
||||
parse,
|
||||
generateValidationReport,
|
||||
saveValidationReport,
|
||||
validateMultiple,
|
||||
validateWithReferences,
|
||||
validateMultipleWithReferences,
|
||||
analyzeDocumentReferences,
|
||||
getSupportedVersions,
|
||||
createVarsity,
|
||||
} from "./varsity.js";
|
||||
import type { ValidationOptions, ReportOptions } from "./types.js";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program
|
||||
.name("varsity")
|
||||
.description("Comprehensive OpenAPI parsing and validation library")
|
||||
.version("1.0.0");
|
||||
|
||||
// Validate command
|
||||
program
|
||||
.command("validate")
|
||||
.description("Validate an OpenAPI specification")
|
||||
.argument("<source>", "Path or URL to OpenAPI specification")
|
||||
.option("-s, --strict", "Enable strict validation mode")
|
||||
.option("-e, --examples", "Validate examples in the specification")
|
||||
.option("-r, --references", "Validate all references")
|
||||
.option("--recursive", "Recursively validate all $ref references")
|
||||
.option(
|
||||
"--max-depth <depth>",
|
||||
"Maximum reference depth for recursive validation",
|
||||
"10"
|
||||
)
|
||||
.option("-v, --verbose", "Show detailed output")
|
||||
.action(async (source: string, options: any) => {
|
||||
try {
|
||||
const validationOptions: ValidationOptions = {
|
||||
strict: options.strict,
|
||||
validateExamples: options.examples,
|
||||
validateReferences: options.references,
|
||||
recursive: options.recursive,
|
||||
maxRefDepth: parseInt(options.maxDepth) || 10,
|
||||
};
|
||||
|
||||
let result;
|
||||
if (options.recursive) {
|
||||
result = await validateWithReferences(source, validationOptions);
|
||||
|
||||
if (result.valid) {
|
||||
console.log("✅ Specification and all references are valid");
|
||||
if (options.verbose) {
|
||||
console.log(`Version: ${result.version}`);
|
||||
console.log(`Total documents: ${result.totalDocuments}`);
|
||||
console.log(`Valid documents: ${result.validDocuments}`);
|
||||
console.log(
|
||||
`Circular references: ${result.circularReferences.length}`
|
||||
);
|
||||
console.log(`Warnings: ${result.warnings.length}`);
|
||||
}
|
||||
} else {
|
||||
console.log("❌ Specification or references are invalid");
|
||||
console.log(`Errors: ${result.errors.length}`);
|
||||
console.log(`Total documents: ${result.totalDocuments}`);
|
||||
console.log(`Valid documents: ${result.validDocuments}`);
|
||||
|
||||
if (result.circularReferences.length > 0) {
|
||||
console.log(
|
||||
`Circular references: ${result.circularReferences.length}`
|
||||
);
|
||||
for (const circular of result.circularReferences) {
|
||||
console.log(` • ${circular}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const error of result.errors) {
|
||||
console.log(` • ${error.path}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (options.verbose && result.warnings.length > 0) {
|
||||
console.log(`Warnings: ${result.warnings.length}`);
|
||||
for (const warning of result.warnings) {
|
||||
console.log(` • ${warning.path}: ${warning.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
result = await validate(source, validationOptions);
|
||||
|
||||
if (result.valid) {
|
||||
console.log("✅ Specification is valid");
|
||||
if (options.verbose) {
|
||||
console.log(`Version: ${result.version}`);
|
||||
console.log(`Warnings: ${result.warnings.length}`);
|
||||
}
|
||||
} else {
|
||||
console.log("❌ Specification is invalid");
|
||||
console.log(`Errors: ${result.errors.length}`);
|
||||
|
||||
for (const error of result.errors) {
|
||||
console.log(` • ${error.path}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (options.verbose && result.warnings.length > 0) {
|
||||
console.log(`Warnings: ${result.warnings.length}`);
|
||||
for (const warning of result.warnings) {
|
||||
console.log(` • ${warning.path}: ${warning.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Validation failed:",
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Parse command
|
||||
program
|
||||
.command("parse")
|
||||
.description("Parse an OpenAPI specification without validation")
|
||||
.argument("<source>", "Path or URL to OpenAPI specification")
|
||||
.option("-j, --json", "Output as JSON")
|
||||
.action(async (source: string, options: any) => {
|
||||
try {
|
||||
const parsed = await parse(source);
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(parsed, null, 2));
|
||||
} else {
|
||||
console.log("📄 Parsed OpenAPI Specification");
|
||||
console.log(`Version: ${parsed.version}`);
|
||||
console.log(`Source: ${parsed.source}`);
|
||||
console.log(`Title: ${parsed.metadata.title || "N/A"}`);
|
||||
console.log(`Version: ${parsed.metadata.version || "N/A"}`);
|
||||
if (parsed.metadata.description) {
|
||||
console.log(`Description: ${parsed.metadata.description}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Parsing failed:",
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Report command
|
||||
program
|
||||
.command("report")
|
||||
.description("Generate a validation report")
|
||||
.argument("<source>", "Path or URL to OpenAPI specification")
|
||||
.option(
|
||||
"-f, --format <format>",
|
||||
"Report format (json, yaml, html, markdown)",
|
||||
"json"
|
||||
)
|
||||
.option("-o, --output <file>", "Output file path")
|
||||
.option("-s, --strict", "Enable strict validation mode")
|
||||
.option("-e, --examples", "Validate examples in the specification")
|
||||
.option("-r, --references", "Validate all references")
|
||||
.option("-w, --warnings", "Include warnings in report")
|
||||
.option("-m, --metadata", "Include metadata in report")
|
||||
.action(async (source: string, options: any) => {
|
||||
try {
|
||||
const validationOptions: ValidationOptions = {
|
||||
strict: options.strict,
|
||||
validateExamples: options.examples,
|
||||
validateReferences: options.references,
|
||||
};
|
||||
|
||||
const reportOptions: ReportOptions = {
|
||||
format: options.format,
|
||||
output: options.output,
|
||||
includeWarnings: options.warnings,
|
||||
includeMetadata: options.metadata,
|
||||
};
|
||||
|
||||
if (options.output) {
|
||||
await saveValidationReport(source, reportOptions, validationOptions);
|
||||
console.log(`📊 Report saved to: ${options.output}`);
|
||||
} else {
|
||||
const report = await generateValidationReport(
|
||||
source,
|
||||
reportOptions,
|
||||
validationOptions
|
||||
);
|
||||
console.log(report);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Report generation failed:",
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Batch command
|
||||
program
|
||||
.command("batch")
|
||||
.description("Validate multiple OpenAPI specifications")
|
||||
.argument("<sources...>", "Paths or URLs to OpenAPI specifications")
|
||||
.option("-s, --strict", "Enable strict validation mode")
|
||||
.option("-e, --examples", "Validate examples in the specification")
|
||||
.option("-r, --references", "Validate all references")
|
||||
.option("-j, --json", "Output as JSON")
|
||||
.action(async (sources: string[], options: any) => {
|
||||
try {
|
||||
const validationOptions: ValidationOptions = {
|
||||
strict: options.strict,
|
||||
validateExamples: options.examples,
|
||||
validateReferences: options.references,
|
||||
};
|
||||
|
||||
const results = await validateMultiple(sources, validationOptions);
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
} else {
|
||||
console.log("📋 Batch Validation Results");
|
||||
console.log("=".repeat(50));
|
||||
|
||||
let validCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
const source = sources[i];
|
||||
const result = results[i];
|
||||
|
||||
console.log(`\n${i + 1}. ${source}`);
|
||||
if (result && result.valid) {
|
||||
console.log(" ✅ Valid");
|
||||
validCount++;
|
||||
} else {
|
||||
console.log(" ❌ Invalid");
|
||||
console.log(` Errors: ${result?.errors.length || 0}`);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n" + "=".repeat(50));
|
||||
console.log(`Summary: ${validCount} valid, ${errorCount} invalid`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Batch validation failed:",
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Analyze command
|
||||
program
|
||||
.command("analyze")
|
||||
.description("Analyze references in an OpenAPI specification")
|
||||
.argument("<source>", "Path or URL to OpenAPI specification")
|
||||
.option("-j, --json", "Output as JSON")
|
||||
.action(async (source: string, options: any) => {
|
||||
try {
|
||||
const analysis = await analyzeDocumentReferences(source);
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(analysis, null, 2));
|
||||
} else {
|
||||
console.log("🔍 Reference Analysis");
|
||||
console.log("=".repeat(40));
|
||||
console.log(`Total references: ${analysis.totalReferences}`);
|
||||
console.log(
|
||||
`Circular references: ${analysis.circularReferences.length}`
|
||||
);
|
||||
|
||||
if (analysis.circularReferences.length > 0) {
|
||||
console.log("\nCircular references found:");
|
||||
for (const circular of analysis.circularReferences) {
|
||||
console.log(` • ${circular}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (analysis.references.length > 0) {
|
||||
console.log("\nAll references:");
|
||||
for (const ref of analysis.references) {
|
||||
console.log(` • ${ref.path}: ${ref.value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Analysis failed:",
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Info command
|
||||
program
|
||||
.command("info")
|
||||
.description("Show information about supported OpenAPI versions")
|
||||
.action(() => {
|
||||
const versions = getSupportedVersions();
|
||||
|
||||
console.log("🔍 Supported OpenAPI Versions");
|
||||
console.log("=".repeat(40));
|
||||
versions.forEach((version) => {
|
||||
console.log(` • ${version}`);
|
||||
});
|
||||
console.log("\nFor more information, visit: https://spec.openapis.org/");
|
||||
});
|
||||
|
||||
// Parse command line arguments
|
||||
program.parse();
|
||||
120
src/parser.ts
Normal file
120
src/parser.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import type { ParsedSpec, OpenAPIVersion, OpenAPISpec } from "./types.js";
|
||||
import type { OpenAPI2, OpenAPI3, OpenAPI3_1 } from "oas-types";
|
||||
|
||||
/**
|
||||
* Detect OpenAPI version from specification
|
||||
*/
|
||||
const detectVersion = (spec: any): OpenAPIVersion => {
|
||||
// Check for OpenAPI 3.x
|
||||
if (spec.openapi) {
|
||||
const version = spec.openapi;
|
||||
if (version.startsWith("3.0")) {
|
||||
return version as OpenAPIVersion;
|
||||
} else if (version.startsWith("3.1")) {
|
||||
return version as OpenAPIVersion;
|
||||
} else if (version.startsWith("3.2")) {
|
||||
return version as OpenAPIVersion;
|
||||
}
|
||||
throw new Error(`Unsupported OpenAPI version: ${version}`);
|
||||
}
|
||||
|
||||
// Check for Swagger 2.0
|
||||
if (spec.swagger === "2.0") {
|
||||
return "2.0";
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Unable to detect OpenAPI version. Specification must have "openapi" or "swagger" field.'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract metadata from specification
|
||||
*/
|
||||
const extractMetadata = (
|
||||
spec: OpenAPISpec,
|
||||
version: OpenAPIVersion
|
||||
): ParsedSpec["metadata"] => {
|
||||
// All OpenAPI versions have the same info structure
|
||||
const info = spec.info;
|
||||
return {
|
||||
title: info?.title,
|
||||
version: info?.version,
|
||||
description: info?.description,
|
||||
contact: info?.contact,
|
||||
license: info?.license,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse an OpenAPI specification from a file path or URL
|
||||
*/
|
||||
export const parseOpenAPISpec = async (source: string): Promise<ParsedSpec> => {
|
||||
let content: string;
|
||||
let spec: any;
|
||||
|
||||
try {
|
||||
// Handle file paths
|
||||
if (source.startsWith("http://") || source.startsWith("https://")) {
|
||||
const response = await fetch(source);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch specification: ${response.statusText}`
|
||||
);
|
||||
}
|
||||
content = await response.text();
|
||||
} else {
|
||||
// Local file
|
||||
const filePath = resolve(source);
|
||||
content = readFileSync(filePath, "utf-8");
|
||||
}
|
||||
|
||||
// Parse JSON or YAML
|
||||
if (content.trim().startsWith("{") || content.trim().startsWith("[")) {
|
||||
spec = JSON.parse(content);
|
||||
} else {
|
||||
// For YAML parsing, we'll use a simple approach or add yaml dependency later
|
||||
throw new Error(
|
||||
"YAML parsing not yet implemented. Please use JSON format."
|
||||
);
|
||||
}
|
||||
|
||||
const version = detectVersion(spec);
|
||||
|
||||
// Type the spec based on the detected version
|
||||
const typedSpec = spec as OpenAPISpec;
|
||||
|
||||
return {
|
||||
spec: typedSpec,
|
||||
version,
|
||||
source,
|
||||
metadata: extractMetadata(typedSpec, version),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to parse OpenAPI specification: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that the parsed spec has required fields
|
||||
*/
|
||||
export const validateBasicStructure = (
|
||||
spec: OpenAPISpec,
|
||||
version: OpenAPIVersion
|
||||
): boolean => {
|
||||
if (version === "2.0") {
|
||||
const swaggerSpec = spec as OpenAPI2.Specification;
|
||||
return !!(swaggerSpec.swagger && swaggerSpec.info && swaggerSpec.paths);
|
||||
} else {
|
||||
const openapiSpec = spec as
|
||||
| OpenAPI3.Specification
|
||||
| OpenAPI3_1.Specification;
|
||||
return !!(openapiSpec.openapi && openapiSpec.info && openapiSpec.paths);
|
||||
}
|
||||
};
|
||||
228
src/partial-validator.ts
Normal file
228
src/partial-validator.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import Ajv from "ajv";
|
||||
import addFormats from "ajv-formats";
|
||||
import { allSchemas } from "oas-types/schemas";
|
||||
import type {
|
||||
OpenAPIVersion,
|
||||
ValidationError,
|
||||
ValidationResult,
|
||||
} from "./types.js";
|
||||
|
||||
// Initialize AJV instance for partial validation
|
||||
const createPartialAjvInstance = (): Ajv => {
|
||||
const ajv = new Ajv({
|
||||
allErrors: true,
|
||||
verbose: true,
|
||||
strict: false,
|
||||
validateFormats: true,
|
||||
});
|
||||
addFormats(ajv);
|
||||
return ajv;
|
||||
};
|
||||
|
||||
const partialAjv = createPartialAjvInstance();
|
||||
|
||||
// Schema map for partial documents
|
||||
const partialSchemas = new Map<string, any>();
|
||||
|
||||
// Initialize partial schemas
|
||||
partialSchemas.set("2.0.schema", allSchemas["2.0"].schema);
|
||||
partialSchemas.set("2.0.parameter", allSchemas["2.0"].parameter);
|
||||
partialSchemas.set("2.0.response", allSchemas["2.0"].response);
|
||||
partialSchemas.set("2.0.pathitem", allSchemas["2.0"].pathitem);
|
||||
|
||||
partialSchemas.set("3.0.schema", allSchemas["3.0"].schema);
|
||||
partialSchemas.set("3.0.parameter", allSchemas["3.0"].parameter);
|
||||
partialSchemas.set("3.0.response", allSchemas["3.0"].response);
|
||||
partialSchemas.set("3.0.pathitem", allSchemas["3.0"].pathitem);
|
||||
partialSchemas.set("3.0.requestbody", allSchemas["3.0"].requestbody);
|
||||
partialSchemas.set("3.0.header", allSchemas["3.0"].header);
|
||||
partialSchemas.set("3.0.example", allSchemas["3.0"].example);
|
||||
partialSchemas.set("3.0.link", allSchemas["3.0"].link);
|
||||
partialSchemas.set("3.0.callback", allSchemas["3.0"].callback);
|
||||
partialSchemas.set("3.0.securityscheme", allSchemas["3.0"].securityscheme);
|
||||
|
||||
partialSchemas.set("3.1.schema", allSchemas["3.1"].schema);
|
||||
partialSchemas.set("3.1.parameter", allSchemas["3.1"].parameter);
|
||||
partialSchemas.set("3.1.response", allSchemas["3.1"].response);
|
||||
partialSchemas.set("3.1.pathitem", allSchemas["3.1"].pathitem);
|
||||
partialSchemas.set("3.1.requestbody", allSchemas["3.1"].requestbody);
|
||||
partialSchemas.set("3.1.header", allSchemas["3.1"].header);
|
||||
partialSchemas.set("3.1.example", allSchemas["3.1"].example);
|
||||
partialSchemas.set("3.1.link", allSchemas["3.1"].link);
|
||||
partialSchemas.set("3.1.callback", allSchemas["3.1"].callback);
|
||||
partialSchemas.set("3.1.securityscheme", allSchemas["3.1"].securityscheme);
|
||||
|
||||
partialSchemas.set("3.2.schema", allSchemas["3.2"].schema);
|
||||
partialSchemas.set("3.2.parameter", allSchemas["3.2"].parameter);
|
||||
partialSchemas.set("3.2.response", allSchemas["3.2"].response);
|
||||
partialSchemas.set("3.2.pathitem", allSchemas["3.2"].pathitem);
|
||||
partialSchemas.set("3.2.requestbody", allSchemas["3.2"].requestbody);
|
||||
partialSchemas.set("3.2.header", allSchemas["3.2"].header);
|
||||
partialSchemas.set("3.2.example", allSchemas["3.2"].example);
|
||||
partialSchemas.set("3.2.link", allSchemas["3.2"].link);
|
||||
partialSchemas.set("3.2.callback", allSchemas["3.2"].callback);
|
||||
partialSchemas.set("3.2.securityscheme", allSchemas["3.2"].securityscheme);
|
||||
|
||||
/**
|
||||
* Detect the type of partial document based on its structure
|
||||
*/
|
||||
const detectPartialType = (
|
||||
doc: any,
|
||||
version: OpenAPIVersion
|
||||
): string | null => {
|
||||
// Check for schema-like structure
|
||||
if (
|
||||
doc.type ||
|
||||
doc.properties ||
|
||||
doc.items ||
|
||||
doc.allOf ||
|
||||
doc.oneOf ||
|
||||
doc.anyOf
|
||||
) {
|
||||
return "schema";
|
||||
}
|
||||
|
||||
// Check for parameter-like structure
|
||||
if (doc.name && (doc.in || doc.parameter)) {
|
||||
return "parameter";
|
||||
}
|
||||
|
||||
// Check for response-like structure
|
||||
if (doc.description && (doc.content || doc.schema || doc.headers)) {
|
||||
return "response";
|
||||
}
|
||||
|
||||
// Check for path item-like structure
|
||||
if (
|
||||
doc.get ||
|
||||
doc.post ||
|
||||
doc.put ||
|
||||
doc.delete ||
|
||||
doc.patch ||
|
||||
doc.head ||
|
||||
doc.options
|
||||
) {
|
||||
return "pathitem";
|
||||
}
|
||||
|
||||
// Check for request body-like structure
|
||||
if (doc.content && !doc.description) {
|
||||
return "requestbody";
|
||||
}
|
||||
|
||||
// Check for header-like structure
|
||||
if (doc.schema && !doc.name) {
|
||||
return "header";
|
||||
}
|
||||
|
||||
// Check for example-like structure
|
||||
if (doc.summary || doc.description || doc.value !== undefined) {
|
||||
return "example";
|
||||
}
|
||||
|
||||
// Check for link-like structure
|
||||
if (doc.operationRef || doc.operationId) {
|
||||
return "link";
|
||||
}
|
||||
|
||||
// Check for callback-like structure
|
||||
if (doc.expression && typeof doc.expression === "string") {
|
||||
return "callback";
|
||||
}
|
||||
|
||||
// Check for security scheme-like structure
|
||||
if (doc.type && (doc.flows || doc.openIdConnectUrl || doc.scheme)) {
|
||||
return "securityscheme";
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate a partial OpenAPI document
|
||||
*/
|
||||
export const validatePartialDocument = (
|
||||
document: any,
|
||||
version: OpenAPIVersion,
|
||||
documentPath?: string
|
||||
): ValidationResult => {
|
||||
const errors: ValidationError[] = [];
|
||||
const warnings: ValidationError[] = [];
|
||||
|
||||
// Detect the type of partial document
|
||||
const partialType = detectPartialType(document, version);
|
||||
|
||||
if (!partialType) {
|
||||
errors.push({
|
||||
path: "/",
|
||||
message:
|
||||
"Unable to determine document type. This doesn't appear to be a valid OpenAPI partial document.",
|
||||
});
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
errors,
|
||||
warnings,
|
||||
spec: document,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the appropriate schema for this partial document type
|
||||
const schemaKey = `${version}.${partialType}`;
|
||||
const schema = partialSchemas.get(schemaKey);
|
||||
|
||||
if (!schema) {
|
||||
errors.push({
|
||||
path: "/",
|
||||
message: `No validation schema available for ${partialType} in OpenAPI ${version}`,
|
||||
});
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
errors,
|
||||
warnings,
|
||||
spec: document,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate against the schema
|
||||
const validate = partialAjv.compile(schema);
|
||||
const valid = validate(document);
|
||||
|
||||
if (!valid && validate.errors) {
|
||||
for (const error of validate.errors) {
|
||||
const validationError: ValidationError = {
|
||||
path: error.instancePath || error.schemaPath || "/",
|
||||
message: error.message || "Validation error",
|
||||
data: error.data,
|
||||
schemaPath: error.schemaPath,
|
||||
};
|
||||
|
||||
if (error.keyword === "required" || error.keyword === "type") {
|
||||
errors.push(validationError);
|
||||
} else {
|
||||
warnings.push(validationError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add document path information to errors if available
|
||||
if (documentPath) {
|
||||
errors.forEach((error) => {
|
||||
error.path = `${documentPath}${error.path}`;
|
||||
});
|
||||
warnings.forEach((warning) => {
|
||||
warning.path = `${documentPath}${warning.path}`;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
spec: document,
|
||||
version,
|
||||
};
|
||||
};
|
||||
204
src/recursive-validator.ts
Normal file
204
src/recursive-validator.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { parseOpenAPISpec } from "./parser.js";
|
||||
import { validateOpenAPISpec } from "./validator.js";
|
||||
import { validatePartialDocument } from "./partial-validator.js";
|
||||
import { resolveAllReferences, findReferences } from "./ref-resolver.js";
|
||||
import type {
|
||||
ValidationResult,
|
||||
ValidationError,
|
||||
ValidationOptions,
|
||||
OpenAPIVersion,
|
||||
OpenAPISpec,
|
||||
} from "./types.js";
|
||||
|
||||
export interface RecursiveValidationResult extends ValidationResult {
|
||||
partialValidations: Array<{
|
||||
path: string;
|
||||
result: ValidationResult;
|
||||
isCircular: boolean;
|
||||
}>;
|
||||
circularReferences: string[];
|
||||
totalDocuments: number;
|
||||
validDocuments: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively validate an OpenAPI specification and all its references
|
||||
*/
|
||||
export const validateRecursively = async (
|
||||
source: string,
|
||||
options: ValidationOptions = {}
|
||||
): Promise<RecursiveValidationResult> => {
|
||||
// Parse the root document
|
||||
const rootParsed = await parseOpenAPISpec(source);
|
||||
|
||||
// Validate the root document
|
||||
const rootValidation = validateOpenAPISpec(
|
||||
rootParsed.spec,
|
||||
rootParsed.version,
|
||||
options
|
||||
);
|
||||
|
||||
// Resolve all references
|
||||
const { resolvedRefs, circularRefs } = await resolveAllReferences(
|
||||
rootParsed.spec,
|
||||
source,
|
||||
options.maxRefDepth || 10
|
||||
);
|
||||
|
||||
// Validate each resolved reference
|
||||
const partialValidations: Array<{
|
||||
path: string;
|
||||
result: ValidationResult;
|
||||
isCircular: boolean;
|
||||
}> = [];
|
||||
|
||||
let validDocuments = rootValidation.valid ? 1 : 0;
|
||||
|
||||
for (const ref of resolvedRefs) {
|
||||
if (ref.isCircular) {
|
||||
partialValidations.push({
|
||||
path: ref.path,
|
||||
result: {
|
||||
valid: false,
|
||||
errors: [
|
||||
{
|
||||
path: "/",
|
||||
message: "Circular reference detected",
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
spec: null,
|
||||
version: ref.version || "3.0",
|
||||
},
|
||||
isCircular: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ref.content === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine the version for this partial document
|
||||
const version = ref.version || rootParsed.version;
|
||||
|
||||
// Validate the partial document
|
||||
const partialResult = validatePartialDocument(
|
||||
ref.content,
|
||||
version,
|
||||
ref.path
|
||||
);
|
||||
|
||||
partialValidations.push({
|
||||
path: ref.path,
|
||||
result: partialResult,
|
||||
isCircular: false,
|
||||
});
|
||||
|
||||
if (partialResult.valid) {
|
||||
validDocuments++;
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all errors and warnings
|
||||
const allErrors: ValidationError[] = [...rootValidation.errors];
|
||||
const allWarnings: ValidationError[] = [...rootValidation.warnings];
|
||||
|
||||
for (const partial of partialValidations) {
|
||||
allErrors.push(...partial.result.errors);
|
||||
allWarnings.push(...partial.result.warnings);
|
||||
}
|
||||
|
||||
return {
|
||||
valid:
|
||||
rootValidation.valid && partialValidations.every((p) => p.result.valid),
|
||||
errors: allErrors,
|
||||
warnings: allWarnings,
|
||||
spec: rootParsed.spec,
|
||||
version: rootParsed.version,
|
||||
partialValidations,
|
||||
circularReferences: circularRefs,
|
||||
totalDocuments: 1 + partialValidations.length,
|
||||
validDocuments,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate multiple OpenAPI specifications recursively
|
||||
*/
|
||||
export const validateMultipleRecursively = async (
|
||||
sources: string[],
|
||||
options: ValidationOptions = {}
|
||||
): Promise<RecursiveValidationResult[]> => {
|
||||
const results: RecursiveValidationResult[] = [];
|
||||
|
||||
for (const source of sources) {
|
||||
try {
|
||||
const result = await validateRecursively(source, options);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
// Create error result for failed parsing
|
||||
const errorResult: RecursiveValidationResult = {
|
||||
valid: false,
|
||||
errors: [
|
||||
{
|
||||
path: "/",
|
||||
message: `Failed to parse specification: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
spec: null,
|
||||
version: "3.0",
|
||||
partialValidations: [],
|
||||
circularReferences: [],
|
||||
totalDocuments: 0,
|
||||
validDocuments: 0,
|
||||
};
|
||||
results.push(errorResult);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find all references in a document without resolving them
|
||||
*/
|
||||
export const analyzeReferences = async (
|
||||
source: string
|
||||
): Promise<{
|
||||
references: Array<{ path: string; value: string }>;
|
||||
circularReferences: string[];
|
||||
totalReferences: number;
|
||||
}> => {
|
||||
const parsed = await parseOpenAPISpec(source);
|
||||
const references = findReferences(parsed.spec);
|
||||
|
||||
// Check for circular references by analyzing reference paths
|
||||
const circularReferences: string[] = [];
|
||||
const referenceMap = new Map<string, string[]>();
|
||||
|
||||
for (const ref of references) {
|
||||
const refValue = ref.value;
|
||||
if (!referenceMap.has(refValue)) {
|
||||
referenceMap.set(refValue, []);
|
||||
}
|
||||
referenceMap.get(refValue)!.push(ref.path);
|
||||
}
|
||||
|
||||
// Simple circular reference detection based on reference patterns
|
||||
for (const [refValue, paths] of referenceMap) {
|
||||
if (paths.length > 1) {
|
||||
// This is a potential circular reference
|
||||
circularReferences.push(refValue);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
references,
|
||||
circularReferences,
|
||||
totalReferences: references.length,
|
||||
};
|
||||
};
|
||||
211
src/ref-resolver.ts
Normal file
211
src/ref-resolver.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { resolve, dirname, join } from "path";
|
||||
import { URL } from "url";
|
||||
import type { OpenAPIVersion, OpenAPISpec } from "./types.js";
|
||||
|
||||
export interface ResolvedReference {
|
||||
path: string;
|
||||
content: any;
|
||||
version?: OpenAPIVersion;
|
||||
isCircular: boolean;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export interface ReferenceContext {
|
||||
basePath: string;
|
||||
visited: Set<string>;
|
||||
maxDepth: number;
|
||||
currentDepth: number;
|
||||
baseDocument: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect OpenAPI version from a document
|
||||
*/
|
||||
const detectDocumentVersion = (doc: any): OpenAPIVersion | null => {
|
||||
if (doc.openapi) {
|
||||
const version = doc.openapi;
|
||||
if (version.startsWith("3.0")) return "3.0";
|
||||
if (version.startsWith("3.1")) return "3.1";
|
||||
if (version.startsWith("3.2")) return "3.2";
|
||||
}
|
||||
if (doc.swagger === "2.0") return "2.0";
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a JSON or YAML file
|
||||
*/
|
||||
const parseFile = (filePath: string): any => {
|
||||
const content = readFileSync(filePath, "utf-8");
|
||||
|
||||
if (content.trim().startsWith("{") || content.trim().startsWith("[")) {
|
||||
return JSON.parse(content);
|
||||
} else {
|
||||
// For now, throw error for YAML - can be enhanced later
|
||||
throw new Error(`YAML parsing not implemented for file: ${filePath}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a $ref to its content
|
||||
*/
|
||||
export const resolveReference = async (
|
||||
ref: string,
|
||||
context: ReferenceContext
|
||||
): Promise<ResolvedReference> => {
|
||||
const { basePath, visited, maxDepth, currentDepth } = context;
|
||||
|
||||
// Check for circular reference
|
||||
if (visited.has(ref)) {
|
||||
return {
|
||||
path: ref,
|
||||
content: null,
|
||||
isCircular: true,
|
||||
depth: currentDepth,
|
||||
};
|
||||
}
|
||||
|
||||
// Check depth limit
|
||||
if (currentDepth >= maxDepth) {
|
||||
throw new Error(`Maximum reference depth (${maxDepth}) exceeded`);
|
||||
}
|
||||
|
||||
// Add to visited set
|
||||
visited.add(ref);
|
||||
|
||||
try {
|
||||
let resolvedPath: string;
|
||||
let content: any;
|
||||
|
||||
if (ref.startsWith("http://") || ref.startsWith("https://")) {
|
||||
// External URL reference
|
||||
const response = await fetch(ref);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch external reference: ${response.statusText}`
|
||||
);
|
||||
}
|
||||
content = await response.json();
|
||||
resolvedPath = ref;
|
||||
} else if (ref.startsWith("#/")) {
|
||||
// Internal reference - resolve within the same document
|
||||
const pathSegments = ref.substring(2).split("/");
|
||||
let current = context.baseDocument;
|
||||
|
||||
for (const segment of pathSegments) {
|
||||
if (current && typeof current === "object" && segment in current) {
|
||||
current = (current as any)[segment];
|
||||
} else {
|
||||
throw new Error(`Reference not found: ${ref}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
path: ref,
|
||||
content: current,
|
||||
isCircular: false,
|
||||
depth: currentDepth,
|
||||
};
|
||||
} else {
|
||||
// Local file reference
|
||||
const baseDir = dirname(basePath);
|
||||
resolvedPath = resolve(baseDir, ref);
|
||||
content = parseFile(resolvedPath);
|
||||
}
|
||||
|
||||
// Detect version of the resolved document
|
||||
const version = detectDocumentVersion(content);
|
||||
|
||||
return {
|
||||
path: ref,
|
||||
content,
|
||||
version: version || undefined,
|
||||
isCircular: false,
|
||||
depth: currentDepth,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to resolve reference '${ref}': ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
} finally {
|
||||
// Remove from visited set when done
|
||||
visited.delete(ref);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Find all $ref references in a document
|
||||
*/
|
||||
export const findReferences = (
|
||||
obj: any,
|
||||
path = ""
|
||||
): Array<{ path: string; value: string }> => {
|
||||
const refs: Array<{ path: string; value: string }> = [];
|
||||
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const currentPath = path ? `${path}.${key}` : key;
|
||||
|
||||
if (key === "$ref" && typeof value === "string") {
|
||||
refs.push({ path: currentPath, value });
|
||||
} else if (typeof value === "object") {
|
||||
refs.push(...findReferences(value, currentPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively resolve all references in a document
|
||||
*/
|
||||
export const resolveAllReferences = async (
|
||||
document: any,
|
||||
basePath: string,
|
||||
maxDepth: number = 10
|
||||
): Promise<{
|
||||
document: any;
|
||||
resolvedRefs: ResolvedReference[];
|
||||
circularRefs: string[];
|
||||
}> => {
|
||||
const visited = new Set<string>();
|
||||
const resolvedRefs: ResolvedReference[] = [];
|
||||
const circularRefs: string[] = [];
|
||||
|
||||
const context: ReferenceContext = {
|
||||
basePath,
|
||||
visited,
|
||||
maxDepth,
|
||||
currentDepth: 0,
|
||||
baseDocument: document,
|
||||
};
|
||||
|
||||
const refs = findReferences(document);
|
||||
|
||||
for (const ref of refs) {
|
||||
try {
|
||||
const resolved = await resolveReference(ref.value, context);
|
||||
resolvedRefs.push(resolved);
|
||||
|
||||
if (resolved.isCircular) {
|
||||
circularRefs.push(ref.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Warning: Failed to resolve reference '${ref.value}': ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
document,
|
||||
resolvedRefs,
|
||||
circularRefs,
|
||||
};
|
||||
};
|
||||
331
src/reporter.ts
Normal file
331
src/reporter.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import { writeFileSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import type { ValidationResult, ReportOptions } from "./types.js";
|
||||
|
||||
/**
|
||||
* Generate a comprehensive validation report
|
||||
*/
|
||||
export const generateReport = (
|
||||
result: ValidationResult,
|
||||
options: ReportOptions
|
||||
): string => {
|
||||
switch (options.format) {
|
||||
case "json":
|
||||
return generateJSONReport(result, options);
|
||||
case "yaml":
|
||||
return generateYAMLReport(result, options);
|
||||
case "html":
|
||||
return generateHTMLReport(result, options);
|
||||
case "markdown":
|
||||
return generateMarkdownReport(result, options);
|
||||
default:
|
||||
throw new Error(`Unsupported report format: ${options.format}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Save report to file
|
||||
*/
|
||||
export const saveReport = (content: string, outputPath: string): void => {
|
||||
const fullPath = resolve(outputPath);
|
||||
writeFileSync(fullPath, content, "utf-8");
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract metadata from specification
|
||||
*/
|
||||
const extractMetadata = (spec: any): Record<string, any> => {
|
||||
return {
|
||||
title: spec.info?.title,
|
||||
version: spec.info?.version,
|
||||
description: spec.info?.description,
|
||||
contact: spec.info?.contact ? JSON.stringify(spec.info.contact) : undefined,
|
||||
license: spec.info?.license ? JSON.stringify(spec.info.license) : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate summary section for HTML
|
||||
*/
|
||||
const generateSummarySection = (result: ValidationResult): string => {
|
||||
return `
|
||||
<div class="section">
|
||||
<h3>Summary</h3>
|
||||
<p><strong>Errors:</strong> ${result.errors.length}</p>
|
||||
<p><strong>Warnings:</strong> ${result.warnings.length}</p>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate errors section for HTML
|
||||
*/
|
||||
const generateErrorsSection = (result: ValidationResult): string => {
|
||||
if (result.errors.length === 0) {
|
||||
return '<div class="section"><h3>Errors</h3><p>No errors found.</p></div>';
|
||||
}
|
||||
|
||||
const errorItems = result.errors
|
||||
.map(
|
||||
(error) => `
|
||||
<div class="error">
|
||||
<div><strong>Path:</strong> <span class="path">${error.path}</span></div>
|
||||
<div><strong>Message:</strong> ${error.message}</div>
|
||||
${
|
||||
error.schemaPath
|
||||
? `<div><strong>Schema Path:</strong> <span class="path">${error.schemaPath}</span></div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="section">
|
||||
<h3>Errors (${result.errors.length})</h3>
|
||||
${errorItems}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate warnings section for HTML
|
||||
*/
|
||||
const generateWarningsSection = (result: ValidationResult): string => {
|
||||
if (result.warnings.length === 0) {
|
||||
return '<div class="section"><h3>Warnings</h3><p>No warnings found.</p></div>';
|
||||
}
|
||||
|
||||
const warningItems = result.warnings
|
||||
.map(
|
||||
(warning) => `
|
||||
<div class="warning">
|
||||
<div><strong>Path:</strong> <span class="path">${
|
||||
warning.path
|
||||
}</span></div>
|
||||
<div><strong>Message:</strong> ${warning.message}</div>
|
||||
${
|
||||
warning.schemaPath
|
||||
? `<div><strong>Schema Path:</strong> <span class="path">${warning.schemaPath}</span></div>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="section">
|
||||
<h3>Warnings (${result.warnings.length})</h3>
|
||||
${warningItems}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate metadata section for HTML
|
||||
*/
|
||||
const generateMetadataSection = (result: ValidationResult): string => {
|
||||
const metadata = extractMetadata(result.spec);
|
||||
const metadataItems = Object.entries(metadata)
|
||||
.filter(([_, value]) => value)
|
||||
.map(
|
||||
([key, value]) => `
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">${key}:</span> ${value}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<div class="section">
|
||||
<h3>Metadata</h3>
|
||||
<div class="metadata">
|
||||
${metadataItems}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate JSON report
|
||||
*/
|
||||
const generateJSONReport = (
|
||||
result: ValidationResult,
|
||||
options: ReportOptions
|
||||
): string => {
|
||||
const report = {
|
||||
summary: {
|
||||
valid: result.valid,
|
||||
version: result.version,
|
||||
errorCount: result.errors.length,
|
||||
warningCount: result.warnings.length,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
errors: result.errors,
|
||||
warnings: options.includeWarnings ? result.warnings : undefined,
|
||||
metadata: options.includeMetadata
|
||||
? extractMetadata(result.spec)
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return JSON.stringify(report, null, 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate YAML report
|
||||
*/
|
||||
const generateYAMLReport = (
|
||||
result: ValidationResult,
|
||||
options: ReportOptions
|
||||
): string => {
|
||||
// Simple YAML generation - in production, use a proper YAML library
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push("summary:");
|
||||
lines.push(` valid: ${result.valid}`);
|
||||
lines.push(` version: ${result.version}`);
|
||||
lines.push(` errorCount: ${result.errors.length}`);
|
||||
lines.push(` warningCount: ${result.warnings.length}`);
|
||||
lines.push(` timestamp: ${new Date().toISOString()}`);
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
lines.push("errors:");
|
||||
for (const error of result.errors) {
|
||||
lines.push(` - path: ${error.path}`);
|
||||
lines.push(` message: ${error.message}`);
|
||||
if (error.schemaPath) {
|
||||
lines.push(` schemaPath: ${error.schemaPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.includeWarnings && result.warnings.length > 0) {
|
||||
lines.push("warnings:");
|
||||
for (const warning of result.warnings) {
|
||||
lines.push(` - path: ${warning.path}`);
|
||||
lines.push(` message: ${warning.message}`);
|
||||
if (warning.schemaPath) {
|
||||
lines.push(` schemaPath: ${warning.schemaPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate HTML report
|
||||
*/
|
||||
const generateHTMLReport = (
|
||||
result: ValidationResult,
|
||||
options: ReportOptions
|
||||
): string => {
|
||||
const status = result.valid ? "valid" : "invalid";
|
||||
const statusColor = result.valid ? "#28a745" : "#dc3545";
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OpenAPI Validation Report</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f8f9fa; }
|
||||
.container { max-width: 1200px; margin: 0 auto; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
.header { padding: 20px; border-bottom: 1px solid #e9ecef; }
|
||||
.status { display: inline-block; padding: 8px 16px; border-radius: 4px; color: white; font-weight: bold; background: ${statusColor}; }
|
||||
.content { padding: 20px; }
|
||||
.section { margin-bottom: 30px; }
|
||||
.section h3 { color: #495057; margin-bottom: 15px; }
|
||||
.error, .warning { padding: 12px; margin: 8px 0; border-radius: 4px; border-left: 4px solid; }
|
||||
.error { background: #f8d7da; border-color: #dc3545; color: #721c24; }
|
||||
.warning { background: #fff3cd; border-color: #ffc107; color: #856404; }
|
||||
.path { font-family: monospace; background: #e9ecef; padding: 2px 6px; border-radius: 3px; }
|
||||
.metadata { background: #f8f9fa; padding: 15px; border-radius: 4px; }
|
||||
.metadata-item { margin: 5px 0; }
|
||||
.metadata-label { font-weight: bold; color: #495057; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>OpenAPI Validation Report</h1>
|
||||
<span class="status">${status.toUpperCase()}</span>
|
||||
<p>OpenAPI Version: ${result.version}</p>
|
||||
<p>Generated: ${new Date().toLocaleString()}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
${generateSummarySection(result)}
|
||||
${generateErrorsSection(result)}
|
||||
${options.includeWarnings ? generateWarningsSection(result) : ""}
|
||||
${options.includeMetadata ? generateMetadataSection(result) : ""}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>`.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate Markdown report
|
||||
*/
|
||||
const generateMarkdownReport = (
|
||||
result: ValidationResult,
|
||||
options: ReportOptions
|
||||
): string => {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push("# OpenAPI Validation Report");
|
||||
lines.push("");
|
||||
lines.push(`**Status:** ${result.valid ? "✅ Valid" : "❌ Invalid"}`);
|
||||
lines.push(`**Version:** ${result.version}`);
|
||||
lines.push(`**Generated:** ${new Date().toLocaleString()}`);
|
||||
lines.push("");
|
||||
|
||||
lines.push("## Summary");
|
||||
lines.push(`- **Errors:** ${result.errors.length}`);
|
||||
lines.push(`- **Warnings:** ${result.warnings.length}`);
|
||||
lines.push("");
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
lines.push("## Errors");
|
||||
lines.push("");
|
||||
for (const error of result.errors) {
|
||||
lines.push(`### \`${error.path}\``);
|
||||
lines.push(`**Message:** ${error.message}`);
|
||||
if (error.schemaPath) {
|
||||
lines.push(`**Schema Path:** \`${error.schemaPath}\``);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.includeWarnings && result.warnings.length > 0) {
|
||||
lines.push("## Warnings");
|
||||
lines.push("");
|
||||
for (const warning of result.warnings) {
|
||||
lines.push(`### \`${warning.path}\``);
|
||||
lines.push(`**Message:** ${warning.message}`);
|
||||
if (warning.schemaPath) {
|
||||
lines.push(`**Schema Path:** \`${warning.schemaPath}\``);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.includeMetadata) {
|
||||
lines.push("## Metadata");
|
||||
lines.push("");
|
||||
const metadata = extractMetadata(result.spec);
|
||||
for (const [key, value] of Object.entries(metadata)) {
|
||||
if (value) {
|
||||
lines.push(`- **${key}:** ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
};
|
||||
92
src/types.ts
Normal file
92
src/types.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import type { OpenAPI2, OpenAPI3, OpenAPI3_1, OpenAPI3_2 } from "oas-types";
|
||||
|
||||
export interface ValidationError {
|
||||
path: string;
|
||||
message: string;
|
||||
data?: any;
|
||||
schemaPath?: string;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: ValidationError[];
|
||||
warnings: ValidationError[];
|
||||
spec: OpenAPISpec;
|
||||
version: OpenAPIVersion;
|
||||
}
|
||||
|
||||
export interface ValidationOptions {
|
||||
strict?: boolean;
|
||||
validateExamples?: boolean;
|
||||
validateReferences?: boolean;
|
||||
customRules?: Record<string, any>;
|
||||
maxRefDepth?: number;
|
||||
recursive?: boolean;
|
||||
}
|
||||
|
||||
export interface ReportOptions {
|
||||
format: "json" | "yaml" | "html" | "markdown";
|
||||
output?: string;
|
||||
includeWarnings?: boolean;
|
||||
includeMetadata?: boolean;
|
||||
}
|
||||
|
||||
export interface ParsedSpec {
|
||||
spec: OpenAPISpec;
|
||||
version: OpenAPIVersion;
|
||||
source: string;
|
||||
metadata: {
|
||||
title?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
contact?: any;
|
||||
license?: any;
|
||||
};
|
||||
}
|
||||
|
||||
export type OpenAPIVersion =
|
||||
| "2.0"
|
||||
| "3.0"
|
||||
| "3.0.0"
|
||||
| "3.0.1"
|
||||
| "3.0.2"
|
||||
| "3.0.3"
|
||||
| "3.0.4"
|
||||
| "3.1"
|
||||
| "3.1.0"
|
||||
| "3.1.1"
|
||||
| "3.2"
|
||||
| "3.2.0";
|
||||
|
||||
// Union type for all OpenAPI specifications
|
||||
export type OpenAPISpec =
|
||||
| OpenAPI2.Specification
|
||||
| OpenAPI3.Specification
|
||||
| OpenAPI3_1.Specification
|
||||
| OpenAPI3_2.Specification;
|
||||
|
||||
export interface VarsityConfig {
|
||||
defaultVersion?: OpenAPIVersion;
|
||||
strictMode?: boolean;
|
||||
customSchemas?: Record<string, JSONSchemaType<any>>;
|
||||
reportFormats?: ReportOptions["format"][];
|
||||
}
|
||||
|
||||
export interface CLIResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: any;
|
||||
errors?: ValidationError[];
|
||||
}
|
||||
|
||||
export interface RecursiveValidationResult extends ValidationResult {
|
||||
partialValidations: Array<{
|
||||
path: string;
|
||||
result: ValidationResult;
|
||||
isCircular: boolean;
|
||||
}>;
|
||||
circularReferences: string[];
|
||||
totalDocuments: number;
|
||||
validDocuments: number;
|
||||
}
|
||||
283
src/validator.ts
Normal file
283
src/validator.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import Ajv from "ajv";
|
||||
import type { JSONSchemaType } from "ajv";
|
||||
import addFormats from "ajv-formats";
|
||||
import type {
|
||||
ValidationResult,
|
||||
ValidationError,
|
||||
ValidationOptions,
|
||||
OpenAPIVersion,
|
||||
OpenAPISpec,
|
||||
} from "./types.js";
|
||||
|
||||
import { allSchemas } from "oas-types/schemas";
|
||||
import type { OpenAPI3_1, OpenAPI3_2, OpenAPI3, OpenAPI2 } from "oas-types";
|
||||
|
||||
// Initialize AJV instance
|
||||
const createAjvInstance = (): Ajv => {
|
||||
const ajv = new Ajv({
|
||||
allErrors: true,
|
||||
verbose: true,
|
||||
strict: false,
|
||||
validateFormats: true,
|
||||
});
|
||||
addFormats(ajv);
|
||||
return ajv;
|
||||
};
|
||||
|
||||
// Global instances
|
||||
const ajv = createAjvInstance();
|
||||
|
||||
// Create schemas map from oas-types
|
||||
const schemas = new Map<OpenAPIVersion, any>();
|
||||
schemas.set("2.0", allSchemas["2.0"].specification);
|
||||
schemas.set("3.0", allSchemas["3.0"].specification);
|
||||
schemas.set("3.1", allSchemas["3.1"].specification);
|
||||
schemas.set("3.2", allSchemas["3.2"].specification);
|
||||
|
||||
/**
|
||||
* Normalize OpenAPI version to the base version for schema lookup
|
||||
*/
|
||||
const normalizeVersion = (version: OpenAPIVersion): OpenAPIVersion => {
|
||||
if (version.startsWith("3.0")) {
|
||||
return "3.0";
|
||||
} else if (version.startsWith("3.1")) {
|
||||
return "3.1";
|
||||
} else if (version.startsWith("3.2")) {
|
||||
return "3.2";
|
||||
}
|
||||
return version;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find all $ref references in the specification
|
||||
*/
|
||||
const findReferences = (
|
||||
obj: OpenAPISpec,
|
||||
path = ""
|
||||
): Array<{ path: string; value: string }> => {
|
||||
const refs: Array<{ path: string; value: string }> = [];
|
||||
|
||||
if (typeof obj === "object" && obj !== null) {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const currentPath = path ? `${path}.${key}` : key;
|
||||
|
||||
if (key === "$ref" && typeof value === "string") {
|
||||
refs.push({ path: currentPath, value });
|
||||
} else if (typeof value === "object") {
|
||||
refs.push(...findReferences(value, currentPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a reference to check if it's valid
|
||||
*/
|
||||
const resolveReference = (
|
||||
spec: OpenAPISpec,
|
||||
ref: { path: string; value: string }
|
||||
): boolean => {
|
||||
// Simple reference resolution - in a real implementation, this would be more comprehensive
|
||||
if (ref.value.startsWith("#/")) {
|
||||
const path = ref.value.substring(2).split("/");
|
||||
let current = spec;
|
||||
|
||||
for (const segment of path) {
|
||||
if (current && typeof current === "object" && segment in current) {
|
||||
current = (current as any)[segment];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return current !== undefined;
|
||||
}
|
||||
|
||||
return false; // External references not supported in this simple implementation
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform strict validation checks
|
||||
*/
|
||||
const performStrictValidation = (
|
||||
spec: OpenAPISpec,
|
||||
version: OpenAPIVersion,
|
||||
errors: ValidationError[],
|
||||
warnings: ValidationError[]
|
||||
): void => {
|
||||
// Check for required fields based on version
|
||||
if (version === "2.0") {
|
||||
const swaggerSpec = spec as OpenAPI2.Specification;
|
||||
if (!swaggerSpec.host) {
|
||||
errors.push({
|
||||
path: "/",
|
||||
message: 'Either "host" or "servers" must be specified in Swagger 2.0',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const openapiSpec = spec as
|
||||
| OpenAPI3.Specification
|
||||
| OpenAPI3_1.Specification
|
||||
| OpenAPI3_2.Specification;
|
||||
if (!openapiSpec.servers || openapiSpec.servers.length === 0) {
|
||||
warnings.push({
|
||||
path: "/",
|
||||
message: "No servers specified. Consider adding at least one server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for security definitions
|
||||
if (version === "2.0") {
|
||||
const swaggerSpec = spec as OpenAPI2.Specification;
|
||||
if (
|
||||
swaggerSpec.security &&
|
||||
swaggerSpec.security.length > 0 &&
|
||||
!swaggerSpec.securityDefinitions
|
||||
) {
|
||||
errors.push({
|
||||
path: "/",
|
||||
message: "Security schemes must be defined when using security",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const openapiSpec = spec as
|
||||
| OpenAPI3.Specification
|
||||
| OpenAPI3_1.Specification
|
||||
| OpenAPI3_2.Specification;
|
||||
if (
|
||||
openapiSpec.security &&
|
||||
openapiSpec.security.length > 0 &&
|
||||
!openapiSpec.components?.securitySchemes
|
||||
) {
|
||||
errors.push({
|
||||
path: "/",
|
||||
message: "Security schemes must be defined when using security",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate examples in the specification
|
||||
*/
|
||||
const validateExamples = (
|
||||
spec: OpenAPISpec,
|
||||
version: OpenAPIVersion,
|
||||
errors: ValidationError[],
|
||||
warnings: ValidationError[]
|
||||
): void => {
|
||||
// This would implement example validation logic
|
||||
// For now, just a placeholder
|
||||
if (spec.paths) {
|
||||
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
||||
if (typeof pathItem === "object" && pathItem !== null) {
|
||||
for (const [method, operation] of Object.entries(pathItem)) {
|
||||
if (
|
||||
typeof operation === "object" &&
|
||||
operation !== null &&
|
||||
"responses" in operation
|
||||
) {
|
||||
// Check response examples
|
||||
for (const [statusCode, response] of Object.entries(
|
||||
operation.responses
|
||||
)) {
|
||||
if (
|
||||
typeof response === "object" &&
|
||||
response !== null &&
|
||||
"examples" in response
|
||||
) {
|
||||
// Validate examples here
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate references in the specification
|
||||
*/
|
||||
const validateReferences = (
|
||||
spec: OpenAPISpec,
|
||||
version: OpenAPIVersion,
|
||||
errors: ValidationError[],
|
||||
warnings: ValidationError[]
|
||||
): void => {
|
||||
// This would implement reference validation logic
|
||||
// Check for broken $ref references
|
||||
const refs = findReferences(spec);
|
||||
for (const ref of refs) {
|
||||
if (!resolveReference(spec, ref)) {
|
||||
errors.push({
|
||||
path: ref.path,
|
||||
message: `Broken reference: ${ref.value}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate an OpenAPI specification
|
||||
*/
|
||||
export const validateOpenAPISpec = (
|
||||
spec: OpenAPISpec,
|
||||
version: OpenAPIVersion,
|
||||
options: ValidationOptions = {}
|
||||
): ValidationResult => {
|
||||
const normalizedVersion = normalizeVersion(version);
|
||||
const schema = schemas.get(normalizedVersion);
|
||||
if (!schema) {
|
||||
throw new Error(
|
||||
`No schema available for OpenAPI version: ${version} (normalized to ${normalizedVersion})`
|
||||
);
|
||||
}
|
||||
|
||||
const validate = ajv.compile(schema);
|
||||
const valid = validate(spec);
|
||||
|
||||
const errors: ValidationError[] = [];
|
||||
const warnings: ValidationError[] = [];
|
||||
|
||||
if (!valid && validate.errors) {
|
||||
for (const error of validate.errors) {
|
||||
const validationError: ValidationError = {
|
||||
path: error.instancePath || error.schemaPath || "/",
|
||||
message: error.message || "Validation error",
|
||||
data: error.data,
|
||||
schemaPath: error.schemaPath,
|
||||
};
|
||||
|
||||
if (error.keyword === "required" || error.keyword === "type") {
|
||||
errors.push(validationError);
|
||||
} else {
|
||||
warnings.push(validationError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additional custom validations
|
||||
if (options.strict) {
|
||||
performStrictValidation(spec, version, errors, warnings);
|
||||
}
|
||||
|
||||
if (options.validateExamples) {
|
||||
validateExamples(spec, version, errors, warnings);
|
||||
}
|
||||
|
||||
if (options.validateReferences) {
|
||||
validateReferences(spec, version, errors, warnings);
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
spec,
|
||||
version,
|
||||
};
|
||||
};
|
||||
232
src/varsity.ts
Normal file
232
src/varsity.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { parseOpenAPISpec, validateBasicStructure } from "./parser.js";
|
||||
import { validateOpenAPISpec } from "./validator.js";
|
||||
import { generateReport, saveReport } from "./reporter.js";
|
||||
import {
|
||||
validateRecursively,
|
||||
validateMultipleRecursively,
|
||||
analyzeReferences,
|
||||
} from "./recursive-validator.js";
|
||||
import type {
|
||||
ParsedSpec,
|
||||
ValidationResult,
|
||||
ValidationOptions,
|
||||
ReportOptions,
|
||||
VarsityConfig,
|
||||
OpenAPISpec,
|
||||
RecursiveValidationResult,
|
||||
} from "./types.js";
|
||||
|
||||
// Default configuration
|
||||
const defaultConfig: VarsityConfig = {
|
||||
defaultVersion: "3.0",
|
||||
strictMode: false,
|
||||
customSchemas: {},
|
||||
reportFormats: ["json"],
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse and validate an OpenAPI specification or multiple specifications
|
||||
*/
|
||||
export const validate = async (
|
||||
source: string | string[],
|
||||
options: ValidationOptions = {},
|
||||
config: VarsityConfig = defaultConfig
|
||||
): Promise<ValidationResult | ValidationResult[]> => {
|
||||
// If source is an array, validate multiple specifications
|
||||
if (Array.isArray(source)) {
|
||||
const results: ValidationResult[] = [];
|
||||
|
||||
for (const singleSource of source) {
|
||||
try {
|
||||
const result = await validateSingle(singleSource, options, config);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
// Create error result for failed parsing
|
||||
const errorResult: ValidationResult = {
|
||||
valid: false,
|
||||
errors: [
|
||||
{
|
||||
path: "/",
|
||||
message: `Failed to parse specification: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
spec: {} as OpenAPISpec,
|
||||
version: config.defaultVersion!,
|
||||
};
|
||||
results.push(errorResult);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Single specification validation
|
||||
return validateSingle(source, options, config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal function to validate a single OpenAPI specification
|
||||
*/
|
||||
const validateSingle = async (
|
||||
source: string,
|
||||
options: ValidationOptions = {},
|
||||
config: VarsityConfig = defaultConfig
|
||||
): Promise<ValidationResult> => {
|
||||
const parsed = await parseOpenAPISpec(source);
|
||||
|
||||
// Merge options with config
|
||||
const validationOptions: ValidationOptions = {
|
||||
strict: options.strict ?? config.strictMode,
|
||||
validateExamples: options.validateExamples ?? false,
|
||||
validateReferences: options.validateReferences ?? false,
|
||||
customRules: options.customRules,
|
||||
...options,
|
||||
};
|
||||
|
||||
// If recursive validation is requested, use the recursive validator
|
||||
if (options.recursive) {
|
||||
const recursiveResult = await validateRecursively(
|
||||
source,
|
||||
validationOptions
|
||||
);
|
||||
return {
|
||||
valid: recursiveResult.valid,
|
||||
errors: recursiveResult.errors,
|
||||
warnings: recursiveResult.warnings,
|
||||
spec: recursiveResult.spec,
|
||||
version: recursiveResult.version,
|
||||
};
|
||||
}
|
||||
|
||||
return validateOpenAPISpec(parsed.spec, parsed.version, validationOptions);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse an OpenAPI specification without validation
|
||||
*/
|
||||
export const parse = async (source: string): Promise<ParsedSpec> => {
|
||||
return parseOpenAPISpec(source);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a validation report
|
||||
*/
|
||||
export const generateValidationReport = async (
|
||||
source: string,
|
||||
reportOptions: ReportOptions,
|
||||
validationOptions: ValidationOptions = {},
|
||||
config: VarsityConfig = defaultConfig
|
||||
): Promise<string> => {
|
||||
const result = await validate(source, validationOptions, config);
|
||||
// Since source is a string, result will be ValidationResult, not ValidationResult[]
|
||||
return generateReport(result as ValidationResult, reportOptions);
|
||||
};
|
||||
|
||||
/**
|
||||
* Save a validation report to file
|
||||
*/
|
||||
export const saveValidationReport = async (
|
||||
source: string,
|
||||
reportOptions: ReportOptions,
|
||||
validationOptions: ValidationOptions = {},
|
||||
config: VarsityConfig = defaultConfig
|
||||
): Promise<void> => {
|
||||
const report = await generateValidationReport(
|
||||
source,
|
||||
reportOptions,
|
||||
validationOptions,
|
||||
config
|
||||
);
|
||||
if (reportOptions.output) {
|
||||
saveReport(report, reportOptions.output);
|
||||
} else {
|
||||
console.log(report);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively validate an OpenAPI specification and all its references
|
||||
*/
|
||||
export const validateWithReferences = async (
|
||||
source: string,
|
||||
options: ValidationOptions = {},
|
||||
config: VarsityConfig = defaultConfig
|
||||
): Promise<RecursiveValidationResult> => {
|
||||
return validateRecursively(source, { ...options, recursive: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively validate multiple OpenAPI specifications
|
||||
*/
|
||||
export const validateMultipleWithReferences = async (
|
||||
sources: string[],
|
||||
options: ValidationOptions = {},
|
||||
config: VarsityConfig = defaultConfig
|
||||
): Promise<RecursiveValidationResult[]> => {
|
||||
return validateMultipleRecursively(sources, { ...options, recursive: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Analyze references in an OpenAPI specification
|
||||
*/
|
||||
export const analyzeDocumentReferences = async (source: string) => {
|
||||
return analyzeReferences(source);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get supported OpenAPI versions
|
||||
*/
|
||||
export const getSupportedVersions = (): string[] => {
|
||||
return ["2.0", "3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.1.0"];
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a Varsity instance with configuration
|
||||
*/
|
||||
export const createVarsity = (config: VarsityConfig = {}) => {
|
||||
const mergedConfig = { ...defaultConfig, ...config };
|
||||
|
||||
return {
|
||||
validate: (source: string | string[], options: ValidationOptions = {}) =>
|
||||
validate(source, options, mergedConfig),
|
||||
parse,
|
||||
generateReport: (
|
||||
source: string,
|
||||
reportOptions: ReportOptions,
|
||||
validationOptions: ValidationOptions = {}
|
||||
) =>
|
||||
generateValidationReport(
|
||||
source,
|
||||
reportOptions,
|
||||
validationOptions,
|
||||
mergedConfig
|
||||
),
|
||||
getSupportedVersions,
|
||||
getConfig: () => ({ ...mergedConfig }),
|
||||
updateConfig: (newConfig: Partial<VarsityConfig>) => {
|
||||
Object.assign(mergedConfig, newConfig);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Export individual functions for direct use
|
||||
export { parseOpenAPISpec, validateBasicStructure } from "./parser.js";
|
||||
export { validateOpenAPISpec } from "./validator.js";
|
||||
export { generateReport, saveReport } from "./reporter.js";
|
||||
|
||||
export type {
|
||||
ParsedSpec,
|
||||
ValidationResult,
|
||||
ValidationError,
|
||||
ValidationOptions,
|
||||
ReportOptions,
|
||||
VarsityConfig,
|
||||
OpenAPIVersion,
|
||||
CLIResult,
|
||||
} from "./types.js";
|
||||
|
||||
// Default export - create a default instance
|
||||
export default createVarsity();
|
||||
53
test/backwards-refs/main-api.json
Normal file
53
test/backwards-refs/main-api.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Backwards References API",
|
||||
"version": "1.0.0",
|
||||
"description": "API with backwards $ref paths for testing"
|
||||
},
|
||||
"paths": {
|
||||
"/users": {
|
||||
"get": {
|
||||
"summary": "Get users",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "../shared/responses/users-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/products": {
|
||||
"get": {
|
||||
"summary": "Get products",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "../shared/responses/products-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/orders": {
|
||||
"get": {
|
||||
"summary": "Get orders",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "../shared/responses/orders-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"User": {
|
||||
"$ref": "../shared/schemas/user-schema.json"
|
||||
},
|
||||
"Product": {
|
||||
"$ref": "../shared/schemas/product-schema.json"
|
||||
},
|
||||
"Order": {
|
||||
"$ref": "../shared/schemas/order-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
145
test/basic.test.ts
Normal file
145
test/basic.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import {
|
||||
validate,
|
||||
parse,
|
||||
generateValidationReport,
|
||||
getSupportedVersions,
|
||||
createVarsity,
|
||||
type ValidationResult,
|
||||
} from "../src/varsity.js";
|
||||
import { resolve } from "path";
|
||||
|
||||
describe("Varsity Library", () => {
|
||||
const sampleSpecPath = resolve(__dirname, "sample-openapi.json");
|
||||
|
||||
test("should parse a valid OpenAPI specification", async () => {
|
||||
const parsed = await parse(sampleSpecPath);
|
||||
|
||||
expect(parsed).toBeDefined();
|
||||
expect(parsed.version).toBe("3.0.3");
|
||||
expect(parsed.spec.info.title).toBe("Sample API");
|
||||
});
|
||||
|
||||
test("should validate a valid OpenAPI specification", async () => {
|
||||
const result = (await validate(sampleSpecPath)) as ValidationResult;
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
expect(result.version).toBe("3.0.3");
|
||||
});
|
||||
|
||||
test("should generate a JSON report", async () => {
|
||||
const report = await generateValidationReport(sampleSpecPath, {
|
||||
format: "json",
|
||||
includeWarnings: true,
|
||||
includeMetadata: true,
|
||||
});
|
||||
|
||||
const parsedReport = JSON.parse(report);
|
||||
expect(parsedReport.summary.valid).toBe(true);
|
||||
expect(parsedReport.summary.version).toBe("3.0.3");
|
||||
});
|
||||
|
||||
test("should generate a markdown report", async () => {
|
||||
const report = await generateValidationReport(sampleSpecPath, {
|
||||
format: "markdown",
|
||||
includeWarnings: true,
|
||||
includeMetadata: true,
|
||||
});
|
||||
|
||||
expect(report).toContain("# OpenAPI Validation Report");
|
||||
expect(report).toContain("✅ Valid");
|
||||
expect(report).toContain("Sample API");
|
||||
});
|
||||
|
||||
test("should get supported versions", () => {
|
||||
const versions = getSupportedVersions();
|
||||
|
||||
expect(versions).toContain("2.0");
|
||||
expect(versions).toContain("3.0.3");
|
||||
expect(versions).toContain("3.1.0");
|
||||
});
|
||||
|
||||
test("should handle invalid specification", async () => {
|
||||
const invalidSpec = {
|
||||
openapi: "3.0.3",
|
||||
// Missing required 'info' and 'paths' fields
|
||||
};
|
||||
|
||||
// Create a temporary file with invalid spec
|
||||
const fs = await import("fs");
|
||||
const tempPath = resolve(__dirname, "invalid-spec.json");
|
||||
fs.writeFileSync(tempPath, JSON.stringify(invalidSpec));
|
||||
|
||||
try {
|
||||
const result = (await validate(tempPath)) as ValidationResult;
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
} finally {
|
||||
// Clean up
|
||||
fs.unlinkSync(tempPath);
|
||||
}
|
||||
});
|
||||
|
||||
test("should work with createVarsity factory", async () => {
|
||||
const varsity = createVarsity();
|
||||
|
||||
const result = (await varsity.validate(sampleSpecPath)) as ValidationResult;
|
||||
expect(result.valid).toBe(true);
|
||||
|
||||
const versions = varsity.getSupportedVersions();
|
||||
expect(versions).toContain("3.0.3");
|
||||
});
|
||||
|
||||
test("should work with createVarsity factory for multiple specs", async () => {
|
||||
const varsity = createVarsity();
|
||||
|
||||
const results = (await varsity.validate([
|
||||
sampleSpecPath,
|
||||
sampleSpecPath,
|
||||
])) as ValidationResult[];
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]?.valid).toBe(true);
|
||||
expect(results[1]?.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should validate multiple specifications", async () => {
|
||||
const results = (await validate([
|
||||
sampleSpecPath,
|
||||
sampleSpecPath,
|
||||
])) as ValidationResult[];
|
||||
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]?.valid).toBe(true);
|
||||
expect(results[1]?.valid).toBe(true);
|
||||
});
|
||||
|
||||
test("should handle mixed valid and invalid specifications", async () => {
|
||||
const invalidSpec = {
|
||||
openapi: "3.0.3",
|
||||
// Missing required 'info' and 'paths' fields
|
||||
};
|
||||
|
||||
// Create a temporary file with invalid spec
|
||||
const fs = await import("fs");
|
||||
const tempPath = resolve(__dirname, "invalid-spec-mixed.json");
|
||||
fs.writeFileSync(tempPath, JSON.stringify(invalidSpec));
|
||||
|
||||
try {
|
||||
const results = (await validate([
|
||||
sampleSpecPath,
|
||||
tempPath,
|
||||
])) as ValidationResult[];
|
||||
|
||||
expect(Array.isArray(results)).toBe(true);
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0]?.valid).toBe(true); // Valid spec
|
||||
expect(results[1]?.valid).toBe(false); // Invalid spec
|
||||
} finally {
|
||||
// Clean up
|
||||
fs.unlinkSync(tempPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
30
test/circular-refs-api.json
Normal file
30
test/circular-refs-api.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Circular References API",
|
||||
"version": "1.0.0",
|
||||
"description": "API with intentional circular references for testing"
|
||||
},
|
||||
"paths": {
|
||||
"/users": {
|
||||
"get": {
|
||||
"summary": "Get users",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "./responses/users-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"User": {
|
||||
"$ref": "./schemas/user-schema.json"
|
||||
},
|
||||
"Category": {
|
||||
"$ref": "./schemas/category-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
test/deep/nested/api/main-api.json
Normal file
40
test/deep/nested/api/main-api.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Deep Nested API with Backwards References",
|
||||
"version": "1.0.0",
|
||||
"description": "API in deep nested directory with backwards $ref paths"
|
||||
},
|
||||
"paths": {
|
||||
"/users": {
|
||||
"get": {
|
||||
"summary": "Get users",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "../../../shared/responses/users-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/products": {
|
||||
"get": {
|
||||
"summary": "Get products",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "../../../shared/responses/products-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"User": {
|
||||
"$ref": "../../../shared/schemas/user-schema.json"
|
||||
},
|
||||
"Product": {
|
||||
"$ref": "../../../shared/schemas/product-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
test/edge-cases/very-deep/api/main-api.json
Normal file
27
test/edge-cases/very-deep/api/main-api.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Edge Cases API",
|
||||
"version": "1.0.0",
|
||||
"description": "API testing various edge cases for $ref resolution"
|
||||
},
|
||||
"paths": {
|
||||
"/test": {
|
||||
"get": {
|
||||
"summary": "Test endpoint",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "../../../../shared/responses/test-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"TestSchema": {
|
||||
"$ref": "../../../../shared/schemas/test-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
108
test/main-api.json
Normal file
108
test/main-api.json
Normal file
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Main API",
|
||||
"version": "1.0.0",
|
||||
"description": "Main API with external references"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.example.com/v1"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/users": {
|
||||
"get": {
|
||||
"summary": "Get all users",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "./responses/users-response.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Create user",
|
||||
"requestBody": {
|
||||
"$ref": "./request-bodies/user-request.json"
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"$ref": "./responses/user-created-response.json"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "./responses/error-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{id}": {
|
||||
"get": {
|
||||
"summary": "Get user by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "./parameters/user-id-param.json"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "./responses/user-response.json"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "./responses/not-found-response.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"summary": "Update user",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "./parameters/user-id-param.json"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"$ref": "./request-bodies/user-update-request.json"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "./responses/user-response.json"
|
||||
},
|
||||
"400": {
|
||||
"$ref": "./responses/error-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/products": {
|
||||
"$ref": "./paths/products-path.json"
|
||||
},
|
||||
"/orders": {
|
||||
"$ref": "./paths/orders-path.json"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"User": {
|
||||
"$ref": "./schemas/user-schema.json"
|
||||
},
|
||||
"Product": {
|
||||
"$ref": "./schemas/product-schema.json"
|
||||
},
|
||||
"Error": {
|
||||
"$ref": "./schemas/error-schema.json"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"PageParam": {
|
||||
"$ref": "./parameters/page-param.json"
|
||||
},
|
||||
"LimitParam": {
|
||||
"$ref": "./parameters/limit-param.json"
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"UnauthorizedResponse": {
|
||||
"$ref": "./responses/unauthorized-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
test/parameters/category-filter-param.json
Normal file
10
test/parameters/category-filter-param.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "category",
|
||||
"in": "query",
|
||||
"description": "Filter products by category",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
12
test/parameters/limit-param.json
Normal file
12
test/parameters/limit-param.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"description": "Number of items per page",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"default": 20
|
||||
}
|
||||
}
|
||||
10
test/parameters/order-id-param.json
Normal file
10
test/parameters/order-id-param.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "orderId",
|
||||
"in": "path",
|
||||
"description": "Order ID",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"pattern": "^ORD-[0-9]{8}$"
|
||||
}
|
||||
}
|
||||
11
test/parameters/page-param.json
Normal file
11
test/parameters/page-param.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"description": "Page number for pagination",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"default": 1
|
||||
}
|
||||
}
|
||||
10
test/parameters/status-filter-param.json
Normal file
10
test/parameters/status-filter-param.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "status",
|
||||
"in": "query",
|
||||
"description": "Filter orders by status",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "processing", "shipped", "delivered", "cancelled"]
|
||||
}
|
||||
}
|
||||
11
test/parameters/user-id-param.json
Normal file
11
test/parameters/user-id-param.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"description": "User ID",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
74
test/paths/orders-path.json
Normal file
74
test/paths/orders-path.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"get": {
|
||||
"summary": "Get all orders",
|
||||
"description": "Retrieve orders for the authenticated user",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/page-param.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/limit-param.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/status-filter-param.json"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of orders",
|
||||
"$ref": "../responses/orders-response.json"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"$ref": "../responses/unauthorized-response.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Create a new order",
|
||||
"description": "Place a new order",
|
||||
"requestBody": {
|
||||
"$ref": "../request-bodies/order-request.json"
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Order created successfully",
|
||||
"$ref": "../responses/order-created-response.json"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"$ref": "../responses/error-response.json"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"$ref": "../responses/unauthorized-response.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"summary": "Update order status",
|
||||
"description": "Update the status of an existing order",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/order-id-param.json"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"$ref": "../request-bodies/order-status-update.json"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Order updated successfully",
|
||||
"$ref": "../responses/order-response.json"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"$ref": "../responses/error-response.json"
|
||||
},
|
||||
"404": {
|
||||
"description": "Order not found",
|
||||
"$ref": "../responses/not-found-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
test/paths/products-path.json
Normal file
48
test/paths/products-path.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"get": {
|
||||
"summary": "Get all products",
|
||||
"description": "Retrieve a paginated list of products with optional filtering",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/page-param.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/limit-param.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/category-filter-param.json"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of products",
|
||||
"$ref": "../responses/products-response.json"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"$ref": "../responses/error-response.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Create a new product",
|
||||
"description": "Add a new product to the catalog",
|
||||
"requestBody": {
|
||||
"$ref": "../request-bodies/product-request.json"
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Product created successfully",
|
||||
"$ref": "../responses/product-created-response.json"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"$ref": "../responses/error-response.json"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"$ref": "../responses/unauthorized-response.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
test/request-bodies/order-request.json
Normal file
34
test/request-bodies/order-request.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"description": "Order creation data",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["items", "shippingAddress"],
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../schemas/order-item-schema.json"
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"shippingAddress": {
|
||||
"$ref": "../schemas/address-schema.json"
|
||||
},
|
||||
"billingAddress": {
|
||||
"$ref": "../schemas/address-schema.json"
|
||||
},
|
||||
"paymentMethod": {
|
||||
"$ref": "../schemas/payment-method-schema.json"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
test/request-bodies/order-status-update.json
Normal file
32
test/request-bodies/order-status-update.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"description": "Order status update data",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["status"],
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pending",
|
||||
"processing",
|
||||
"shipped",
|
||||
"delivered",
|
||||
"cancelled"
|
||||
]
|
||||
},
|
||||
"trackingNumber": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z0-9]{10,20}$"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
test/request-bodies/product-request.json
Normal file
38
test/request-bodies/product-request.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"description": "Product creation data",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["name", "price"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 200
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 1000
|
||||
},
|
||||
"price": {
|
||||
"$ref": "../schemas/price-schema.json"
|
||||
},
|
||||
"category": {
|
||||
"$ref": "../schemas/category-schema.json"
|
||||
},
|
||||
"inventory": {
|
||||
"$ref": "../schemas/inventory-schema.json"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
test/request-bodies/user-request.json
Normal file
34
test/request-bodies/user-request.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"description": "User creation data",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["name", "email"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"age": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 150
|
||||
},
|
||||
"address": {
|
||||
"$ref": "../schemas/address-schema.json"
|
||||
},
|
||||
"preferences": {
|
||||
"$ref": "../schemas/user-preferences-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
test/request-bodies/user-update-request.json
Normal file
33
test/request-bodies/user-update-request.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"description": "User update data",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"age": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 150
|
||||
},
|
||||
"address": {
|
||||
"$ref": "../schemas/address-schema.json"
|
||||
},
|
||||
"preferences": {
|
||||
"$ref": "../schemas/user-preferences-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
test/responses/error-response.json
Normal file
10
test/responses/error-response.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"description": "Bad request error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../schemas/error-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
test/responses/not-found-response.json
Normal file
10
test/responses/not-found-response.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"description": "Resource not found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../schemas/error-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/responses/order-created-response.json
Normal file
18
test/responses/order-created-response.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"description": "Order created successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "../schemas/order-schema.json"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
test/responses/order-response.json
Normal file
15
test/responses/order-response.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"description": "Order details",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "../schemas/order-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
test/responses/orders-response.json
Normal file
21
test/responses/orders-response.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"description": "List of orders",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../schemas/order-schema.json"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"$ref": "../schemas/pagination-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/responses/product-created-response.json
Normal file
18
test/responses/product-created-response.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"description": "Product created successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "../schemas/product-schema.json"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
test/responses/products-response.json
Normal file
21
test/responses/products-response.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"description": "List of products",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../schemas/product-schema.json"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"$ref": "../schemas/pagination-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
test/responses/unauthorized-response.json
Normal file
10
test/responses/unauthorized-response.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"description": "Unauthorized access",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../schemas/error-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/responses/user-created-response.json
Normal file
18
test/responses/user-created-response.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"description": "User created successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "../schemas/user-schema.json"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
test/responses/user-response.json
Normal file
15
test/responses/user-response.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"description": "User details",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "../schemas/user-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/responses/users-response.json
Normal file
18
test/responses/users-response.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"description": "List of users",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../schemas/user-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
test/sample-openapi.json
Normal file
43
test/sample-openapi.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "Sample API",
|
||||
"version": "1.0.0",
|
||||
"description": "A sample OpenAPI specification for testing"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.example.com/v1"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/users": {
|
||||
"get": {
|
||||
"summary": "Get all users",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
test/schemas/address-schema.json
Normal file
28
test/schemas/address-schema.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"street": {
|
||||
"type": "string",
|
||||
"description": "Street address"
|
||||
},
|
||||
"city": {
|
||||
"type": "string",
|
||||
"description": "City name"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description": "State or province"
|
||||
},
|
||||
"zipCode": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{5}(-[0-9]{4})?$",
|
||||
"description": "ZIP or postal code"
|
||||
},
|
||||
"country": {
|
||||
"type": "string",
|
||||
"minLength": 2,
|
||||
"maxLength": 2,
|
||||
"description": "Two-letter country code"
|
||||
}
|
||||
}
|
||||
}
|
||||
21
test/schemas/category-schema.json
Normal file
21
test/schemas/category-schema.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent": {
|
||||
"$ref": "./category-schema.json"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "./category-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
test/schemas/error-schema.json
Normal file
19
test/schemas/error-schema.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["code", "message"],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"details": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
20
test/schemas/inventory-schema.json
Normal file
20
test/schemas/inventory-schema.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quantity": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"reserved": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"available": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"warehouse": {
|
||||
"$ref": "./warehouse-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
test/schemas/order-item-schema.json
Normal file
23
test/schemas/order-item-schema.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["productId", "quantity"],
|
||||
"properties": {
|
||||
"productId": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 1
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"price": {
|
||||
"$ref": "./price-schema.json"
|
||||
},
|
||||
"discount": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
44
test/schemas/order-schema.json
Normal file
44
test/schemas/order-schema.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "status", "items", "total"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^ORD-[0-9]{8}$"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "processing", "shipped", "delivered", "cancelled"]
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "./order-item-schema.json"
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"$ref": "./price-schema.json"
|
||||
},
|
||||
"shippingAddress": {
|
||||
"$ref": "./address-schema.json"
|
||||
},
|
||||
"billingAddress": {
|
||||
"$ref": "./address-schema.json"
|
||||
},
|
||||
"paymentMethod": {
|
||||
"$ref": "./payment-method-schema.json"
|
||||
},
|
||||
"trackingNumber": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z0-9]{10,20}$"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
28
test/schemas/pagination-schema.json
Normal file
28
test/schemas/pagination-schema.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"page": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100
|
||||
},
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"totalPages": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"hasNext": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasPrev": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
test/schemas/payment-method-schema.json
Normal file
35
test/schemas/payment-method-schema.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["type", "details"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["credit_card", "debit_card", "paypal", "bank_transfer"]
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cardNumber": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{13,19}$"
|
||||
},
|
||||
"expiryMonth": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 12
|
||||
},
|
||||
"expiryYear": {
|
||||
"type": "integer",
|
||||
"minimum": 2024
|
||||
},
|
||||
"cvv": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{3,4}$"
|
||||
},
|
||||
"accountId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
test/schemas/price-schema.json
Normal file
30
test/schemas/price-schema.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["amount", "currency"],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"multipleOf": 0.01
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z]{3}$",
|
||||
"description": "ISO 4217 currency code"
|
||||
},
|
||||
"discount": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"percentage": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
"validUntil": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/schemas/privacy-settings-schema.json
Normal file
18
test/schemas/privacy-settings-schema.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"profileVisibility": {
|
||||
"type": "string",
|
||||
"enum": ["public", "private", "friends"],
|
||||
"default": "private"
|
||||
},
|
||||
"dataSharing": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"analytics": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
34
test/schemas/product-schema.json
Normal file
34
test/schemas/product-schema.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "price"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 200
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 1000
|
||||
},
|
||||
"price": {
|
||||
"$ref": "./price-schema.json"
|
||||
},
|
||||
"category": {
|
||||
"$ref": "./category-schema.json"
|
||||
},
|
||||
"inventory": {
|
||||
"$ref": "./inventory-schema.json"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
test/schemas/user-preferences-schema.json
Normal file
37
test/schemas/user-preferences-schema.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"theme": {
|
||||
"type": "string",
|
||||
"enum": ["light", "dark", "auto"],
|
||||
"default": "auto",
|
||||
"description": "User's preferred theme"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z]{2}(-[A-Z]{2})?$",
|
||||
"default": "en",
|
||||
"description": "User's preferred language code"
|
||||
},
|
||||
"notifications": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"push": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"sms": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"privacy": {
|
||||
"$ref": "./privacy-settings-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
44
test/schemas/user-schema.json
Normal file
44
test/schemas/user-schema.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "email"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "Unique identifier for the user"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100,
|
||||
"description": "User's full name"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"description": "User's email address"
|
||||
},
|
||||
"age": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 150,
|
||||
"description": "User's age"
|
||||
},
|
||||
"address": {
|
||||
"$ref": "./address-schema.json"
|
||||
},
|
||||
"preferences": {
|
||||
"$ref": "./user-preferences-schema.json"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the user was created"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "When the user was last updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
test/schemas/warehouse-schema.json
Normal file
15
test/schemas/warehouse-schema.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"location": {
|
||||
"$ref": "./address-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/shared/responses/orders-response.json
Normal file
18
test/shared/responses/orders-response.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"description": "List of orders",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../schemas/order-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/shared/responses/products-response.json
Normal file
18
test/shared/responses/products-response.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"description": "List of products",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../schemas/product-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
test/shared/responses/test-response.json
Normal file
15
test/shared/responses/test-response.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"description": "Test response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/shared/responses/users-response.json
Normal file
18
test/shared/responses/users-response.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"description": "List of users",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../schemas/user-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
test/shared/schemas/address-schema.json
Normal file
23
test/shared/schemas/address-schema.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"street": {
|
||||
"type": "string"
|
||||
},
|
||||
"city": {
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
"type": "string"
|
||||
},
|
||||
"zipCode": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{5}(-[0-9]{4})?$"
|
||||
},
|
||||
"country": {
|
||||
"type": "string",
|
||||
"minLength": 2,
|
||||
"maxLength": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
15
test/shared/schemas/category-schema.json
Normal file
15
test/shared/schemas/category-schema.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/shared/schemas/order-item-schema.json
Normal file
18
test/shared/schemas/order-item-schema.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["productId", "quantity"],
|
||||
"properties": {
|
||||
"productId": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"minimum": 1
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"price": {
|
||||
"$ref": "./price-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
23
test/shared/schemas/order-schema.json
Normal file
23
test/shared/schemas/order-schema.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "status", "items"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^ORD-[0-9]{8}$"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "processing", "shipped", "delivered", "cancelled"]
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "./order-item-schema.json"
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"$ref": "./price-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
test/shared/schemas/price-schema.json
Normal file
15
test/shared/schemas/price-schema.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["amount", "currency"],
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"multipleOf": 0.01
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Z]{3}$"
|
||||
}
|
||||
}
|
||||
}
|
||||
21
test/shared/schemas/product-schema.json
Normal file
21
test/shared/schemas/product-schema.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "price"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 200
|
||||
},
|
||||
"price": {
|
||||
"$ref": "./price-schema.json"
|
||||
},
|
||||
"category": {
|
||||
"$ref": "./category-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
test/shared/schemas/test-schema.json
Normal file
15
test/shared/schemas/test-schema.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
22
test/shared/schemas/user-schema.json
Normal file
22
test/shared/schemas/user-schema.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["id", "name", "email"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"address": {
|
||||
"$ref": "./address-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user