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