mirror of
https://github.com/LukeHagar/varsity.git
synced 2025-12-06 04:22:00 +00:00
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:
155
.github/workflows/publish.yml
vendored
Normal file
155
.github/workflows/publish.yml
vendored
Normal 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
29
.github/workflows/test.yml
vendored
Normal 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
117
.gitignore
vendored
@@ -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
21
LICENSE
Normal 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
364
README.md
@@ -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
|
||||
8
bun.lock
8
bun.lock
@@ -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=="],
|
||||
|
||||
21
index.ts
21
index.ts
@@ -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";
|
||||
|
||||
54
package.json
54
package.json
@@ -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
304
src/cli.ts
Normal file → Executable 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
443
src/logger.ts
Normal 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),
|
||||
};
|
||||
120
src/parser.ts
120
src/parser.ts
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
465
src/summary-analyzer.ts
Normal 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);
|
||||
};
|
||||
129
src/validator.ts
129
src/validator.ts
@@ -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;
|
||||
};
|
||||
|
||||
178
src/varsity.ts
178
src/varsity.ts
@@ -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
45
test/sample-openapi.yaml
Normal 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
|
||||
Reference in New Issue
Block a user