Enhance .gitignore, update dependencies, and improve CLI functionality

- Expanded .gitignore to include additional build outputs, environment files, and IDE configurations.
- Updated bun.lock to include new dependencies: js-yaml and its types.
- Refactored index.ts to export new validation functions and types for better modularity.
- Updated package.json to reflect version bump to 1.0.3, added repository and homepage information, and improved script commands.
- Enhanced README.md with clearer usage instructions and examples for library and CLI usage.
- Improved CLI commands for validation, parsing, and reporting, including support for multiple sources and detailed output options.
- Added logging functionality throughout the codebase for better debugging and user feedback.
- Implemented recursive validation and reference analysis features for comprehensive OpenAPI specification validation.
This commit is contained in:
Luke Hagar
2025-10-01 22:56:10 +00:00
parent a02388f5c9
commit c4a250359f
16 changed files with 2328 additions and 301 deletions

155
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,155 @@
name: Publish to npm
on:
workflow_run:
workflows: ["Test"]
types: [completed]
branches: [main]
permissions:
contents: write # Needed to create tags/releases
packages: write # If you also publish GitHub Packages
id-token: write # Optional (for OIDC to cloud registries)
jobs:
publish:
if: >
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-latest
steps:
# Check out the exact commit that passed CI
- name: Checkout the successful commit
uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Use Node 20
uses: actions/setup-node@v5
with:
node-version: latest
registry-url: https://registry.npmjs.org
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install
run: bun install --frozen-lockfile
# Optional: Re-run build to ensure publish artifacts exist
- name: Build
run: bun run build
# Read current version from package.json
- name: Read current version
id: current-version
shell: bash
run: |
ver=$(node -p "require('./package.json').version")
echo "version=$ver" >> "$GITHUB_OUTPUT"
# Check if version was already bumped
- name: Check if version was bumped
id: version-check
shell: bash
run: |
# Get the latest GitHub release version using jq for reliable JSON parsing
LATEST_RELEASE=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r '.tag_name // empty' 2>/dev/null || echo "")
CURRENT_VERSION="${{ steps.current-version.outputs.version }}"
echo "Latest release: $LATEST_RELEASE"
echo "Current version: $CURRENT_VERSION"
# Remove 'v' prefix from release tag for comparison
if [ -n "$LATEST_RELEASE" ] && [ "$LATEST_RELEASE" != "null" ]; then
LATEST_VERSION=$(echo "$LATEST_RELEASE" | sed 's/^v//')
else
LATEST_VERSION="0.0.0"
fi
if [ "$LATEST_VERSION" != "$CURRENT_VERSION" ]; then
echo "Version was already bumped from $LATEST_VERSION to $CURRENT_VERSION"
echo "bumped=false" >> "$GITHUB_OUTPUT"
echo "final_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
else
echo "No version bump detected, will auto-patch bump"
echo "bumped=true" >> "$GITHUB_OUTPUT"
fi
# Auto-patch bump version if no version change was made
- name: Auto-patch bump version
if: steps.version-check.outputs.bumped == 'true'
id: bump-version
shell: bash
run: |
npm version patch --no-git-tag-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "Auto-bumped version to $NEW_VERSION"
# Set final version
- name: Set final version
id: final-version
shell: bash
run: |
if [ "${{ steps.version-check.outputs.bumped }}" = "true" ]; then
echo "version=${{ steps.bump-version.outputs.version }}" >> "$GITHUB_OUTPUT"
else
echo "version=${{ steps.current-version.outputs.version }}" >> "$GITHUB_OUTPUT"
fi
# Commit version bump if auto-bumped
- name: Commit auto-bumped version
if: steps.version-check.outputs.bumped == 'true'
shell: bash
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add package.json
git commit -m "chore: auto-bump version to ${{ steps.bump-version.outputs.version }}"
git push origin HEAD:main
# Create a git tag like v1.2.3 if it doesn't already exist
- name: Create tag if missing
shell: bash
run: |
TAG="v${{ steps.final-version.outputs.version }}"
# Check if tag exists locally
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists locally."
# Try to push it anyway (in case it's not on remote)
if git push origin "$TAG" 2>/dev/null; then
echo "Successfully pushed existing tag $TAG to remote."
else
echo "Tag $TAG already exists on remote as well."
fi
else
echo "Creating new tag $TAG"
git tag "$TAG" ${{ github.event.workflow_run.head_sha }}
if git push origin "$TAG" 2>/dev/null; then
echo "Successfully created and pushed tag $TAG"
else
echo "Tag $TAG already exists on remote, skipping push."
fi
fi
# Publish to npm (requires NPM_TOKEN in repo secrets)
- name: Publish to npm
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Create a GitHub Release for the tag
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.final-version.outputs.version }}
name: v${{ steps.final-version.outputs.version }}
generate_release_notes: true

29
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run tests
run: bun test
- name: Run linting
run: bun run lint

117
.gitignore vendored
View File

@@ -1,34 +1,101 @@
# dependencies (bun install)
node_modules
# Dependencies
node_modules/
bun.lockb
# output
out
dist
*.tgz
# Build outputs
dist/
build/
*.tsbuildinfo
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
# OS files
.DS_Store
Thumbs.db
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
bun-debug.log*
bun-error.log*
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output/
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Luke
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

364
README.md
View File

