Initial commit

This commit is contained in:
Luke Hagar
2025-10-01 20:01:42 +00:00
commit a02388f5c9
72 changed files with 3983 additions and 0 deletions

34
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();

View 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
View 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);
}
});
});

View 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"
}
}
}
}

View 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"
}
}
}
}

View 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
View 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"
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "category",
"in": "query",
"description": "Filter products by category",
"required": false,
"schema": {
"type": "string",
"minLength": 1
}
}

View 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
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "orderId",
"in": "path",
"description": "Order ID",
"required": true,
"schema": {
"type": "string",
"pattern": "^ORD-[0-9]{8}$"
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "page",
"in": "query",
"description": "Page number for pagination",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"default": 1
}
}

View 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"]
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "id",
"in": "path",
"description": "User ID",
"required": true,
"schema": {
"type": "integer",
"format": "int64",
"minimum": 1
}
}

View 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"
}
}
}
}

View 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"
}
}
}
}

View 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
}
}
}
}
}
}

View 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
}
}
}
}
}
}

View 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"
}
}
}
}
}
}
}

View 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"
}
}
}
}
}
}

View 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"
}
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"description": "Bad request error",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/error-schema.json"
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/error-schema.json"
}
}
}
}

View 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"
}
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
{
"description": "Order details",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "../schemas/order-schema.json"
}
}
}
}
}
}

View 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"
}
}
}
}
}
}

View 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"
}
}
}
}
}
}

View 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"
}
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"description": "Unauthorized access",
"content": {
"application/json": {
"schema": {
"$ref": "../schemas/error-schema.json"
}
}
}
}

View 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"
}
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
{
"description": "User details",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"$ref": "../schemas/user-schema.json"
}
}
}
}
}
}

View 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
View 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"
}
}
}
}
}
}
}
}
}
}
}
}

View 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"
}
}
}

View 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"
}
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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"
}
}
}
}
}

View 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"
}
}
}
}
}

View 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
}
}
}

View 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"
}
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View File

@@ -0,0 +1,15 @@
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"location": {
"$ref": "./address-schema.json"
}
}
}

View 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"
}
}
}
}
}
}
}

View 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"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
{
"description": "Test response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}

View 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"
}
}
}
}
}
}
}

View 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
}
}
}

View File

@@ -0,0 +1,15 @@
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
}
}
}

View 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"
}
}
}

View 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"
}
}
}

View 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}$"
}
}
}

View 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"
}
}
}

View File

@@ -0,0 +1,15 @@
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
}
}
}

View 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
View 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
}
}