@@ -1,177 +1,303 @@
# 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).
A comprehensive OpenAPI parsing and validation library that supports both programmatic usage and command-line operations.
## 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
- 🔍 **Comprehensive Validation**: Validate OpenAPI 2.0, 3.0.x, and 3.1.x specifications
- 🔄 **Recursive Validation**: Validate all `$ref` references and detect circular dependencies
- 📊 **Rich Reporting**: Generate reports in JSON, YAML, HTML, and Markdown formats
- 🚀 **CLI & Library**: Use as both a command-line tool and a JavaScript/TypeScript library
- 🎯 **TypeScript Support**: Full TypeScript definitions included
- **Fast**: Built with Bun for optimal performance
- 🔧 **Flexible**: Support for custom validation rules and configurations
## Installation
```bash
bun install
# Using npm
npm install varsity
# Using yarn
yarn add varsity
# Using pnpm
pnpm add varsity
# Using bun
bun add varsity
```
## Usage
### Command Line Interface
### As a Library
#### Validate a specification
```bash
bun run src/cli.ts validate path/to/spec.json
```
#### Basic Validation
#### Parse without validation
```bash
bun run src/cli.ts parse path/to/spec.json
```
```javascript
import { validate, parse } from 'varsity';
#### 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
// Validate an OpenAPI specification
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
});
if (result.valid) {
console.log('✅ Specification is valid');
} else {
console.log('❌ Validation errors:', result.errors);
}
// Parse without validation
const parsed = await parse('path/to/spec.json');
console.log('Version:', parsed.version);
console.log('Title:', parsed.metadata.title);
```
#### Factory Pattern (For Configuration)
#### Advanced Validation
```typescript
import { createVarsity } from './src/varsity.js';
```javascript
import {
validate,
validateWithReferences,
createVarsity
} from 'varsity';
const varsity = createVarsity({
defaultVersion: '3.0.3',
strictMode: false,
customSchemas: {},
reportFormats: ['json']
// Validate with custom options
const result = await validate('spec.json', {
strict: true,
validateExamples: true,
validateReferences: true,
recursive: true,
maxRefDepth: 10
});
// Use the configured instance
const result = await varsity.validate('path/to/spec.json');
const report = await varsity.generateReport('path/to/spec.json', {
format: 'json',
// Recursive validation with reference resolution
const recursiveResult = await validateWithReferences('spec.json', {
strict: true,
validateExamples: true
});
// Create a configured instance
const varsity = createVarsity({
defaultVersion: '3.0',
strictMode: true,
reportFormats: ['json', 'html']
});
const result = await varsity.validate('spec.json');
```
#### Report Generation
```javascript
import { generateValidationReport, saveValidationReport } from 'varsity';
// Generate a report
const report = await generateValidationReport('spec.json', {
format: 'html',
includeWarnings: true,
includeMetadata: true
});
// Save report to file
await saveValidationReport('spec.json', {
format: 'json',
output: 'validation-report.json',
includeWarnings: true
});
```
#### Reference Analysis
```javascript
import { analyzeDocumentReferences, analyzeReferences } from 'varsity';
// Analyze references in a document
const analysis = await analyzeDocumentReferences('spec.json');
console.log('Total references:', analysis.totalReferences);
console.log('Circular references:', analysis.circularReferences);
// Find all references
const references = await analyzeReferences('spec.json');
```
### As a CLI Tool
#### Basic Commands
```bash
# Validate a specification
varsity validate spec.json
# Parse without validation
varsity parse spec.json
# Show supported OpenAPI versions
varsity info
```
#### Advanced Validation
```bash
# Strict validation with examples
varsity validate spec.json --strict --examples
# Recursive validation with references
varsity validate spec.json --recursive --references
# Verbose output
varsity validate spec.json --verbose
```
#### Report Generation
```bash
# Generate HTML report
varsity report spec.json --format html --output report.html
# Generate JSON report with warnings
varsity report spec.json --format json --warnings --metadata
```
#### Batch Processing
```bash
# Validate multiple specifications
varsity batch spec1.json spec2.json spec3.json
# Batch validation with JSON output
varsity batch *.json --json
```
#### Reference Analysis
```bash
# Analyze references
varsity analyze spec.json
# JSON output for analysis
varsity analyze spec.json --json
```
## 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[]`
#### `validate(source, options?, config?)`
Validates an OpenAPI specification.
#### Factory Function
- `createVarsity(config?: VarsityConfig)`: Creates a configured instance with methods
- `source`: Path, URL, or array of paths/URLs to OpenAPI specifications
- `options`: Validation options (optional)
- `config`: Varsity configuration (optional)
#### 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`
#### `parse(source)`
Parses an OpenAPI specification without validation.
### Types
- `source`: Path or URL to OpenAPI specification
- `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
#### `validateWithReferences(source, options?, config?)`
Recursively validates an OpenAPI specification and all its references.
### Type Safety
#### `validateMultipleWithReferences(sources, options?, config?)`
Validates multiple OpenAPI specifications with reference resolution.
Varsity leverages the comprehensive `oas-types` package for full TypeScript support:
### Validation Options
```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
interface ValidationOptions {
strict?: boolean; // Enable strict validation
validateExamples?: boolean; // Validate examples in the spec
validateReferences?: boolean; // Validate all references
recursive?: boolean; // Enable recursive validation
maxRefDepth?: number; // Maximum reference depth
customRules?: Record<string, any>; // Custom validation rules
}
```
## Development
### Report Options
### Running Tests
```bash
bun test
```typescript
interface ReportOptions {
format: 'json' | 'yaml' | 'html' | 'markdown';
output?: string; // Output file path
includeWarnings?: boolean; // Include warnings in report
includeMetadata?: boolean; // Include metadata in report
}
```
### Building
### Configuration
```typescript
interface VarsityConfig {
defaultVersion?: OpenAPIVersion;
strictMode?: boolean;
customSchemas?: Record<string, JSONSchemaType<any>>;
reportFormats?: ReportOptions['format'][];
}
```
## Supported OpenAPI Versions
- OpenAPI 2.0 (Swagger 2.0)
- OpenAPI 3.0.0, 3.0.1, 3.0.2, 3.0.3
- OpenAPI 3.1.0
## Development
### Prerequisites
- [Bun](https://bun.sh) (recommended) or Node.js 18+
- TypeScript 5+
### Setup
```bash
# Clone the repository
git clone https://github.com/luke/varsity.git
cd varsity
# Install dependencies
bun install
# Run tests
bun test
# Run linting
bun run lint
# Build the project
bun run build
```
### Linting
### Testing
```bash
bun run lint
# Run all tests
bun test
# Run tests in watch mode
bun test --watch
# Run specific test file
bun test test/basic.test.ts
```
## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
MIT
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Changelog
### 1.0.0
- Initial release
- Support for OpenAPI 2.0, 3.0.x, and 3.1.x
- CLI and library usage
- Recursive validation with reference resolution
- Multiple report formats
- TypeScript support

View File

@@ -8,10 +8,12 @@
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"commander": "^14.0.1",
"js-yaml": "^4.1.0",
"oas-types": "^1.0.6",
},
"devDependencies": {
"@types/bun": "latest",
"@types/js-yaml": "^4.0.9",
},
"peerDependencies": {
"typescript": "^5",
@@ -21,6 +23,8 @@
"packages": {
"@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="],
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
"@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=="],
@@ -29,6 +33,8 @@
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"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=="],
@@ -39,6 +45,8 @@
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"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=="],

View File

@@ -4,7 +4,9 @@ export {
parse,
generateValidationReport,
saveValidationReport,
validateMultiple,
validateWithReferences,
validateMultipleWithReferences,
analyzeDocumentReferences,
getSupportedVersions,
createVarsity,
// Individual module exports
@@ -13,6 +15,16 @@ export {
validateOpenAPISpec,
generateReport,
saveReport,
// Recursive validation exports
validateRecursively,
validateMultipleRecursively,
analyzeReferences,
// Reference resolver exports
resolveReference,
findReferences,
resolveAllReferences,
// Partial validation exports
validatePartialDocument,
} from "./src/varsity.js";
// Type exports
@@ -25,7 +37,14 @@ export type {
VarsityConfig,
OpenAPIVersion,
CLIResult,
RecursiveValidationResult,
} from "./src/types.js";
// Export types from other modules
export type {
ResolvedReference,
ReferenceContext,
} from "./src/ref-resolver.js";
// Default export - functional instance
export { default } from "./src/varsity.js";

View File

@@ -1,19 +1,32 @@
{
"name": "varsity",
"version": "1.0.0",
"version": "1.0.3",
"description": "Comprehensive OpenAPI parsing and validation library",
"module": "index.ts",
"type": "module",
"private": true,
"main": "./dist/index.js",
"module": "./dist/index.js",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./index.ts"
},
"./cli": {
"import": "./dist/cli.js",
"types": "./src/cli.ts"
}
},
"bin": {
"varsity": "./src/cli.ts"
"varsity": "dist/cli.js"
},
"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"
"build": "bun build index.ts --outdir dist --target node --format esm",
"build:cli": "bun build src/cli.ts --outdir dist --target node --format esm --outfile varsity",
"build:all": "bun run build && bun run build:cli",
"lint": "bun run --bun tsc --noEmit",
"prepublishOnly": "bun run build:all"
},
"keywords": [
"openapi",
@@ -21,12 +34,34 @@
"validation",
"parser",
"api",
"specification"
"specification",
"cli",
"typescript",
"bun",
"json-schema",
"api-validation",
"openapi-validator"
],
"author": "",
"author": "Luke",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/luke/varsity.git"
},
"homepage": "https://github.com/luke/varsity#readme",
"bugs": {
"url": "https://github.com/luke/varsity/issues"
},
"files": [
"index.ts",
"src/",
"dist/",
"README.md",
"LICENSE"
],
"devDependencies": {
"@types/bun": "latest"
"@types/bun": "latest",
"@types/js-yaml": "^4.0.9"
},
"peerDependencies": {
"typescript": "^5"
@@ -36,6 +71,7 @@
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"commander": "^14.0.1",
"js-yaml": "^4.1.0",
"oas-types": "^1.0.6"
}
}

304
src/cli.ts Normal file → Executable file
View File

@@ -5,12 +5,13 @@ import {
parse,
generateValidationReport,
saveValidationReport,
validateMultiple,
validateWithReferences,
validateMultipleWithReferences,
analyzeDocumentReferences,
generateSpecificationSummary,
getSupportedVersions,
createVarsity,
log,
} from "./varsity.js";
import type { ValidationOptions, ReportOptions } from "./types.js";
@@ -18,14 +19,19 @@ const program = new Command();
program
.name("varsity")
.description("Comprehensive OpenAPI parsing and validation library")
.description(
"Comprehensive OpenAPI parsing and validation library (supports JSON and YAML)"
)
.version("1.0.0");
// Validate command
program
.command("validate")
.description("Validate an OpenAPI specification")
.argument("<source>", "Path or URL to OpenAPI specification")
.description("Validate one or more OpenAPI specifications")
.argument(
"<sources...>",
"Path(s) or URL(s) to OpenAPI specification(s) (JSON or YAML)"
)
.option("-s, --strict", "Enable strict validation mode")
.option("-e, --examples", "Validate examples in the specification")
.option("-r, --references", "Validate all references")
@@ -36,7 +42,14 @@ program
"10"
)
.option("-v, --verbose", "Show detailed output")
.action(async (source: string, options: any) => {
.option("-j, --json", "Output as JSON")
.option("--no-progress", "Disable progress indicators")
.option("--no-colors", "Disable colored output")
.action(async (sources: string[], options: any) => {
// Configure logger based on options
log.setVerbose(options.verbose);
log.setShowProgress(!options.noProgress);
log.setUseColors(!options.noColors);
try {
const validationOptions: ValidationOptions = {
strict: options.strict,
@@ -46,13 +59,55 @@ program
maxRefDepth: parseInt(options.maxDepth) || 10,
};
// Handle single vs multiple sources
if (sources.length === 1) {
const source = sources[0];
if (!source) {
console.log("❌ No source provided");
process.exit(1);
}
let result;
if (options.recursive) {
result = await validateWithReferences(source, validationOptions);
if (result.valid) {
console.log("✅ Specification and all references are valid");
if (options.verbose) {
// Show summary if not in JSON mode
if (!options.json) {
try {
const { summary } = await generateSpecificationSummary(
source,
validationOptions
);
console.log("\n📊 Summary:");
console.log(` Version: ${summary.version}`);
console.log(` Paths: ${summary.paths}`);
console.log(` Endpoints: ${summary.endpoints}`);
console.log(` Components: ${summary.components}`);
console.log(` Schemas: ${summary.schemas}`);
console.log(` Total Documents: ${result.totalDocuments}`);
console.log(` Valid Documents: ${result.validDocuments}`);
console.log(
` References: ${summary.referenceAnalysis.totalReferences}`
);
console.log(
` Circular References: ${result.circularReferences.length}`
);
console.log(` Errors: ${result.errors.length}`);
console.log(` Warnings: ${result.warnings.length}`);
} catch (error) {
// Fallback to basic info if summary generation fails
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 if (options.verbose) {
console.log(`Version: ${result.version}`);
console.log(`Total documents: ${result.totalDocuments}`);
console.log(`Valid documents: ${result.validDocuments}`);
@@ -90,25 +145,62 @@ program
process.exit(1);
}
} else {
result = await validate(source, validationOptions);
result = await validate([source], validationOptions);
if (result.valid) {
// Handle both single result and array of results
const validationResult = Array.isArray(result) ? result[0] : result;
if (!validationResult) {
console.log("❌ No validation result received");
process.exit(1);
}
if (validationResult.valid) {
console.log("✅ Specification is valid");
if (options.verbose) {
console.log(`Version: ${result.version}`);
console.log(`Warnings: ${result.warnings.length}`);
// Show summary if not in JSON mode
if (!options.json) {
try {
const { summary } = await generateSpecificationSummary(
source,
validationOptions
);
console.log("\n📊 Summary:");
console.log(` Version: ${summary.version}`);
console.log(` Paths: ${summary.paths}`);
console.log(` Endpoints: ${summary.endpoints}`);
console.log(` Components: ${summary.components}`);
console.log(` Schemas: ${summary.schemas}`);
console.log(
` References: ${summary.referenceAnalysis.totalReferences}`
);
console.log(
` Circular References: ${summary.referenceAnalysis.circularReferences}`
);
console.log(` Errors: ${summary.validationResults.errors}`);
console.log(
` Warnings: ${summary.validationResults.warnings}`
);
} catch (error) {
// Fallback to basic info if summary generation fails
console.log(`Version: ${validationResult.version}`);
console.log(`Warnings: ${validationResult.warnings.length}`);
}
} else if (options.verbose) {
console.log(`Version: ${validationResult.version}`);
console.log(`Warnings: ${validationResult.warnings.length}`);
}
} else {
console.log("❌ Specification is invalid");
console.log(`Errors: ${result.errors.length}`);
console.log(`Errors: ${validationResult.errors.length}`);
for (const error of result.errors) {
for (const error of validationResult.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) {
if (options.verbose && validationResult.warnings.length > 0) {
console.log(`Warnings: ${validationResult.warnings.length}`);
for (const warning of validationResult.warnings) {
console.log(`${warning.path}: ${warning.message}`);
}
}
@@ -116,6 +208,54 @@ program
process.exit(1);
}
}
} else {
// Multiple sources - use batch validation logic
const results = await validateMultipleWithReferences(
sources,
validationOptions
);
if (options.json) {
console.log(JSON.stringify(results, null, 2));
} else {
console.log("📋 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");
if (options.verbose) {
console.log(` Version: ${result.version}`);
console.log(` Warnings: ${result.warnings.length}`);
}
validCount++;
} else {
console.log(" ❌ Invalid");
console.log(` Errors: ${result?.errors.length || 0}`);
if (options.verbose && result?.errors) {
for (const error of result.errors) {
console.log(`${error.path}: ${error.message}`);
}
}
errorCount++;
}
}
console.log("\n" + "=".repeat(50));
console.log(`Summary: ${validCount} valid, ${errorCount} invalid`);
if (errorCount > 0) {
process.exit(1);
}
}
}
} catch (error) {
console.error(
"❌ Validation failed:",
@@ -129,9 +269,15 @@ program
program
.command("parse")
.description("Parse an OpenAPI specification without validation")
.argument("<source>", "Path or URL to OpenAPI specification")
.argument("<source>", "Path or URL to OpenAPI specification (JSON or YAML)")
.option("-j, --json", "Output as JSON")
.option("--no-progress", "Disable progress indicators")
.option("--no-colors", "Disable colored output")
.action(async (source: string, options: any) => {
// Configure logger based on options
log.setVerbose(options.verbose);
log.setShowProgress(!options.noProgress);
log.setUseColors(!options.noColors);
try {
const parsed = await parse(source);
@@ -160,7 +306,7 @@ program
program
.command("report")
.description("Generate a validation report")
.argument("<source>", "Path or URL to OpenAPI specification")
.argument("<source>", "Path or URL to OpenAPI specification (JSON or YAML)")
.option(
"-f, --format <format>",
"Report format (json, yaml, html, markdown)",
@@ -207,66 +353,11 @@ program
}
});
// 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")
.argument("<source>", "Path or URL to OpenAPI specification (JSON or YAML)")
.option("-j, --json", "Output as JSON")
.action(async (source: string, options: any) => {
try {
@@ -305,6 +396,65 @@ program
}
});
// Summary command
program
.command("summary")
.description("Generate a comprehensive summary of an OpenAPI specification")
.argument("<source>", "Path or URL to OpenAPI specification (JSON or YAML)")
.option("-j, --json", "Output as JSON")
.option("-d, --detailed", "Show detailed summary")
.option("-s, --strict", "Enable strict validation mode")
.option("-e, --examples", "Validate examples in the specification")
.option("-r, --references", "Validate all references")
.option("--no-progress", "Disable progress indicators")
.option("--no-colors", "Disable colored output")
.action(async (source: string, options: any) => {
// Configure logger based on options
log.setVerbose(options.verbose);
log.setShowProgress(!options.noProgress);
log.setUseColors(!options.noColors);
try {
const validationOptions: ValidationOptions = {
strict: options.strict,
validateExamples: options.examples,
validateReferences: options.references,
};
const { summary, detailedSummary, jsonSummary } =
await generateSpecificationSummary(source, validationOptions);
if (options.json) {
console.log(jsonSummary);
} else if (options.detailed) {
console.log(detailedSummary);
} else {
console.log("📊 OpenAPI Specification Summary");
console.log("=".repeat(50));
console.log(`Version: ${summary.version}`);
console.log(`Title: ${summary.title || "N/A"}`);
console.log(`Paths: ${summary.paths}`);
console.log(`Endpoints: ${summary.endpoints}`);
console.log(`Components: ${summary.components}`);
console.log(`Schemas: ${summary.schemas}`);
console.log(`Valid: ${summary.validationResults.valid ? "Yes" : "No"}`);
if (summary.validationResults.errors > 0) {
console.log(`Errors: ${summary.validationResults.errors}`);
}
if (summary.validationResults.warnings > 0) {
console.log(`Warnings: ${summary.validationResults.warnings}`);
}
console.log("=".repeat(50));
}
} catch (error) {
console.error(
"❌ Summary generation failed:",
error instanceof Error ? error.message : "Unknown error"
);
process.exit(1);
}
});
// Info command
program
.command("info")
@@ -320,5 +470,7 @@ program
console.log("\nFor more information, visit: https://spec.openapis.org/");
});
// Parse command line arguments
program.parse();
// Only parse command line arguments if this file is being run directly
if (import.meta.main) {
program.parse();
}

443
src/logger.ts Normal file
View File

@@ -0,0 +1,443 @@
import { performance } from "perf_hooks";
export interface LogLevel {
ERROR: 0;
WARN: 1;
INFO: 2;
DEBUG: 3;
TRACE: 4;
}
export const LOG_LEVELS: LogLevel = {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3,
TRACE: 4,
};
export interface LoggerConfig {
level: keyof LogLevel;
verbose: boolean;
showTimestamps: boolean;
showProgress: boolean;
useColors: boolean;
}
export interface ProgressInfo {
current: number;
total: number;
label: string;
percentage: number;
}
export interface ValidationSummary {
filesProcessed: number;
schemasFound: number;
endpointsFound: number;
pathsFound: number;
componentsFound: number;
callbacksFound: number;
webhooksFound: number;
referencesFound: number;
circularReferences: number;
validationErrors: number;
validationWarnings: number;
processingTime: number;
}
export class Logger {
private config: LoggerConfig;
private startTime: number;
private operationStartTime: number;
private currentProgress: ProgressInfo | null = null;
constructor(config: Partial<LoggerConfig> = {}) {
this.config = {
level: "INFO",
verbose: false,
showTimestamps: true,
showProgress: true,
useColors: true,
...config,
};
this.startTime = performance.now();
this.operationStartTime = this.startTime;
}
private getTimestamp(): string {
if (!this.config.showTimestamps) return "";
const now = new Date();
return `[${now.toISOString().split("T")[1].split(".")[0]}] `;
}
private getColorCode(level: keyof LogLevel): string {
if (!this.config.useColors) return "";
const colors = {
ERROR: "\x1b[31m", // Red
WARN: "\x1b[33m", // Yellow
INFO: "\x1b[36m", // Cyan
DEBUG: "\x1b[35m", // Magenta
TRACE: "\x1b[90m", // Gray
};
return colors[level] || "";
}
private getResetColor(): string {
return this.config.useColors ? "\x1b[0m" : "";
}
private shouldLog(level: keyof LogLevel): boolean {
return LOG_LEVELS[level] <= LOG_LEVELS[this.config.level];
}
private formatMessage(
level: keyof LogLevel,
message: string,
data?: any
): string {
const timestamp = this.getTimestamp();
const color = this.getColorCode(level);
const reset = this.getResetColor();
const levelStr = level.padEnd(5);
let formatted = `${timestamp}${color}${levelStr}${reset} ${message}`;
if (data && this.config.verbose) {
formatted += `\n${JSON.stringify(data, null, 2)}`;
}
return formatted;
}
private log(level: keyof LogLevel, message: string, data?: any): void {
if (!this.shouldLog(level)) return;
const formatted = this.formatMessage(level, message, data);
console.log(formatted);
}
public error(message: string, data?: any): void {
this.log("ERROR", message, data);
}
public warn(message: string, data?: any): void {
this.log("WARN", message, data);
}
public info(message: string, data?: any): void {
this.log("INFO", message, data);
}
public debug(message: string, data?: any): void {
this.log("DEBUG", message, data);
}
public trace(message: string, data?: any): void {
this.log("TRACE", message, data);
}
public startOperation(operation: string): void {
this.operationStartTime = performance.now();
this.info(`🚀 Starting ${operation}`);
}
public endOperation(operation: string, success: boolean = true): void {
const duration = performance.now() - this.operationStartTime;
const status = success ? "✅" : "❌";
this.info(`${status} Completed ${operation} in ${duration.toFixed(2)}ms`);
}
public startProgress(total: number, label: string): void {
this.currentProgress = {
current: 0,
total,
label,
percentage: 0,
};
this.updateProgress();
}
public updateProgress(current: number): void {
if (!this.currentProgress) return;
this.currentProgress.current = current;
this.currentProgress.percentage = Math.round(
(current / this.currentProgress.total) * 100
);
this.updateProgress();
}
public updateProgress(): void {
if (!this.currentProgress || !this.config.showProgress) return;
const { current, total, label, percentage } = this.currentProgress;
const barLength = 20;
const filledLength = Math.round((percentage / 100) * barLength);
const bar = "█".repeat(filledLength) + "░".repeat(barLength - filledLength);
process.stdout.write(
`\r${this.getTimestamp()}${this.getColorCode(
"INFO"
)}PROGRESS${this.getResetColor()} ${label}: [${bar}] ${percentage}% (${current}/${total})`
);
if (current >= total) {
process.stdout.write("\n");
this.currentProgress = null;
}
}
public endProgress(): void {
if (this.currentProgress) {
this.updateProgress(this.currentProgress.total);
}
}
public step(step: string, details?: string): void {
const detailsStr = details ? ` - ${details}` : "";
this.info(`📋 ${step}${detailsStr}`);
}
public fileOperation(
operation: string,
filePath: string,
details?: string
): void {
const detailsStr = details ? ` (${details})` : "";
this.info(`📁 ${operation}: ${filePath}${detailsStr}`);
}
public parsingStep(step: string, details?: string): void {
const detailsStr = details ? ` - ${details}` : "";
this.info(`🔍 Parsing: ${step}${detailsStr}`);
}
public validationStep(step: string, details?: string): void {
const detailsStr = details ? ` - ${details}` : "";
this.info(`✅ Validation: ${step}${detailsStr}`);
}
public referenceStep(step: string, refPath: string, details?: string): void {
const detailsStr = details ? ` - ${details}` : "";
this.info(`🔗 Reference: ${step} ${refPath}${detailsStr}`);
}
public schemaStep(step: string, schemaName: string, details?: string): void {
const detailsStr = details ? ` - ${details}` : "";
this.info(`📋 Schema: ${step} ${schemaName}${detailsStr}`);
}
public endpointStep(
step: string,
method: string,
path: string,
details?: string
): void {
const detailsStr = details ? ` - ${details}` : "";
this.info(
`🌐 Endpoint: ${step} ${method.toUpperCase()} ${path}${detailsStr}`
);
}
public componentStep(
step: string,
componentType: string,
componentName: string,
details?: string
): void {
const detailsStr = details ? ` - ${details}` : "";
this.info(
`🧩 Component: ${step} ${componentType} ${componentName}${detailsStr}`
);
}
public webhookStep(
step: string,
webhookName: string,
details?: string
): void {
const detailsStr = details ? ` - ${details}` : "";
this.info(`🎣 Webhook: ${step} ${webhookName}${detailsStr}`);
}
public callbackStep(
step: string,
callbackName: string,
details?: string
): void {
const detailsStr = details ? ` - ${details}` : "";
this.info(`🔄 Callback: ${step} ${callbackName}${detailsStr}`);
}
public summary(summary: ValidationSummary): void {
const duration = performance.now() - this.startTime;
this.info("📊 Validation Summary");
this.info("=".repeat(50));
this.info(`Files Processed: ${summary.filesProcessed}`);
this.info(`Schemas Found: ${summary.schemasFound}`);
this.info(`Endpoints Found: ${summary.endpointsFound}`);
this.info(`Paths Found: ${summary.pathsFound}`);
this.info(`Components Found: ${summary.componentsFound}`);
this.info(`Callbacks Found: ${summary.callbacksFound}`);
this.info(`Webhooks Found: ${summary.webhooksFound}`);
this.info(`References Found: ${summary.referencesFound}`);
this.info(`Circular References: ${summary.circularReferences}`);
this.info(`Validation Errors: ${summary.validationErrors}`);
this.info(`Validation Warnings: ${summary.validationWarnings}`);
this.info(`Total Processing Time: ${duration.toFixed(2)}ms`);
this.info("=".repeat(50));
}
public detailedSummary(spec: any, version: string): void {
this.info("📋 Detailed Specification Summary");
this.info("=".repeat(50));
// Basic info
this.info(`OpenAPI Version: ${version}`);
this.info(`Title: ${spec.info?.title || "N/A"}`);
this.info(`Version: ${spec.info?.version || "N/A"}`);
this.info(`Description: ${spec.info?.description || "N/A"}`);
// Paths analysis
if (spec.paths) {
const paths = Object.keys(spec.paths);
this.info(`Total Paths: ${paths.length}`);
let totalEndpoints = 0;
const methods = new Set<string>();
for (const path of paths) {
const pathItem = spec.paths[path];
if (typeof pathItem === "object" && pathItem !== null) {
for (const [method, operation] of Object.entries(pathItem)) {
if (
typeof operation === "object" &&
operation !== null &&
"responses" in operation
) {
totalEndpoints++;
methods.add(method.toUpperCase());
}
}
}
}
this.info(`Total Endpoints: ${totalEndpoints}`);
this.info(`HTTP Methods Used: ${Array.from(methods).join(", ")}`);
}
// Components analysis
if (spec.components) {
this.info("Components Analysis:");
for (const [componentType, components] of Object.entries(
spec.components
)) {
if (typeof components === "object" && components !== null) {
const componentCount = Object.keys(components).length;
this.info(` ${componentType}: ${componentCount}`);
}
}
}
// Security analysis
if (spec.security) {
this.info(`Security Requirements: ${spec.security.length}`);
}
if (spec.components?.securitySchemes) {
const securitySchemes = Object.keys(spec.components.securitySchemes);
this.info(`Security Schemes: ${securitySchemes.length}`);
}
// Servers analysis
if (spec.servers) {
this.info(`Servers: ${spec.servers.length}`);
}
// Tags analysis
if (spec.tags) {
this.info(`Tags: ${spec.tags.length}`);
}
// External docs
if (spec.externalDocs) {
this.info("External Documentation: Yes");
}
this.info("=".repeat(50));
}
public setVerbose(verbose: boolean): void {
this.config.verbose = verbose;
}
public setShowProgress(showProgress: boolean): void {
this.config.showProgress = showProgress;
}
public setUseColors(useColors: boolean): void {
this.config.useColors = useColors;
}
public setLevel(level: keyof LogLevel): void {
this.config.level = level;
}
}
// Global logger instance
export const logger = new Logger();
// Export convenience functions
export const log = {
error: (message: string, data?: any) => logger.error(message, data),
warn: (message: string, data?: any) => logger.warn(message, data),
info: (message: string, data?: any) => logger.info(message, data),
debug: (message: string, data?: any) => logger.debug(message, data),
trace: (message: string, data?: any) => logger.trace(message, data),
step: (step: string, details?: string) => logger.step(step, details),
fileOperation: (operation: string, filePath: string, details?: string) =>
logger.fileOperation(operation, filePath, details),
parsingStep: (step: string, details?: string) =>
logger.parsingStep(step, details),
validationStep: (step: string, details?: string) =>
logger.validationStep(step, details),
referenceStep: (step: string, refPath: string, details?: string) =>
logger.referenceStep(step, refPath, details),
schemaStep: (step: string, schemaName: string, details?: string) =>
logger.schemaStep(step, schemaName, details),
endpointStep: (
step: string,
method: string,
path: string,
details?: string
) => logger.endpointStep(step, method, path, details),
componentStep: (
step: string,
componentType: string,
componentName: string,
details?: string
) => logger.componentStep(step, componentType, componentName, details),
webhookStep: (step: string, webhookName: string, details?: string) =>
logger.webhookStep(step, webhookName, details),
callbackStep: (step: string, callbackName: string, details?: string) =>
logger.callbackStep(step, callbackName, details),
summary: (summary: ValidationSummary) => logger.summary(summary),
detailedSummary: (spec: any, version: string) =>
logger.detailedSummary(spec, version),
startOperation: (operation: string) => logger.startOperation(operation),
endOperation: (operation: string, success: boolean) =>
logger.endOperation(operation, success),
startProgress: (total: number, label: string) =>
logger.startProgress(total, label),
updateProgress: (current: number) => logger.updateProgress(current),
endProgress: () => logger.endProgress(),
setVerbose: (verbose: boolean) => logger.setVerbose(verbose),
setShowProgress: (showProgress: boolean) =>
logger.setShowProgress(showProgress),
setUseColors: (useColors: boolean) => logger.setUseColors(useColors),
setLevel: (level: keyof LogLevel) => logger.setLevel(level),
};

View File

@@ -1,30 +1,47 @@
import { readFileSync } from "fs";
import { resolve } from "path";
import * as yaml from "js-yaml";
import type { ParsedSpec, OpenAPIVersion, OpenAPISpec } from "./types.js";
import type { OpenAPI2, OpenAPI3, OpenAPI3_1 } from "oas-types";
import { log } from "./logger.js";
/**
* Detect OpenAPI version from specification
*/
const detectVersion = (spec: any): OpenAPIVersion => {
log.parsingStep("Detecting OpenAPI version");
// Check for OpenAPI 3.x
if (spec.openapi) {
const version = spec.openapi;
log.parsingStep("Found OpenAPI 3.x specification", `Version: ${version}`);
if (version.startsWith("3.0")) {
log.parsingStep("Detected OpenAPI 3.0.x", version);
return version as OpenAPIVersion;
} else if (version.startsWith("3.1")) {
log.parsingStep("Detected OpenAPI 3.1.x", version);
return version as OpenAPIVersion;
} else if (version.startsWith("3.2")) {
log.parsingStep("Detected OpenAPI 3.2.x", version);
return version as OpenAPIVersion;
}
log.error("Unsupported OpenAPI version", { version });
throw new Error(`Unsupported OpenAPI version: ${version}`);
}
// Check for Swagger 2.0
if (spec.swagger === "2.0") {
log.parsingStep("Detected Swagger 2.0 specification");
return "2.0";
}
log.error("Unable to detect OpenAPI version", {
hasOpenapi: !!spec.openapi,
hasSwagger: !!spec.swagger,
openapiValue: spec.openapi,
swaggerValue: spec.swagger,
});
throw new Error(
'Unable to detect OpenAPI version. Specification must have "openapi" or "swagger" field.'
);
@@ -37,62 +54,132 @@ const extractMetadata = (
spec: OpenAPISpec,
version: OpenAPIVersion
): ParsedSpec["metadata"] => {
log.parsingStep("Extracting metadata from specification");
// All OpenAPI versions have the same info structure
const info = spec.info;
return {
const metadata = {
title: info?.title,
version: info?.version,
description: info?.description,
contact: info?.contact,
license: info?.license,
};
log.parsingStep(
"Metadata extracted",
`Title: ${metadata.title}, Version: ${
metadata.version
}, HasDescription: ${!!metadata.description}, HasContact: ${!!metadata.contact}, HasLicense: ${!!metadata.license}`
);
return metadata;
};
/**
* Parse an OpenAPI specification from a file path or URL
*/
export const parseOpenAPISpec = async (source: string): Promise<ParsedSpec> => {
log.startOperation("Parsing OpenAPI specification");
log.fileOperation("Reading specification", source);
let content: string;
let spec: any;
try {
// Handle file paths
if (source.startsWith("http://") || source.startsWith("https://")) {
log.parsingStep("Fetching remote specification", source);
const response = await fetch(source);
if (!response.ok) {
log.error("Failed to fetch remote specification", {
url: source,
status: response.status,
statusText: response.statusText,
});
throw new Error(
`Failed to fetch specification: ${response.statusText}`
);
}
content = await response.text();
log.parsingStep(
"Remote specification fetched",
`Size: ${content.length} characters`
);
} else {
// Local file
log.parsingStep("Reading local file", source);
const filePath = resolve(source);
log.fileOperation("Reading file", filePath);
content = readFileSync(filePath, "utf-8");
log.parsingStep("Local file read", `Size: ${content.length} characters`);
}
// Parse JSON or YAML
log.parsingStep("Determining content format");
if (content.trim().startsWith("{") || content.trim().startsWith("[")) {
log.parsingStep("Detected JSON format");
log.parsingStep("Parsing JSON content");
spec = JSON.parse(content);
log.parsingStep("JSON parsing completed");
} else {
// For YAML parsing, we'll use a simple approach or add yaml dependency later
// Parse YAML
log.parsingStep("Detected YAML format");
log.parsingStep("Parsing YAML content");
try {
spec = yaml.load(content);
log.parsingStep("YAML parsing completed");
} catch (yamlError) {
log.error("YAML parsing failed", {
error:
yamlError instanceof Error
? yamlError.message
: "Unknown YAML error",
});
throw new Error(
"YAML parsing not yet implemented. Please use JSON format."
`Failed to parse YAML: ${
yamlError instanceof Error
? yamlError.message
: "Unknown YAML error"
}`
);
}
}
const version = detectVersion(spec);
log.parsingStep("Version detection completed", `Detected: ${version}`);
// Type the spec based on the detected version
const typedSpec = spec as OpenAPISpec;
log.parsingStep("Specification typed", `Type: OpenAPISpec`);
return {
const metadata = extractMetadata(typedSpec, version);
const result = {
spec: typedSpec,
version,
source,
metadata: extractMetadata(typedSpec, version),
metadata,
};
log.endOperation("Parsing OpenAPI specification", true);
log.parsingStep(
"Parsing completed successfully",
`Version: ${version}, Source: ${source}, Title: ${
metadata.title
}, HasPaths: ${!!typedSpec.paths}, PathCount: ${
typedSpec.paths ? Object.keys(typedSpec.paths).length : 0
}`
);
return result;
} catch (error) {
log.error("Parsing failed", {
source,
error: error instanceof Error ? error.message : "Unknown error",
});
log.endOperation("Parsing OpenAPI specification", false);
throw new Error(
`Failed to parse OpenAPI specification: ${
error instanceof Error ? error.message : "Unknown error"
@@ -108,13 +195,32 @@ export const validateBasicStructure = (
spec: OpenAPISpec,
version: OpenAPIVersion
): boolean => {
log.parsingStep("Validating basic structure");
let isValid: boolean;
if (version === "2.0") {
log.parsingStep("Validating Swagger 2.0 structure");
const swaggerSpec = spec as OpenAPI2.Specification;
return !!(swaggerSpec.swagger && swaggerSpec.info && swaggerSpec.paths);
isValid = !!(swaggerSpec.swagger && swaggerSpec.info && swaggerSpec.paths);
log.parsingStep(
"Swagger 2.0 structure validation",
`HasSwagger: ${!!swaggerSpec.swagger}, HasInfo: ${!!swaggerSpec.info}, HasPaths: ${!!swaggerSpec.paths}, IsValid: ${isValid}`
);
} else {
log.parsingStep("Validating OpenAPI 3.x structure");
const openapiSpec = spec as
| OpenAPI3.Specification
| OpenAPI3_1.Specification;
return !!(openapiSpec.openapi && openapiSpec.info && openapiSpec.paths);
isValid = !!(openapiSpec.openapi && openapiSpec.info && openapiSpec.paths);
log.parsingStep(
"OpenAPI 3.x structure validation",
`HasOpenapi: ${!!openapiSpec.openapi}, HasInfo: ${!!openapiSpec.info}, HasPaths: ${!!openapiSpec.paths}, IsValid: ${isValid}`
);
}
log.parsingStep("Basic structure validation completed", `Valid: ${isValid}`);
return isValid;
};

View File

@@ -9,6 +9,7 @@ import type {
OpenAPIVersion,
OpenAPISpec,
} from "./types.js";
import { log } from "./logger.js";
export interface RecursiveValidationResult extends ValidationResult {
partialValidations: Array<{
@@ -45,6 +46,8 @@ export const validateRecursively = async (
options.maxRefDepth || 10
);
log.info(`🔗 Following ${resolvedRefs.length} references...`);
// Validate each resolved reference
const partialValidations: Array<{
path: string;
@@ -54,8 +57,12 @@ export const validateRecursively = async (
let validDocuments = rootValidation.valid ? 1 : 0;
for (const ref of resolvedRefs) {
for (let i = 0; i < resolvedRefs.length; i++) {
const ref = resolvedRefs[i];
if (!ref) continue;
if (ref.isCircular) {
log.info(`🔄 Circular reference: ${ref.path}`);
partialValidations.push({
path: ref.path,
result: {
@@ -67,7 +74,7 @@ export const validateRecursively = async (
},
],
warnings: [],
spec: null,
spec: {} as OpenAPISpec,
version: ref.version || "3.0",
},
isCircular: true,
@@ -97,6 +104,11 @@ export const validateRecursively = async (
if (partialResult.valid) {
validDocuments++;
log.info(`✅ Reference: ${ref.path}`);
} else {
log.info(
`❌ Reference: ${ref.path} (${partialResult.errors.length} errors)`
);
}
}
@@ -109,7 +121,7 @@ export const validateRecursively = async (
allWarnings.push(...partial.result.warnings);
}
return {
const result = {
valid:
rootValidation.valid && partialValidations.every((p) => p.result.valid),
errors: allErrors,
@@ -121,6 +133,8 @@ export const validateRecursively = async (
totalDocuments: 1 + partialValidations.length,
validDocuments,
};
return result;
};
/**
@@ -130,13 +144,37 @@ export const validateMultipleRecursively = async (
sources: string[],
options: ValidationOptions = {}
): Promise<RecursiveValidationResult[]> => {
log.startOperation("Multiple recursive validation");
log.validationStep(
"Starting batch validation",
`${sources.length} specifications`
);
const results: RecursiveValidationResult[] = [];
for (const source of sources) {
log.startProgress(sources.length, "Validating specifications");
for (let i = 0; i < sources.length; i++) {
const source = sources[i];
if (!source) continue;
log.updateProgress(i);
log.fileOperation(
"Processing specification",
source,
`${i + 1}/${sources.length}`
);
try {
const result = await validateRecursively(source, options);
results.push(result);
log.validationStep("Specification validated", `Valid: ${result.valid}`);
} catch (error) {
log.error("Specification validation failed", {
source,
error: error instanceof Error ? error.message : "Unknown error",
});
// Create error result for failed parsing
const errorResult: RecursiveValidationResult = {
valid: false,
@@ -149,7 +187,7 @@ export const validateMultipleRecursively = async (
},
],
warnings: [],
spec: null,
spec: {} as OpenAPISpec,
version: "3.0",
partialValidations: [],
circularReferences: [],
@@ -160,6 +198,16 @@ export const validateMultipleRecursively = async (
}
}
log.endProgress();
log.endOperation("Multiple recursive validation", true);
const validCount = results.filter((r) => r.valid).length;
const invalidCount = results.length - validCount;
log.validationStep(
"Batch validation completed",
`Valid: ${validCount}, Invalid: ${invalidCount}`
);
return results;
};
@@ -173,10 +221,17 @@ export const analyzeReferences = async (
circularReferences: string[];
totalReferences: number;
}> => {
log.startOperation("Analyzing references");
log.fileOperation("Analyzing references", source);
const parsed = await parseOpenAPISpec(source);
log.validationStep("Parsing completed for reference analysis");
const references = findReferences(parsed.spec);
log.validationStep("References found", `${references.length} total`);
// Check for circular references by analyzing reference paths
log.validationStep("Analyzing circular references");
const circularReferences: string[] = [];
const referenceMap = new Map<string, string[]>();
@@ -192,13 +247,26 @@ export const analyzeReferences = async (
for (const [refValue, paths] of referenceMap) {
if (paths.length > 1) {
// This is a potential circular reference
log.referenceStep(
"Circular reference detected",
refValue,
`${paths.length} occurrences`
);
circularReferences.push(refValue);
}
}
return {
const result = {
references,
circularReferences,
totalReferences: references.length,
};
log.endOperation("Analyzing references", true);
log.validationStep(
"Reference analysis completed",
`Total: ${result.totalReferences}, Circular: ${result.circularReferences.length}`
);
return result;
};

465
src/summary-analyzer.ts Normal file
View File

@@ -0,0 +1,465 @@
import type { OpenAPISpec, OpenAPIVersion } from "./types.js";
import type { OpenAPI2, OpenAPI3, OpenAPI3_1, OpenAPI3_2 } from "oas-types";
import { log } from "./logger.js";
export interface SpecificationSummary {
// Basic info
version: string;
title?: string;
description?: string;
// Counts
paths: number;
endpoints: number;
schemas: number;
components: number;
callbacks: number;
webhooks: number;
securitySchemes: number;
servers: number;
tags: number;
operations: number;
// HTTP methods used
httpMethods: string[];
// Component breakdown
componentBreakdown: {
schemas: number;
responses: number;
parameters: number;
examples: number;
requestBodies: number;
headers: number;
securitySchemes: number;
links: number;
callbacks: number;
pathItems: number;
};
// Security analysis
securityAnalysis: {
hasSecurity: boolean;
securitySchemes: number;
securityRequirements: number;
oauthFlows: number;
apiKeys: number;
httpAuth: number;
};
// Reference analysis
referenceAnalysis: {
totalReferences: number;
internalReferences: number;
externalReferences: number;
circularReferences: number;
};
// Validation results
validationResults: {
valid: boolean;
errors: number;
warnings: number;
processingTime: number;
};
}
/**
* Analyze an OpenAPI specification and generate a comprehensive summary
*/
export const analyzeSpecification = (
spec: OpenAPISpec,
version: OpenAPIVersion,
validationResults?: {
valid: boolean;
errors: number;
warnings: number;
processingTime: number;
}
): SpecificationSummary => {
log.validationStep("Analyzing specification for summary");
const summary: SpecificationSummary = {
version,
title: spec.info?.title,
description: spec.info?.description,
paths: 0,
endpoints: 0,
schemas: 0,
components: 0,
callbacks: 0,
webhooks: 0,
securitySchemes: 0,
servers: 0,
tags: 0,
operations: 0,
httpMethods: [],
componentBreakdown: {
schemas: 0,
responses: 0,
parameters: 0,
examples: 0,
requestBodies: 0,
headers: 0,
securitySchemes: 0,
links: 0,
callbacks: 0,
pathItems: 0,
},
securityAnalysis: {
hasSecurity: false,
securitySchemes: 0,
securityRequirements: 0,
oauthFlows: 0,
apiKeys: 0,
httpAuth: 0,
},
referenceAnalysis: {
totalReferences: 0,
internalReferences: 0,
externalReferences: 0,
circularReferences: 0,
},
validationResults: validationResults || {
valid: false,
errors: 0,
warnings: 0,
processingTime: 0,
},
};
// Analyze paths and endpoints
if (spec.paths) {
log.validationStep("Analyzing paths and endpoints");
summary.paths = Object.keys(spec.paths).length;
const methods = new Set<string>();
let endpointCount = 0;
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
) {
endpointCount++;
methods.add(method.toUpperCase());
log.endpointStep("Found endpoint", method, path);
}
}
}
}
summary.endpoints = endpointCount;
summary.httpMethods = Array.from(methods);
summary.operations = endpointCount;
log.validationStep(
"Path analysis completed",
`Paths: ${summary.paths}, Endpoints: ${summary.endpoints}`
);
}
// Analyze components (OpenAPI 3.x)
if (version.startsWith("3.") && (spec as any).components) {
log.validationStep("Analyzing components");
const components = (spec as any).components;
if (components.schemas) {
summary.componentBreakdown.schemas = Object.keys(
components.schemas
).length;
summary.schemas = summary.componentBreakdown.schemas;
}
if (components.responses) {
summary.componentBreakdown.responses = Object.keys(
components.responses
).length;
}
if (components.parameters) {
summary.componentBreakdown.parameters = Object.keys(
components.parameters
).length;
}
if (components.examples) {
summary.componentBreakdown.examples = Object.keys(
components.examples
).length;
}
if (components.requestBodies) {
summary.componentBreakdown.requestBodies = Object.keys(
components.requestBodies
).length;
}
if (components.headers) {
summary.componentBreakdown.headers = Object.keys(
components.headers
).length;
}
if (components.securitySchemes) {
summary.componentBreakdown.securitySchemes = Object.keys(
components.securitySchemes
).length;
summary.securitySchemes = summary.componentBreakdown.securitySchemes;
}
if (components.links) {
summary.componentBreakdown.links = Object.keys(components.links).length;
}
if (components.callbacks) {
summary.componentBreakdown.callbacks = Object.keys(
components.callbacks
).length;
summary.callbacks = summary.componentBreakdown.callbacks;
}
if (components.pathItems) {
summary.componentBreakdown.pathItems = Object.keys(
components.pathItems
).length;
}
// Calculate total components
summary.components = Object.values(summary.componentBreakdown).reduce(
(sum, count) => sum + count,
0
);
log.validationStep(
"Component analysis completed",
`Total components: ${summary.components}`
);
}
// Analyze webhooks (OpenAPI 3.1+)
if (version.startsWith("3.1") && (spec as any).webhooks) {
log.validationStep("Analyzing webhooks");
const webhooks = (spec as any).webhooks;
summary.webhooks = Object.keys(webhooks).length;
log.validationStep(
"Webhook analysis completed",
`Webhooks: ${summary.webhooks}`
);
}
// Analyze servers
if ((spec as any).servers) {
log.validationStep("Analyzing servers");
summary.servers = (spec as any).servers.length;
log.validationStep(
"Server analysis completed",
`Servers: ${summary.servers}`
);
}
// Analyze tags
if (spec.tags) {
log.validationStep("Analyzing tags");
summary.tags = spec.tags.length;
log.validationStep("Tag analysis completed", `Tags: ${summary.tags}`);
}
// Analyze security
log.validationStep("Analyzing security");
if (spec.security && spec.security.length > 0) {
summary.securityAnalysis.hasSecurity = true;
summary.securityAnalysis.securityRequirements = spec.security.length;
}
if (version.startsWith("3.") && (spec as any).components?.securitySchemes) {
const securitySchemes = (spec as any).components.securitySchemes;
summary.securityAnalysis.securitySchemes =
Object.keys(securitySchemes).length;
// Analyze security scheme types
for (const [name, scheme] of Object.entries(securitySchemes)) {
if (typeof scheme === "object" && scheme !== null) {
const schemeObj = scheme as any;
if (schemeObj.type === "oauth2") {
summary.securityAnalysis.oauthFlows++;
} else if (schemeObj.type === "apiKey") {
summary.securityAnalysis.apiKeys++;
} else if (schemeObj.type === "http") {
summary.securityAnalysis.httpAuth++;
}
}
}
} else if (version === "2.0" && (spec as any).securityDefinitions) {
const securityDefinitions = (spec as any).securityDefinitions;
summary.securityAnalysis.securitySchemes =
Object.keys(securityDefinitions).length;
}
log.validationStep(
"Security analysis completed",
JSON.stringify({
hasSecurity: summary.securityAnalysis.hasSecurity ? "Yes" : "No",
schemes: summary.securityAnalysis.securitySchemes,
requirements: summary.securityAnalysis.securityRequirements,
})
);
// Analyze references (basic count)
log.validationStep("Analyzing references");
const references = findReferencesInSpec(spec);
summary.referenceAnalysis.totalReferences = references.length;
summary.referenceAnalysis.internalReferences = references.filter((ref) =>
ref.startsWith("#/")
).length;
summary.referenceAnalysis.externalReferences = references.filter(
(ref) => !ref.startsWith("#/")
).length;
log.validationStep(
"Reference analysis completed",
JSON.stringify({
total: summary.referenceAnalysis.totalReferences,
internal: summary.referenceAnalysis.internalReferences,
external: summary.referenceAnalysis.externalReferences,
})
);
log.validationStep(
"Specification analysis completed",
JSON.stringify({
version: summary.version,
paths: summary.paths,
endpoints: summary.endpoints,
components: summary.components,
schemas: summary.schemas,
})
);
return summary;
};
/**
* Find all references in a specification
*/
const findReferencesInSpec = (obj: any, path = ""): string[] => {
const refs: string[] = [];
if (typeof obj === "object" && obj !== null) {
for (const [key, value] of Object.entries(obj)) {
if (key === "$ref" && typeof value === "string") {
refs.push(value);
} else if (typeof value === "object") {
refs.push(...findReferencesInSpec(value, path));
}
}
}
return refs;
};
/**
* Generate a detailed summary report
*/
export const generateDetailedSummary = (
summary: SpecificationSummary
): string => {
const lines: string[] = [];
lines.push("📊 OpenAPI Specification Summary");
lines.push("=".repeat(50));
// Basic information
lines.push(`📋 Basic Information`);
lines.push(` Version: ${summary.version}`);
lines.push(` Title: ${summary.title || "N/A"}`);
lines.push(` Description: ${summary.description ? "Yes" : "No"}`);
lines.push("");
// Paths and endpoints
lines.push(`🛣️ Paths & Endpoints`);
lines.push(` Total Paths: ${summary.paths}`);
lines.push(` Total Endpoints: ${summary.endpoints}`);
lines.push(` HTTP Methods: ${summary.httpMethods.join(", ")}`);
lines.push("");
// Components
lines.push(`🧩 Components`);
lines.push(` Total Components: ${summary.components}`);
lines.push(` Schemas: ${summary.componentBreakdown.schemas}`);
lines.push(` Responses: ${summary.componentBreakdown.responses}`);
lines.push(` Parameters: ${summary.componentBreakdown.parameters}`);
lines.push(` Examples: ${summary.componentBreakdown.examples}`);
lines.push(` Request Bodies: ${summary.componentBreakdown.requestBodies}`);
lines.push(` Headers: ${summary.componentBreakdown.headers}`);
lines.push(
` Security Schemes: ${summary.componentBreakdown.securitySchemes}`
);
lines.push(` Links: ${summary.componentBreakdown.links}`);
lines.push(` Callbacks: ${summary.componentBreakdown.callbacks}`);
lines.push(` Path Items: ${summary.componentBreakdown.pathItems}`);
lines.push("");
// Security
lines.push(`🔒 Security`);
lines.push(
` Has Security: ${summary.securityAnalysis.hasSecurity ? "Yes" : "No"}`
);
lines.push(` Security Schemes: ${summary.securityAnalysis.securitySchemes}`);
lines.push(
` Security Requirements: ${summary.securityAnalysis.securityRequirements}`
);
lines.push(` OAuth Flows: ${summary.securityAnalysis.oauthFlows}`);
lines.push(` API Keys: ${summary.securityAnalysis.apiKeys}`);
lines.push(` HTTP Auth: ${summary.securityAnalysis.httpAuth}`);
lines.push("");
// References
lines.push(`🔗 References`);
lines.push(
` Total References: ${summary.referenceAnalysis.totalReferences}`
);
lines.push(
` Internal References: ${summary.referenceAnalysis.internalReferences}`
);
lines.push(
` External References: ${summary.referenceAnalysis.externalReferences}`
);
lines.push(
` Circular References: ${summary.referenceAnalysis.circularReferences}`
);
lines.push("");
// Additional features
lines.push(`🌐 Additional Features`);
lines.push(` Servers: ${summary.servers}`);
lines.push(` Tags: ${summary.tags}`);
lines.push(` Webhooks: ${summary.webhooks}`);
lines.push("");
// Validation results
lines.push(`✅ Validation Results`);
lines.push(` Valid: ${summary.validationResults.valid ? "Yes" : "No"}`);
lines.push(` Errors: ${summary.validationResults.errors}`);
lines.push(` Warnings: ${summary.validationResults.warnings}`);
lines.push(
` Processing Time: ${summary.validationResults.processingTime.toFixed(
2
)}ms`
);
lines.push("");
lines.push("=".repeat(50));
return lines.join("\n");
};
/**
* Generate a JSON summary report
*/
export const generateJSONSummary = (summary: SpecificationSummary): string => {
return JSON.stringify(summary, null, 2);
};

View File

@@ -11,6 +11,7 @@ import type {
import { allSchemas } from "oas-types/schemas";
import type { OpenAPI3_1, OpenAPI3_2, OpenAPI3, OpenAPI2 } from "oas-types";
import { log } from "./logger.js";
// Initialize AJV instance
const createAjvInstance = (): Ajv => {
@@ -55,6 +56,8 @@ const findReferences = (
obj: OpenAPISpec,
path = ""
): Array<{ path: string; value: string }> => {
log.validationStep("Finding references in specification");
const refs: Array<{ path: string; value: string }> = [];
if (typeof obj === "object" && obj !== null) {
@@ -62,6 +65,7 @@ const findReferences = (
const currentPath = path ? `${path}.${key}` : key;
if (key === "$ref" && typeof value === "string") {
log.referenceStep("Found reference", value, `at ${currentPath}`);
refs.push({ path: currentPath, value });
} else if (typeof value === "object") {
refs.push(...findReferences(value, currentPath));
@@ -69,6 +73,10 @@ const findReferences = (
}
}
log.validationStep(
"Reference search completed",
`Found ${refs.length} references`
);
return refs;
};
@@ -79,6 +87,8 @@ const resolveReference = (
spec: OpenAPISpec,
ref: { path: string; value: string }
): boolean => {
log.referenceStep("Resolving reference", ref.value, `from ${ref.path}`);
// Simple reference resolution - in a real implementation, this would be more comprehensive
if (ref.value.startsWith("#/")) {
const path = ref.value.substring(2).split("/");
@@ -87,14 +97,30 @@ const resolveReference = (
for (const segment of path) {
if (current && typeof current === "object" && segment in current) {
current = (current as any)[segment];
log.referenceStep(
"Traversing path segment",
segment,
`current type: ${typeof current}`
);
} else {
log.referenceStep(
"Reference resolution failed",
`segment '${segment}' not found`
);
return false;
}
}
return current !== undefined;
const isValid = current !== undefined;
log.referenceStep(
"Reference resolution completed",
ref.value,
`Valid: ${isValid}`
);
return isValid;
}
log.referenceStep("External reference not supported", ref.value);
return false; // External references not supported in this simple implementation
};
@@ -107,29 +133,43 @@ const performStrictValidation = (
errors: ValidationError[],
warnings: ValidationError[]
): void => {
log.validationStep("Performing strict validation checks");
// Check for required fields based on version
if (version === "2.0") {
log.validationStep("Validating Swagger 2.0 strict requirements");
const swaggerSpec = spec as OpenAPI2.Specification;
if (!swaggerSpec.host) {
log.validationStep("Missing host field in Swagger 2.0");
errors.push({
path: "/",
message: 'Either "host" or "servers" must be specified in Swagger 2.0',
});
} else {
log.validationStep("Host field found in Swagger 2.0", swaggerSpec.host);
}
} else {
log.validationStep("Validating OpenAPI 3.x strict requirements");
const openapiSpec = spec as
| OpenAPI3.Specification
| OpenAPI3_1.Specification
| OpenAPI3_2.Specification;
if (!openapiSpec.servers || openapiSpec.servers.length === 0) {
log.validationStep("No servers specified in OpenAPI 3.x");
warnings.push({
path: "/",
message: "No servers specified. Consider adding at least one server.",
});
} else {
log.validationStep(
"Servers found in OpenAPI 3.x",
`${openapiSpec.servers.length} servers`
);
}
}
// Check for security definitions
log.validationStep("Validating security definitions");
if (version === "2.0") {
const swaggerSpec = spec as OpenAPI2.Specification;
if (
@@ -137,10 +177,16 @@ const performStrictValidation = (
swaggerSpec.security.length > 0 &&
!swaggerSpec.securityDefinitions
) {
log.validationStep("Security used without definitions in Swagger 2.0");
errors.push({
path: "/",
message: "Security schemes must be defined when using security",
});
} else if (swaggerSpec.securityDefinitions) {
log.validationStep(
"Security definitions found in Swagger 2.0",
`${Object.keys(swaggerSpec.securityDefinitions).length} schemes`
);
}
} else {
const openapiSpec = spec as
@@ -152,12 +198,23 @@ const performStrictValidation = (
openapiSpec.security.length > 0 &&
!openapiSpec.components?.securitySchemes
) {
log.validationStep("Security used without schemes in OpenAPI 3.x");
errors.push({
path: "/",
message: "Security schemes must be defined when using security",
});
} else if (openapiSpec.components?.securitySchemes) {
log.validationStep(
"Security schemes found in OpenAPI 3.x",
`${Object.keys(openapiSpec.components.securitySchemes).length} schemes`
);
}
}
log.validationStep(
"Strict validation completed",
`Errors: ${errors.length}, Warnings: ${warnings.length}`
);
};
/**
@@ -169,9 +226,12 @@ const validateExamples = (
errors: ValidationError[],
warnings: ValidationError[]
): void => {
// This would implement example validation logic
// For now, just a placeholder
log.validationStep("Validating examples in specification");
let exampleCount = 0;
if (spec.paths) {
log.validationStep("Analyzing examples in paths");
for (const [path, pathItem] of Object.entries(spec.paths)) {
if (typeof pathItem === "object" && pathItem !== null) {
for (const [method, operation] of Object.entries(pathItem)) {
@@ -180,6 +240,7 @@ const validateExamples = (
operation !== null &&
"responses" in operation
) {
log.endpointStep("Validating examples", method, path);
// Check response examples
for (const [statusCode, response] of Object.entries(
operation.responses
@@ -189,6 +250,11 @@ const validateExamples = (
response !== null &&
"examples" in response
) {
log.validationStep(
"Found examples in response",
`${method} ${path} ${statusCode}`
);
exampleCount++;
// Validate examples here
}
}
@@ -197,6 +263,11 @@ const validateExamples = (
}
}
}
log.validationStep(
"Example validation completed",
`Found ${exampleCount} examples`
);
};
/**
@@ -208,17 +279,31 @@ const validateReferences = (
errors: ValidationError[],
warnings: ValidationError[]
): void => {
log.validationStep("Validating references in specification");
// This would implement reference validation logic
// Check for broken $ref references
const refs = findReferences(spec);
let validRefs = 0;
let brokenRefs = 0;
for (const ref of refs) {
if (!resolveReference(spec, ref)) {
log.referenceStep("Broken reference found", ref.value, `at ${ref.path}`);
errors.push({
path: ref.path,
message: `Broken reference: ${ref.value}`,
});
brokenRefs++;
} else {
validRefs++;
}
}
log.validationStep(
"Reference validation completed",
`Valid: ${validRefs}, Broken: ${brokenRefs}`
);
};
/**
@@ -229,21 +314,36 @@ export const validateOpenAPISpec = (
version: OpenAPIVersion,
options: ValidationOptions = {}
): ValidationResult => {
log.startOperation("Validating OpenAPI specification");
log.validationStep("Initializing validation", `Version: ${version}`);
const normalizedVersion = normalizeVersion(version);
log.validationStep("Normalized version", normalizedVersion);
const schema = schemas.get(normalizedVersion);
if (!schema) {
log.error("No schema available for version", {
version,
normalizedVersion,
});
throw new Error(
`No schema available for OpenAPI version: ${version} (normalized to ${normalizedVersion})`
);
}
log.validationStep("Compiling schema for validation");
const validate = ajv.compile(schema);
log.validationStep("Running schema validation");
const valid = validate(spec);
const errors: ValidationError[] = [];
const warnings: ValidationError[] = [];
if (!valid && validate.errors) {
log.validationStep(
"Schema validation found errors",
`${validate.errors.length} errors`
);
for (const error of validate.errors) {
const validationError: ValidationError = {
path: error.instancePath || error.schemaPath || "/",
@@ -253,31 +353,52 @@ export const validateOpenAPISpec = (
};
if (error.keyword === "required" || error.keyword === "type") {
log.validationStep(
"Schema validation error",
`${error.keyword}: ${validationError.message}`
);
errors.push(validationError);
} else {
log.validationStep(
"Schema validation warning",
`${error.keyword}: ${validationError.message}`
);
warnings.push(validationError);
}
}
} else {
log.validationStep("Schema validation passed");
}
// Additional custom validations
if (options.strict) {
log.validationStep("Running strict validation");
performStrictValidation(spec, version, errors, warnings);
}
if (options.validateExamples) {
log.validationStep("Running example validation");
validateExamples(spec, version, errors, warnings);
}
if (options.validateReferences) {
log.validationStep("Running reference validation");
validateReferences(spec, version, errors, warnings);
}
return {
const result = {
valid: errors.length === 0,
errors,
warnings,
spec,
version,
};
log.endOperation("Validating OpenAPI specification", result.valid);
log.validationStep(
"Validation completed",
`Valid: ${result.valid}, Errors: ${errors.length}, Warnings: ${warnings.length}`
);
return result;
};

View File

@@ -6,6 +6,12 @@ import {
validateMultipleRecursively,
analyzeReferences,
} from "./recursive-validator.js";
import {
analyzeSpecification,
generateDetailedSummary,
generateJSONSummary,
} from "./summary-analyzer.js";
import { log } from "./logger.js";
import type {
ParsedSpec,
ValidationResult,
@@ -36,11 +42,26 @@ export const validate = async (
if (Array.isArray(source)) {
const results: ValidationResult[] = [];
for (const singleSource of source) {
for (let i = 0; i < source.length; i++) {
const singleSource = source[i];
if (!singleSource) continue;
log.info(`📄 Parsing: ${singleSource}`);
try {
const result = await validateSingle(singleSource, options, config);
results.push(result);
log.info(
`✅ Validated: ${singleSource} - ${
result.valid ? "Valid" : "Invalid"
}`
);
} catch (error) {
log.error(
`❌ Failed: ${singleSource} - ${
error instanceof Error ? error.message : "Unknown error"
}`
);
// Create error result for failed parsing
const errorResult: ValidationResult = {
valid: false,
@@ -60,11 +81,19 @@ export const validate = async (
}
}
const validCount = results.filter((r) => r.valid).length;
const invalidCount = results.length - validCount;
log.info(`📊 Summary: ${validCount} valid, ${invalidCount} invalid`);
return results;
}
// Single specification validation
return validateSingle(source, options, config);
log.info(`📄 Parsing: ${source}`);
const result = await validateSingle(source, options, config);
log.info(`✅ Validated: ${source} - ${result.valid ? "Valid" : "Invalid"}`);
return result;
};
/**
@@ -92,6 +121,7 @@ const validateSingle = async (
source,
validationOptions
);
return {
valid: recursiveResult.valid,
errors: recursiveResult.errors,
@@ -101,14 +131,33 @@ const validateSingle = async (
};
}
return validateOpenAPISpec(parsed.spec, parsed.version, validationOptions);
const result = validateOpenAPISpec(
parsed.spec,
parsed.version,
validationOptions
);
return result;
};
/**
* Parse an OpenAPI specification without validation
*/
export const parse = async (source: string): Promise<ParsedSpec> => {
return parseOpenAPISpec(source);
log.startOperation("Parsing OpenAPI specification");
log.fileOperation("Parsing specification", source);
const result = await parseOpenAPISpec(source);
log.endOperation("Parsing OpenAPI specification", true);
log.validationStep(
"Parsing completed",
`Version: ${result.version}, Title: ${
result.metadata.title
}, HasPaths: ${!!result.spec.paths}`
);
return result;
};
/**
@@ -120,9 +169,23 @@ export const generateValidationReport = async (
validationOptions: ValidationOptions = {},
config: VarsityConfig = defaultConfig
): Promise<string> => {
log.startOperation("Generating validation report");
log.fileOperation("Generating report", source);
const result = await validate(source, validationOptions, config);
// Since source is a string, result will be ValidationResult, not ValidationResult[]
return generateReport(result as ValidationResult, reportOptions);
const validationResult = result as ValidationResult;
log.validationStep("Generating report", `Format: ${reportOptions.format}`);
const report = generateReport(validationResult, reportOptions);
log.endOperation("Generating validation report", true);
log.validationStep(
"Report generated",
`Format: ${reportOptions.format}, Valid: ${validationResult.valid}, Errors: ${validationResult.errors.length}, Warnings: ${validationResult.warnings.length}`
);
return report;
};
/**
@@ -173,7 +236,89 @@ export const validateMultipleWithReferences = async (
* Analyze references in an OpenAPI specification
*/
export const analyzeDocumentReferences = async (source: string) => {
return analyzeReferences(source);
log.startOperation("Analyzing document references");
log.fileOperation("Analyzing references", source);
const result = await analyzeReferences(source);
log.endOperation("Analyzing document references", true);
log.validationStep(
"Reference analysis completed",
`Total: ${result.totalReferences}, Circular: ${result.circularReferences.length}`
);
return result;
};
/**
* Generate a comprehensive summary of an OpenAPI specification
*/
export const generateSpecificationSummary = async (
source: string,
validationOptions: ValidationOptions = {},
config: VarsityConfig = defaultConfig
): Promise<{
summary: any;
detailedSummary: string;
jsonSummary: string;
}> => {
log.startOperation("Generating specification summary");
log.fileOperation("Generating summary", source);
// Parse the specification
const parsed = await parseOpenAPISpec(source);
log.validationStep(
"Specification parsed for summary",
`Version: ${parsed.version}`
);
// Validate if requested
let validationResults;
if (
validationOptions.strict ||
validationOptions.validateExamples ||
validationOptions.validateReferences
) {
log.validationStep("Running validation for summary");
const validation = await validate(source, validationOptions, config);
const result = Array.isArray(validation) ? validation[0] : validation;
if (result) {
validationResults = {
valid: result.valid,
errors: result.errors.length,
warnings: result.warnings.length,
processingTime: 0, // This would be calculated from actual timing
};
}
}
// Analyze the specification
log.validationStep("Analyzing specification structure");
const summary = analyzeSpecification(
parsed.spec,
parsed.version,
validationResults
);
// Generate detailed summary
log.validationStep("Generating detailed summary");
const detailedSummary = generateDetailedSummary(summary);
// Generate JSON summary
log.validationStep("Generating JSON summary");
const jsonSummary = generateJSONSummary(summary);
log.endOperation("Generating specification summary", true);
log.validationStep(
"Summary generation completed",
`Version: ${summary.version}, Paths: ${summary.paths}, Endpoints: ${summary.endpoints}, Components: ${summary.components}, Valid: ${summary.validationResults.valid}`
);
return {
summary,
detailedSummary,
jsonSummary,
};
};
/**
@@ -216,6 +361,23 @@ export const createVarsity = (config: VarsityConfig = {}) => {
export { parseOpenAPISpec, validateBasicStructure } from "./parser.js";
export { validateOpenAPISpec } from "./validator.js";
export { generateReport, saveReport } from "./reporter.js";
export {
validateRecursively,
validateMultipleRecursively,
analyzeReferences,
} from "./recursive-validator.js";
export {
resolveReference,
findReferences,
resolveAllReferences,
} from "./ref-resolver.js";
export { validatePartialDocument } from "./partial-validator.js";
export {
analyzeSpecification,
generateDetailedSummary,
generateJSONSummary,
} from "./summary-analyzer.js";
export { log, Logger } from "./logger.js";
export type {
ParsedSpec,
@@ -226,7 +388,11 @@ export type {
VarsityConfig,
OpenAPIVersion,
CLIResult,
RecursiveValidationResult,
} from "./types.js";
// Export types from other modules
export type { ResolvedReference, ReferenceContext } from "./ref-resolver.js";
// Default export - create a default instance
export default createVarsity();

45
test/sample-openapi.yaml Normal file
View File

@@ -0,0 +1,45 @@
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
/products:
get:
summary: Get all products
responses:
"200":
description: Successful response
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
price:
type: number
format: float