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)
|
# Dependencies
|
||||||
node_modules
|
node_modules/
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
# output
|
# Build outputs
|
||||||
out
|
dist/
|
||||||
dist
|
build/
|
||||||
*.tgz
|
*.tsbuildinfo
|
||||||
|
|
||||||
# code coverage
|
# Environment files
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# logs
|
|
||||||
logs
|
|
||||||
_.log
|
|
||||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
.env
|
||||||
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.local
|
|
||||||
|
|
||||||
# caches
|
# IDE files
|
||||||
.eslintcache
|
.vscode/
|
||||||
.cache
|
.idea/
|
||||||
*.tsbuildinfo
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
# IntelliJ based IDEs
|
# OS files
|
||||||
.idea
|
|
||||||
|
|
||||||
# Finder (MacOS) folder config
|
|
||||||
.DS_Store
|
.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
|
# 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
|
## Features
|
||||||
|
|
||||||
- 🔍 **Multi-version Support**: Validates OpenAPI 2.0 (Swagger) and OpenAPI 3.0.x/3.1.0 specifications
|
- 🔍 **Comprehensive Validation**: Validate OpenAPI 2.0, 3.0.x, and 3.1.x specifications
|
||||||
- 📊 **Comprehensive Reporting**: Generate reports in JSON, YAML, HTML, and Markdown formats
|
- 🔄 **Recursive Validation**: Validate all `$ref` references and detect circular dependencies
|
||||||
- 🚀 **High Performance**: Built with Bun for fast execution
|
- 📊 **Rich Reporting**: Generate reports in JSON, YAML, HTML, and Markdown formats
|
||||||
- 🛠️ **Flexible Usage**: Use as a CLI tool or import as a library
|
- 🚀 **CLI & Library**: Use as both a command-line tool and a JavaScript/TypeScript library
|
||||||
- ✅ **AJV Integration**: Robust validation using the industry-standard AJV library
|
- 🎯 **TypeScript Support**: Full TypeScript definitions included
|
||||||
- 📝 **Detailed Error Reporting**: Clear error messages with path information
|
- ⚡ **Fast**: Built with Bun for optimal performance
|
||||||
- 🔧 **Extensible**: Support for custom validation rules and schemas
|
- 🔧 **Flexible**: Support for custom validation rules and configurations
|
||||||
- 🎯 **Type Safety**: Full TypeScript support with comprehensive OpenAPI type definitions from `oas-types`
|
|
||||||
- 📋 **Comprehensive Schemas**: Uses official JSON schemas for accurate validation
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun install
|
# Using npm
|
||||||
|
npm install varsity
|
||||||
|
|
||||||
|
# Using yarn
|
||||||
|
yarn add varsity
|
||||||
|
|
||||||
|
# Using pnpm
|
||||||
|
pnpm add varsity
|
||||||
|
|
||||||
|
# Using bun
|
||||||
|
bun add varsity
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Command Line Interface
|
### As a Library
|
||||||
|
|
||||||
#### Validate a specification
|
#### Basic Validation
|
||||||
```bash
|
|
||||||
bun run src/cli.ts validate path/to/spec.json
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Parse without validation
|
```javascript
|
||||||
```bash
|
import { validate, parse } from 'varsity';
|
||||||
bun run src/cli.ts parse path/to/spec.json
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Generate a report
|
// Validate an OpenAPI specification
|
||||||
```bash
|
|
||||||
bun run src/cli.ts report path/to/spec.json --format html --output report.html
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Batch validation
|
|
||||||
```bash
|
|
||||||
bun run src/cli.ts batch spec1.json spec2.json spec3.json
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Show supported versions
|
|
||||||
```bash
|
|
||||||
bun run src/cli.ts info
|
|
||||||
```
|
|
||||||
|
|
||||||
### CLI Options
|
|
||||||
|
|
||||||
#### Validate Command
|
|
||||||
- `-s, --strict`: Enable strict validation mode
|
|
||||||
- `-e, --examples`: Validate examples in the specification
|
|
||||||
- `-r, --references`: Validate all references
|
|
||||||
- `-v, --verbose`: Show detailed output
|
|
||||||
|
|
||||||
#### Report Command
|
|
||||||
- `-f, --format <format>`: Report format (json, yaml, html, markdown)
|
|
||||||
- `-o, --output <file>`: Output file path
|
|
||||||
- `-w, --warnings`: Include warnings in report
|
|
||||||
- `-m, --metadata`: Include metadata in report
|
|
||||||
|
|
||||||
### Programmatic Usage
|
|
||||||
|
|
||||||
#### Functional Approach (Recommended)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { validate, parse, generateValidationReport } from './src/varsity.js';
|
|
||||||
|
|
||||||
// Parse and validate
|
|
||||||
const result = await validate('path/to/spec.json');
|
const result = await validate('path/to/spec.json');
|
||||||
|
if (result.valid) {
|
||||||
// Generate a report
|
console.log('✅ Specification is valid');
|
||||||
const report = await generateValidationReport('path/to/spec.json', {
|
} else {
|
||||||
format: 'json',
|
console.log('❌ Validation errors:', result.errors);
|
||||||
includeWarnings: true,
|
}
|
||||||
includeMetadata: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse without validation
|
// Parse without validation
|
||||||
const parsed = await parse('path/to/spec.json');
|
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
|
```javascript
|
||||||
import { createVarsity } from './src/varsity.js';
|
import {
|
||||||
|
validate,
|
||||||
|
validateWithReferences,
|
||||||
|
createVarsity
|
||||||
|
} from 'varsity';
|
||||||
|
|
||||||
const varsity = createVarsity({
|
// Validate with custom options
|
||||||
defaultVersion: '3.0.3',
|
const result = await validate('spec.json', {
|
||||||
strictMode: false,
|
strict: true,
|
||||||
customSchemas: {},
|
validateExamples: true,
|
||||||
reportFormats: ['json']
|
validateReferences: true,
|
||||||
|
recursive: true,
|
||||||
|
maxRefDepth: 10
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use the configured instance
|
// Recursive validation with reference resolution
|
||||||
const result = await varsity.validate('path/to/spec.json');
|
const recursiveResult = await validateWithReferences('spec.json', {
|
||||||
const report = await varsity.generateReport('path/to/spec.json', {
|
strict: true,
|
||||||
format: 'json',
|
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,
|
includeWarnings: true,
|
||||||
includeMetadata: 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
|
## API Reference
|
||||||
|
|
||||||
### Core Functions
|
### Core Functions
|
||||||
|
|
||||||
#### Direct Functions
|
#### `validate(source, options?, config?)`
|
||||||
- `validate(source: string, options?: ValidationOptions, config?: VarsityConfig): Promise<ValidationResult>`
|
Validates an OpenAPI specification.
|
||||||
- `parse(source: string): Promise<ParsedSpec>`
|
|
||||||
- `generateValidationReport(source: string, reportOptions: ReportOptions, validationOptions?: ValidationOptions, config?: VarsityConfig): Promise<string>`
|
|
||||||
- `saveValidationReport(source: string, reportOptions: ReportOptions, validationOptions?: ValidationOptions, config?: VarsityConfig): Promise<void>`
|
|
||||||
- `validateMultiple(sources: string[], options?: ValidationOptions, config?: VarsityConfig): Promise<ValidationResult[]>`
|
|
||||||
- `getSupportedVersions(): string[]`
|
|
||||||
|
|
||||||
#### Factory Function
|
- `source`: Path, URL, or array of paths/URLs to OpenAPI specifications
|
||||||
- `createVarsity(config?: VarsityConfig)`: Creates a configured instance with methods
|
- `options`: Validation options (optional)
|
||||||
|
- `config`: Varsity configuration (optional)
|
||||||
|
|
||||||
#### Individual Module Functions
|
#### `parse(source)`
|
||||||
- `parseOpenAPISpec(source: string): Promise<ParsedSpec>`
|
Parses an OpenAPI specification without validation.
|
||||||
- `validateBasicStructure(spec: any, version: OpenAPIVersion): boolean`
|
|
||||||
- `validateOpenAPISpec(spec: any, version: OpenAPIVersion, options?: ValidationOptions): ValidationResult`
|
|
||||||
- `generateReport(result: ValidationResult, options: ReportOptions): string`
|
|
||||||
- `saveReport(content: string, outputPath: string): void`
|
|
||||||
|
|
||||||
### Types
|
- `source`: Path or URL to OpenAPI specification
|
||||||
|
|
||||||
- `ValidationResult`: Contains validation results with errors and warnings
|
#### `validateWithReferences(source, options?, config?)`
|
||||||
- `ParsedSpec`: Parsed specification with metadata
|
Recursively validates an OpenAPI specification and all its references.
|
||||||
- `ValidationOptions`: Configuration for validation behavior
|
|
||||||
- `ReportOptions`: Configuration for report generation
|
|
||||||
- `VarsityConfig`: Global configuration for the library
|
|
||||||
- `OpenAPISpec`: Union type for all OpenAPI specification versions
|
|
||||||
- `OpenAPIVersion`: Supported OpenAPI version strings
|
|
||||||
|
|
||||||
### Type Safety
|
#### `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
|
```typescript
|
||||||
import type { OpenAPI2, OpenAPI3, OpenAPI3_1 } from 'oas-types';
|
interface ValidationOptions {
|
||||||
|
strict?: boolean; // Enable strict validation
|
||||||
// All parsed specifications are properly typed
|
validateExamples?: boolean; // Validate examples in the spec
|
||||||
const result = await validate('spec.json');
|
validateReferences?: boolean; // Validate all references
|
||||||
// result.spec is typed as OpenAPISpec (OpenAPI2 | OpenAPI3 | OpenAPI3_1)
|
recursive?: boolean; // Enable recursive validation
|
||||||
|
maxRefDepth?: number; // Maximum reference depth
|
||||||
// Type guards for version-specific handling
|
customRules?: Record<string, any>; // Custom validation rules
|
||||||
if (result.version === '2.0') {
|
|
||||||
const swaggerSpec = result.spec as OpenAPI2;
|
|
||||||
// swaggerSpec.swagger, swaggerSpec.info, etc. are fully typed
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
### Report Options
|
||||||
|
|
||||||
### Running Tests
|
```typescript
|
||||||
```bash
|
interface ReportOptions {
|
||||||
bun test
|
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
|
```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
|
bun run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Linting
|
### Testing
|
||||||
|
|
||||||
```bash
|
```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
|
## 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": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
"commander": "^14.0.1",
|
"commander": "^14.0.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"oas-types": "^1.0.6",
|
"oas-types": "^1.0.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
@@ -21,6 +23,8 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="],
|
"@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/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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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,
|
parse,
|
||||||
generateValidationReport,
|
generateValidationReport,
|
||||||
saveValidationReport,
|
saveValidationReport,
|
||||||
validateMultiple,
|
validateWithReferences,
|
||||||
|
validateMultipleWithReferences,
|
||||||
|
analyzeDocumentReferences,
|
||||||
getSupportedVersions,
|
getSupportedVersions,
|
||||||
createVarsity,
|
createVarsity,
|
||||||
// Individual module exports
|
// Individual module exports
|
||||||
@@ -13,6 +15,16 @@ export {
|
|||||||
validateOpenAPISpec,
|
validateOpenAPISpec,
|
||||||
generateReport,
|
generateReport,
|
||||||
saveReport,
|
saveReport,
|
||||||
|
// Recursive validation exports
|
||||||
|
validateRecursively,
|
||||||
|
validateMultipleRecursively,
|
||||||
|
analyzeReferences,
|
||||||
|
// Reference resolver exports
|
||||||
|
resolveReference,
|
||||||
|
findReferences,
|
||||||
|
resolveAllReferences,
|
||||||
|
// Partial validation exports
|
||||||
|
validatePartialDocument,
|
||||||
} from "./src/varsity.js";
|
} from "./src/varsity.js";
|
||||||
|
|
||||||
// Type exports
|
// Type exports
|
||||||
@@ -25,7 +37,14 @@ export type {
|
|||||||
VarsityConfig,
|
VarsityConfig,
|
||||||
OpenAPIVersion,
|
OpenAPIVersion,
|
||||||
CLIResult,
|
CLIResult,
|
||||||
|
RecursiveValidationResult,
|
||||||
} from "./src/types.js";
|
} from "./src/types.js";
|
||||||
|
|
||||||
|
// Export types from other modules
|
||||||
|
export type {
|
||||||
|
ResolvedReference,
|
||||||
|
ReferenceContext,
|
||||||
|
} from "./src/ref-resolver.js";
|
||||||
|
|
||||||
// Default export - functional instance
|
// Default export - functional instance
|
||||||
export { default } from "./src/varsity.js";
|
export { default } from "./src/varsity.js";
|
||||||
|
|||||||
54
package.json
54
package.json
@@ -1,19 +1,32 @@
|
|||||||
{
|
{
|
||||||
"name": "varsity",
|
"name": "varsity",
|
||||||
"version": "1.0.0",
|
"version": "1.0.3",
|
||||||
"description": "Comprehensive OpenAPI parsing and validation library",
|
"description": "Comprehensive OpenAPI parsing and validation library",
|
||||||
"module": "index.ts",
|
|
||||||
"type": "module",
|
"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": {
|
"bin": {
|
||||||
"varsity": "./src/cli.ts"
|
"varsity": "dist/cli.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run src/cli.ts",
|
"start": "bun run src/cli.ts",
|
||||||
"dev": "bun run --watch src/cli.ts",
|
"dev": "bun run --watch src/cli.ts",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"build": "bun build index.ts --outdir dist --target bun",
|
"build": "bun build index.ts --outdir dist --target node --format esm",
|
||||||
"lint": "bun run --bun tsc --noEmit"
|
"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": [
|
"keywords": [
|
||||||
"openapi",
|
"openapi",
|
||||||
@@ -21,12 +34,34 @@
|
|||||||
"validation",
|
"validation",
|
||||||
"parser",
|
"parser",
|
||||||
"api",
|
"api",
|
||||||
"specification"
|
"specification",
|
||||||
|
"cli",
|
||||||
|
"typescript",
|
||||||
|
"bun",
|
||||||
|
"json-schema",
|
||||||
|
"api-validation",
|
||||||
|
"openapi-validator"
|
||||||
],
|
],
|
||||||
"author": "",
|
"author": "Luke",
|
||||||
"license": "MIT",
|
"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": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest",
|
||||||
|
"@types/js-yaml": "^4.0.9"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
@@ -36,6 +71,7 @@
|
|||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
"ajv-formats": "^3.0.1",
|
"ajv-formats": "^3.0.1",
|
||||||
"commander": "^14.0.1",
|
"commander": "^14.0.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"oas-types": "^1.0.6"
|
"oas-types": "^1.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
396
src/cli.ts
Normal file → Executable file
396
src/cli.ts
Normal file → Executable file
@@ -5,12 +5,13 @@ import {
|
|||||||
parse,
|
parse,
|
||||||
generateValidationReport,
|
generateValidationReport,
|
||||||
saveValidationReport,
|
saveValidationReport,
|
||||||
validateMultiple,
|
|
||||||
validateWithReferences,
|
validateWithReferences,
|
||||||
validateMultipleWithReferences,
|
validateMultipleWithReferences,
|
||||||
analyzeDocumentReferences,
|
analyzeDocumentReferences,
|
||||||
|
generateSpecificationSummary,
|
||||||
getSupportedVersions,
|
getSupportedVersions,
|
||||||
createVarsity,
|
createVarsity,
|
||||||
|
log,
|
||||||
} from "./varsity.js";
|
} from "./varsity.js";
|
||||||
import type { ValidationOptions, ReportOptions } from "./types.js";
|
import type { ValidationOptions, ReportOptions } from "./types.js";
|
||||||
|
|
||||||
@@ -18,14 +19,19 @@ const program = new Command();
|
|||||||
|
|
||||||
program
|
program
|
||||||
.name("varsity")
|
.name("varsity")
|
||||||
.description("Comprehensive OpenAPI parsing and validation library")
|
.description(
|
||||||
|
"Comprehensive OpenAPI parsing and validation library (supports JSON and YAML)"
|
||||||
|
)
|
||||||
.version("1.0.0");
|
.version("1.0.0");
|
||||||
|
|
||||||
// Validate command
|
// Validate command
|
||||||
program
|
program
|
||||||
.command("validate")
|
.command("validate")
|
||||||
.description("Validate an OpenAPI specification")
|
.description("Validate one or more OpenAPI specifications")
|
||||||
.argument("<source>", "Path or URL to OpenAPI specification")
|
.argument(
|
||||||
|
"<sources...>",
|
||||||
|
"Path(s) or URL(s) to OpenAPI specification(s) (JSON or YAML)"
|
||||||
|
)
|
||||||
.option("-s, --strict", "Enable strict validation mode")
|
.option("-s, --strict", "Enable strict validation mode")
|
||||||
.option("-e, --examples", "Validate examples in the specification")
|
.option("-e, --examples", "Validate examples in the specification")
|
||||||
.option("-r, --references", "Validate all references")
|
.option("-r, --references", "Validate all references")
|
||||||
@@ -36,7 +42,14 @@ program
|
|||||||
"10"
|
"10"
|
||||||
)
|
)
|
||||||
.option("-v, --verbose", "Show detailed output")
|
.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 {
|
try {
|
||||||
const validationOptions: ValidationOptions = {
|
const validationOptions: ValidationOptions = {
|
||||||
strict: options.strict,
|
strict: options.strict,
|
||||||
@@ -46,74 +59,201 @@ program
|
|||||||
maxRefDepth: parseInt(options.maxDepth) || 10,
|
maxRefDepth: parseInt(options.maxDepth) || 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result;
|
// Handle single vs multiple sources
|
||||||
if (options.recursive) {
|
if (sources.length === 1) {
|
||||||
result = await validateWithReferences(source, validationOptions);
|
const source = sources[0];
|
||||||
|
if (!source) {
|
||||||
if (result.valid) {
|
console.log("❌ No source provided");
|
||||||
console.log("✅ Specification and all references are valid");
|
|
||||||
if (options.verbose) {
|
|
||||||
console.log(`Version: ${result.version}`);
|
|
||||||
console.log(`Total documents: ${result.totalDocuments}`);
|
|
||||||
console.log(`Valid documents: ${result.validDocuments}`);
|
|
||||||
console.log(
|
|
||||||
`Circular references: ${result.circularReferences.length}`
|
|
||||||
);
|
|
||||||
console.log(`Warnings: ${result.warnings.length}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("❌ Specification or references are invalid");
|
|
||||||
console.log(`Errors: ${result.errors.length}`);
|
|
||||||
console.log(`Total documents: ${result.totalDocuments}`);
|
|
||||||
console.log(`Valid documents: ${result.validDocuments}`);
|
|
||||||
|
|
||||||
if (result.circularReferences.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`Circular references: ${result.circularReferences.length}`
|
|
||||||
);
|
|
||||||
for (const circular of result.circularReferences) {
|
|
||||||
console.log(` • ${circular}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const error of result.errors) {
|
|
||||||
console.log(` • ${error.path}: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.verbose && result.warnings.length > 0) {
|
|
||||||
console.log(`Warnings: ${result.warnings.length}`);
|
|
||||||
for (const warning of result.warnings) {
|
|
||||||
console.log(` • ${warning.path}: ${warning.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} else {
|
let result;
|
||||||
result = await validate(source, validationOptions);
|
|
||||||
|
|
||||||
if (result.valid) {
|
if (options.recursive) {
|
||||||
console.log("✅ Specification is valid");
|
result = await validateWithReferences(source, validationOptions);
|
||||||
if (options.verbose) {
|
|
||||||
console.log(`Version: ${result.version}`);
|
if (result.valid) {
|
||||||
console.log(`Warnings: ${result.warnings.length}`);
|
console.log("✅ Specification and all references are valid");
|
||||||
|
|
||||||
|
// 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}`);
|
||||||
|
console.log(
|
||||||
|
`Circular references: ${result.circularReferences.length}`
|
||||||
|
);
|
||||||
|
console.log(`Warnings: ${result.warnings.length}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("❌ Specification or references are invalid");
|
||||||
|
console.log(`Errors: ${result.errors.length}`);
|
||||||
|
console.log(`Total documents: ${result.totalDocuments}`);
|
||||||
|
console.log(`Valid documents: ${result.validDocuments}`);
|
||||||
|
|
||||||
|
if (result.circularReferences.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`Circular references: ${result.circularReferences.length}`
|
||||||
|
);
|
||||||
|
for (const circular of result.circularReferences) {
|
||||||
|
console.log(` • ${circular}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const error of result.errors) {
|
||||||
|
console.log(` • ${error.path}: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.verbose && result.warnings.length > 0) {
|
||||||
|
console.log(`Warnings: ${result.warnings.length}`);
|
||||||
|
for (const warning of result.warnings) {
|
||||||
|
console.log(` • ${warning.path}: ${warning.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("❌ Specification is invalid");
|
result = await validate([source], validationOptions);
|
||||||
console.log(`Errors: ${result.errors.length}`);
|
|
||||||
|
|
||||||
for (const error of result.errors) {
|
// Handle both single result and array of results
|
||||||
console.log(` • ${error.path}: ${error.message}`);
|
const validationResult = Array.isArray(result) ? result[0] : result;
|
||||||
|
|
||||||
|
if (!validationResult) {
|
||||||
|
console.log("❌ No validation result received");
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.verbose && result.warnings.length > 0) {
|
if (validationResult.valid) {
|
||||||
console.log(`Warnings: ${result.warnings.length}`);
|
console.log("✅ Specification is valid");
|
||||||
for (const warning of result.warnings) {
|
|
||||||
console.log(` • ${warning.path}: ${warning.message}`);
|
// 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: ${validationResult.errors.length}`);
|
||||||
|
|
||||||
|
for (const error of validationResult.errors) {
|
||||||
|
console.log(` • ${error.path}: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(1);
|
console.log("\n" + "=".repeat(50));
|
||||||
|
console.log(`Summary: ${validCount} valid, ${errorCount} invalid`);
|
||||||
|
|
||||||
|
if (errorCount > 0) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -129,9 +269,15 @@ program
|
|||||||
program
|
program
|
||||||
.command("parse")
|
.command("parse")
|
||||||
.description("Parse an OpenAPI specification without validation")
|
.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("-j, --json", "Output as JSON")
|
||||||
|
.option("--no-progress", "Disable progress indicators")
|
||||||
|
.option("--no-colors", "Disable colored output")
|
||||||
.action(async (source: string, options: any) => {
|
.action(async (source: string, options: any) => {
|
||||||
|
// Configure logger based on options
|
||||||
|
log.setVerbose(options.verbose);
|
||||||
|
log.setShowProgress(!options.noProgress);
|
||||||
|
log.setUseColors(!options.noColors);
|
||||||
try {
|
try {
|
||||||
const parsed = await parse(source);
|
const parsed = await parse(source);
|
||||||
|
|
||||||
@@ -160,7 +306,7 @@ program
|
|||||||
program
|
program
|
||||||
.command("report")
|
.command("report")
|
||||||
.description("Generate a validation 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(
|
.option(
|
||||||
"-f, --format <format>",
|
"-f, --format <format>",
|
||||||
"Report format (json, yaml, html, markdown)",
|
"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
|
// Analyze command
|
||||||
program
|
program
|
||||||
.command("analyze")
|
.command("analyze")
|
||||||
.description("Analyze references in an OpenAPI specification")
|
.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")
|
.option("-j, --json", "Output as JSON")
|
||||||
.action(async (source: string, options: any) => {
|
.action(async (source: string, options: any) => {
|
||||||
try {
|
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
|
// Info command
|
||||||
program
|
program
|
||||||
.command("info")
|
.command("info")
|
||||||
@@ -320,5 +470,7 @@ program
|
|||||||
console.log("\nFor more information, visit: https://spec.openapis.org/");
|
console.log("\nFor more information, visit: https://spec.openapis.org/");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Parse command line arguments
|
// Only parse command line arguments if this file is being run directly
|
||||||
program.parse();
|
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),
|
||||||
|
};
|
||||||
124
src/parser.ts
124
src/parser.ts
@@ -1,30 +1,47 @@
|
|||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
|
import * as yaml from "js-yaml";
|
||||||
import type { ParsedSpec, OpenAPIVersion, OpenAPISpec } from "./types.js";
|
import type { ParsedSpec, OpenAPIVersion, OpenAPISpec } from "./types.js";
|
||||||
import type { OpenAPI2, OpenAPI3, OpenAPI3_1 } from "oas-types";
|
import type { OpenAPI2, OpenAPI3, OpenAPI3_1 } from "oas-types";
|
||||||
|
import { log } from "./logger.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect OpenAPI version from specification
|
* Detect OpenAPI version from specification
|
||||||
*/
|
*/
|
||||||
const detectVersion = (spec: any): OpenAPIVersion => {
|
const detectVersion = (spec: any): OpenAPIVersion => {
|
||||||
|
log.parsingStep("Detecting OpenAPI version");
|
||||||
|
|
||||||
// Check for OpenAPI 3.x
|
// Check for OpenAPI 3.x
|
||||||
if (spec.openapi) {
|
if (spec.openapi) {
|
||||||
const version = spec.openapi;
|
const version = spec.openapi;
|
||||||
|
log.parsingStep("Found OpenAPI 3.x specification", `Version: ${version}`);
|
||||||
|
|
||||||
if (version.startsWith("3.0")) {
|
if (version.startsWith("3.0")) {
|
||||||
|
log.parsingStep("Detected OpenAPI 3.0.x", version);
|
||||||
return version as OpenAPIVersion;
|
return version as OpenAPIVersion;
|
||||||
} else if (version.startsWith("3.1")) {
|
} else if (version.startsWith("3.1")) {
|
||||||
|
log.parsingStep("Detected OpenAPI 3.1.x", version);
|
||||||
return version as OpenAPIVersion;
|
return version as OpenAPIVersion;
|
||||||
} else if (version.startsWith("3.2")) {
|
} else if (version.startsWith("3.2")) {
|
||||||
|
log.parsingStep("Detected OpenAPI 3.2.x", version);
|
||||||
return version as OpenAPIVersion;
|
return version as OpenAPIVersion;
|
||||||
}
|
}
|
||||||
|
log.error("Unsupported OpenAPI version", { version });
|
||||||
throw new Error(`Unsupported OpenAPI version: ${version}`);
|
throw new Error(`Unsupported OpenAPI version: ${version}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for Swagger 2.0
|
// Check for Swagger 2.0
|
||||||
if (spec.swagger === "2.0") {
|
if (spec.swagger === "2.0") {
|
||||||
|
log.parsingStep("Detected Swagger 2.0 specification");
|
||||||
return "2.0";
|
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(
|
throw new Error(
|
||||||
'Unable to detect OpenAPI version. Specification must have "openapi" or "swagger" field.'
|
'Unable to detect OpenAPI version. Specification must have "openapi" or "swagger" field.'
|
||||||
);
|
);
|
||||||
@@ -37,62 +54,132 @@ const extractMetadata = (
|
|||||||
spec: OpenAPISpec,
|
spec: OpenAPISpec,
|
||||||
version: OpenAPIVersion
|
version: OpenAPIVersion
|
||||||
): ParsedSpec["metadata"] => {
|
): ParsedSpec["metadata"] => {
|
||||||
|
log.parsingStep("Extracting metadata from specification");
|
||||||
|
|
||||||
// All OpenAPI versions have the same info structure
|
// All OpenAPI versions have the same info structure
|
||||||
const info = spec.info;
|
const info = spec.info;
|
||||||
return {
|
|
||||||
|
const metadata = {
|
||||||
title: info?.title,
|
title: info?.title,
|
||||||
version: info?.version,
|
version: info?.version,
|
||||||
description: info?.description,
|
description: info?.description,
|
||||||
contact: info?.contact,
|
contact: info?.contact,
|
||||||
license: info?.license,
|
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
|
* Parse an OpenAPI specification from a file path or URL
|
||||||
*/
|
*/
|
||||||
export const parseOpenAPISpec = async (source: string): Promise<ParsedSpec> => {
|
export const parseOpenAPISpec = async (source: string): Promise<ParsedSpec> => {
|
||||||
|
log.startOperation("Parsing OpenAPI specification");
|
||||||
|
log.fileOperation("Reading specification", source);
|
||||||
|
|
||||||
let content: string;
|
let content: string;
|
||||||
let spec: any;
|
let spec: any;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Handle file paths
|
// Handle file paths
|
||||||
if (source.startsWith("http://") || source.startsWith("https://")) {
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
||||||
|
log.parsingStep("Fetching remote specification", source);
|
||||||
const response = await fetch(source);
|
const response = await fetch(source);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
log.error("Failed to fetch remote specification", {
|
||||||
|
url: source,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
});
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to fetch specification: ${response.statusText}`
|
`Failed to fetch specification: ${response.statusText}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
content = await response.text();
|
content = await response.text();
|
||||||
|
log.parsingStep(
|
||||||
|
"Remote specification fetched",
|
||||||
|
`Size: ${content.length} characters`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Local file
|
// Local file
|
||||||
|
log.parsingStep("Reading local file", source);
|
||||||
const filePath = resolve(source);
|
const filePath = resolve(source);
|
||||||
|
log.fileOperation("Reading file", filePath);
|
||||||
content = readFileSync(filePath, "utf-8");
|
content = readFileSync(filePath, "utf-8");
|
||||||
|
log.parsingStep("Local file read", `Size: ${content.length} characters`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse JSON or YAML
|
// Parse JSON or YAML
|
||||||
|
log.parsingStep("Determining content format");
|
||||||
if (content.trim().startsWith("{") || content.trim().startsWith("[")) {
|
if (content.trim().startsWith("{") || content.trim().startsWith("[")) {
|
||||||
|
log.parsingStep("Detected JSON format");
|
||||||
|
log.parsingStep("Parsing JSON content");
|
||||||
spec = JSON.parse(content);
|
spec = JSON.parse(content);
|
||||||
|
log.parsingStep("JSON parsing completed");
|
||||||
} else {
|
} else {
|
||||||
// For YAML parsing, we'll use a simple approach or add yaml dependency later
|
// Parse YAML
|
||||||
throw new Error(
|
log.parsingStep("Detected YAML format");
|
||||||
"YAML parsing not yet implemented. Please use JSON 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(
|
||||||
|
`Failed to parse YAML: ${
|
||||||
|
yamlError instanceof Error
|
||||||
|
? yamlError.message
|
||||||
|
: "Unknown YAML error"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = detectVersion(spec);
|
const version = detectVersion(spec);
|
||||||
|
log.parsingStep("Version detection completed", `Detected: ${version}`);
|
||||||
|
|
||||||
// Type the spec based on the detected version
|
// Type the spec based on the detected version
|
||||||
const typedSpec = spec as OpenAPISpec;
|
const typedSpec = spec as OpenAPISpec;
|
||||||
|
log.parsingStep("Specification typed", `Type: OpenAPISpec`);
|
||||||
|
|
||||||
return {
|
const metadata = extractMetadata(typedSpec, version);
|
||||||
|
|
||||||
|
const result = {
|
||||||
spec: typedSpec,
|
spec: typedSpec,
|
||||||
version,
|
version,
|
||||||
source,
|
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) {
|
} catch (error) {
|
||||||
|
log.error("Parsing failed", {
|
||||||
|
source,
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
log.endOperation("Parsing OpenAPI specification", false);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to parse OpenAPI specification: ${
|
`Failed to parse OpenAPI specification: ${
|
||||||
error instanceof Error ? error.message : "Unknown error"
|
error instanceof Error ? error.message : "Unknown error"
|
||||||
@@ -108,13 +195,32 @@ export const validateBasicStructure = (
|
|||||||
spec: OpenAPISpec,
|
spec: OpenAPISpec,
|
||||||
version: OpenAPIVersion
|
version: OpenAPIVersion
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
log.parsingStep("Validating basic structure");
|
||||||
|
|
||||||
|
let isValid: boolean;
|
||||||
|
|
||||||
if (version === "2.0") {
|
if (version === "2.0") {
|
||||||
|
log.parsingStep("Validating Swagger 2.0 structure");
|
||||||
const swaggerSpec = spec as OpenAPI2.Specification;
|
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 {
|
} else {
|
||||||
|
log.parsingStep("Validating OpenAPI 3.x structure");
|
||||||
const openapiSpec = spec as
|
const openapiSpec = spec as
|
||||||
| OpenAPI3.Specification
|
| OpenAPI3.Specification
|
||||||
| OpenAPI3_1.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,
|
OpenAPIVersion,
|
||||||
OpenAPISpec,
|
OpenAPISpec,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
import { log } from "./logger.js";
|
||||||
|
|
||||||
export interface RecursiveValidationResult extends ValidationResult {
|
export interface RecursiveValidationResult extends ValidationResult {
|
||||||
partialValidations: Array<{
|
partialValidations: Array<{
|
||||||
@@ -45,6 +46,8 @@ export const validateRecursively = async (
|
|||||||
options.maxRefDepth || 10
|
options.maxRefDepth || 10
|
||||||
);
|
);
|
||||||
|
|
||||||
|
log.info(`🔗 Following ${resolvedRefs.length} references...`);
|
||||||
|
|
||||||
// Validate each resolved reference
|
// Validate each resolved reference
|
||||||
const partialValidations: Array<{
|
const partialValidations: Array<{
|
||||||
path: string;
|
path: string;
|
||||||
@@ -54,8 +57,12 @@ export const validateRecursively = async (
|
|||||||
|
|
||||||
let validDocuments = rootValidation.valid ? 1 : 0;
|
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) {
|
if (ref.isCircular) {
|
||||||
|
log.info(`🔄 Circular reference: ${ref.path}`);
|
||||||
partialValidations.push({
|
partialValidations.push({
|
||||||
path: ref.path,
|
path: ref.path,
|
||||||
result: {
|
result: {
|
||||||
@@ -67,7 +74,7 @@ export const validateRecursively = async (
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
spec: null,
|
spec: {} as OpenAPISpec,
|
||||||
version: ref.version || "3.0",
|
version: ref.version || "3.0",
|
||||||
},
|
},
|
||||||
isCircular: true,
|
isCircular: true,
|
||||||
@@ -97,6 +104,11 @@ export const validateRecursively = async (
|
|||||||
|
|
||||||
if (partialResult.valid) {
|
if (partialResult.valid) {
|
||||||
validDocuments++;
|
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);
|
allWarnings.push(...partial.result.warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
valid:
|
valid:
|
||||||
rootValidation.valid && partialValidations.every((p) => p.result.valid),
|
rootValidation.valid && partialValidations.every((p) => p.result.valid),
|
||||||
errors: allErrors,
|
errors: allErrors,
|
||||||
@@ -121,6 +133,8 @@ export const validateRecursively = async (
|
|||||||
totalDocuments: 1 + partialValidations.length,
|
totalDocuments: 1 + partialValidations.length,
|
||||||
validDocuments,
|
validDocuments,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,13 +144,37 @@ export const validateMultipleRecursively = async (
|
|||||||
sources: string[],
|
sources: string[],
|
||||||
options: ValidationOptions = {}
|
options: ValidationOptions = {}
|
||||||
): Promise<RecursiveValidationResult[]> => {
|
): Promise<RecursiveValidationResult[]> => {
|
||||||
|
log.startOperation("Multiple recursive validation");
|
||||||
|
log.validationStep(
|
||||||
|
"Starting batch validation",
|
||||||
|
`${sources.length} specifications`
|
||||||
|
);
|
||||||
|
|
||||||
const results: RecursiveValidationResult[] = [];
|
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 {
|
try {
|
||||||
const result = await validateRecursively(source, options);
|
const result = await validateRecursively(source, options);
|
||||||
results.push(result);
|
results.push(result);
|
||||||
|
log.validationStep("Specification validated", `Valid: ${result.valid}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
log.error("Specification validation failed", {
|
||||||
|
source,
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
});
|
||||||
|
|
||||||
// Create error result for failed parsing
|
// Create error result for failed parsing
|
||||||
const errorResult: RecursiveValidationResult = {
|
const errorResult: RecursiveValidationResult = {
|
||||||
valid: false,
|
valid: false,
|
||||||
@@ -149,7 +187,7 @@ export const validateMultipleRecursively = async (
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
warnings: [],
|
warnings: [],
|
||||||
spec: null,
|
spec: {} as OpenAPISpec,
|
||||||
version: "3.0",
|
version: "3.0",
|
||||||
partialValidations: [],
|
partialValidations: [],
|
||||||
circularReferences: [],
|
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;
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,10 +221,17 @@ export const analyzeReferences = async (
|
|||||||
circularReferences: string[];
|
circularReferences: string[];
|
||||||
totalReferences: number;
|
totalReferences: number;
|
||||||
}> => {
|
}> => {
|
||||||
|
log.startOperation("Analyzing references");
|
||||||
|
log.fileOperation("Analyzing references", source);
|
||||||
|
|
||||||
const parsed = await parseOpenAPISpec(source);
|
const parsed = await parseOpenAPISpec(source);
|
||||||
|
log.validationStep("Parsing completed for reference analysis");
|
||||||
|
|
||||||
const references = findReferences(parsed.spec);
|
const references = findReferences(parsed.spec);
|
||||||
|
log.validationStep("References found", `${references.length} total`);
|
||||||
|
|
||||||
// Check for circular references by analyzing reference paths
|
// Check for circular references by analyzing reference paths
|
||||||
|
log.validationStep("Analyzing circular references");
|
||||||
const circularReferences: string[] = [];
|
const circularReferences: string[] = [];
|
||||||
const referenceMap = new Map<string, string[]>();
|
const referenceMap = new Map<string, string[]>();
|
||||||
|
|
||||||
@@ -192,13 +247,26 @@ export const analyzeReferences = async (
|
|||||||
for (const [refValue, paths] of referenceMap) {
|
for (const [refValue, paths] of referenceMap) {
|
||||||
if (paths.length > 1) {
|
if (paths.length > 1) {
|
||||||
// This is a potential circular reference
|
// This is a potential circular reference
|
||||||
|
log.referenceStep(
|
||||||
|
"Circular reference detected",
|
||||||
|
refValue,
|
||||||
|
`${paths.length} occurrences`
|
||||||
|
);
|
||||||
circularReferences.push(refValue);
|
circularReferences.push(refValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
references,
|
references,
|
||||||
circularReferences,
|
circularReferences,
|
||||||
totalReferences: references.length,
|
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 { allSchemas } from "oas-types/schemas";
|
||||||
import type { OpenAPI3_1, OpenAPI3_2, OpenAPI3, OpenAPI2 } from "oas-types";
|
import type { OpenAPI3_1, OpenAPI3_2, OpenAPI3, OpenAPI2 } from "oas-types";
|
||||||
|
import { log } from "./logger.js";
|
||||||
|
|
||||||
// Initialize AJV instance
|
// Initialize AJV instance
|
||||||
const createAjvInstance = (): Ajv => {
|
const createAjvInstance = (): Ajv => {
|
||||||
@@ -55,6 +56,8 @@ const findReferences = (
|
|||||||
obj: OpenAPISpec,
|
obj: OpenAPISpec,
|
||||||
path = ""
|
path = ""
|
||||||
): Array<{ path: string; value: string }> => {
|
): Array<{ path: string; value: string }> => {
|
||||||
|
log.validationStep("Finding references in specification");
|
||||||
|
|
||||||
const refs: Array<{ path: string; value: string }> = [];
|
const refs: Array<{ path: string; value: string }> = [];
|
||||||
|
|
||||||
if (typeof obj === "object" && obj !== null) {
|
if (typeof obj === "object" && obj !== null) {
|
||||||
@@ -62,6 +65,7 @@ const findReferences = (
|
|||||||
const currentPath = path ? `${path}.${key}` : key;
|
const currentPath = path ? `${path}.${key}` : key;
|
||||||
|
|
||||||
if (key === "$ref" && typeof value === "string") {
|
if (key === "$ref" && typeof value === "string") {
|
||||||
|
log.referenceStep("Found reference", value, `at ${currentPath}`);
|
||||||
refs.push({ path: currentPath, value });
|
refs.push({ path: currentPath, value });
|
||||||
} else if (typeof value === "object") {
|
} else if (typeof value === "object") {
|
||||||
refs.push(...findReferences(value, currentPath));
|
refs.push(...findReferences(value, currentPath));
|
||||||
@@ -69,6 +73,10 @@ const findReferences = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.validationStep(
|
||||||
|
"Reference search completed",
|
||||||
|
`Found ${refs.length} references`
|
||||||
|
);
|
||||||
return refs;
|
return refs;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,6 +87,8 @@ const resolveReference = (
|
|||||||
spec: OpenAPISpec,
|
spec: OpenAPISpec,
|
||||||
ref: { path: string; value: string }
|
ref: { path: string; value: string }
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
log.referenceStep("Resolving reference", ref.value, `from ${ref.path}`);
|
||||||
|
|
||||||
// Simple reference resolution - in a real implementation, this would be more comprehensive
|
// Simple reference resolution - in a real implementation, this would be more comprehensive
|
||||||
if (ref.value.startsWith("#/")) {
|
if (ref.value.startsWith("#/")) {
|
||||||
const path = ref.value.substring(2).split("/");
|
const path = ref.value.substring(2).split("/");
|
||||||
@@ -87,14 +97,30 @@ const resolveReference = (
|
|||||||
for (const segment of path) {
|
for (const segment of path) {
|
||||||
if (current && typeof current === "object" && segment in current) {
|
if (current && typeof current === "object" && segment in current) {
|
||||||
current = (current as any)[segment];
|
current = (current as any)[segment];
|
||||||
|
log.referenceStep(
|
||||||
|
"Traversing path segment",
|
||||||
|
segment,
|
||||||
|
`current type: ${typeof current}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
|
log.referenceStep(
|
||||||
|
"Reference resolution failed",
|
||||||
|
`segment '${segment}' not found`
|
||||||
|
);
|
||||||
return false;
|
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
|
return false; // External references not supported in this simple implementation
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,29 +133,43 @@ const performStrictValidation = (
|
|||||||
errors: ValidationError[],
|
errors: ValidationError[],
|
||||||
warnings: ValidationError[]
|
warnings: ValidationError[]
|
||||||
): void => {
|
): void => {
|
||||||
|
log.validationStep("Performing strict validation checks");
|
||||||
|
|
||||||
// Check for required fields based on version
|
// Check for required fields based on version
|
||||||
if (version === "2.0") {
|
if (version === "2.0") {
|
||||||
|
log.validationStep("Validating Swagger 2.0 strict requirements");
|
||||||
const swaggerSpec = spec as OpenAPI2.Specification;
|
const swaggerSpec = spec as OpenAPI2.Specification;
|
||||||
if (!swaggerSpec.host) {
|
if (!swaggerSpec.host) {
|
||||||
|
log.validationStep("Missing host field in Swagger 2.0");
|
||||||
errors.push({
|
errors.push({
|
||||||
path: "/",
|
path: "/",
|
||||||
message: 'Either "host" or "servers" must be specified in Swagger 2.0',
|
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 {
|
} else {
|
||||||
|
log.validationStep("Validating OpenAPI 3.x strict requirements");
|
||||||
const openapiSpec = spec as
|
const openapiSpec = spec as
|
||||||
| OpenAPI3.Specification
|
| OpenAPI3.Specification
|
||||||
| OpenAPI3_1.Specification
|
| OpenAPI3_1.Specification
|
||||||
| OpenAPI3_2.Specification;
|
| OpenAPI3_2.Specification;
|
||||||
if (!openapiSpec.servers || openapiSpec.servers.length === 0) {
|
if (!openapiSpec.servers || openapiSpec.servers.length === 0) {
|
||||||
|
log.validationStep("No servers specified in OpenAPI 3.x");
|
||||||
warnings.push({
|
warnings.push({
|
||||||
path: "/",
|
path: "/",
|
||||||
message: "No servers specified. Consider adding at least one server.",
|
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
|
// Check for security definitions
|
||||||
|
log.validationStep("Validating security definitions");
|
||||||
if (version === "2.0") {
|
if (version === "2.0") {
|
||||||
const swaggerSpec = spec as OpenAPI2.Specification;
|
const swaggerSpec = spec as OpenAPI2.Specification;
|
||||||
if (
|
if (
|
||||||
@@ -137,10 +177,16 @@ const performStrictValidation = (
|
|||||||
swaggerSpec.security.length > 0 &&
|
swaggerSpec.security.length > 0 &&
|
||||||
!swaggerSpec.securityDefinitions
|
!swaggerSpec.securityDefinitions
|
||||||
) {
|
) {
|
||||||
|
log.validationStep("Security used without definitions in Swagger 2.0");
|
||||||
errors.push({
|
errors.push({
|
||||||
path: "/",
|
path: "/",
|
||||||
message: "Security schemes must be defined when using security",
|
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 {
|
} else {
|
||||||
const openapiSpec = spec as
|
const openapiSpec = spec as
|
||||||
@@ -152,12 +198,23 @@ const performStrictValidation = (
|
|||||||
openapiSpec.security.length > 0 &&
|
openapiSpec.security.length > 0 &&
|
||||||
!openapiSpec.components?.securitySchemes
|
!openapiSpec.components?.securitySchemes
|
||||||
) {
|
) {
|
||||||
|
log.validationStep("Security used without schemes in OpenAPI 3.x");
|
||||||
errors.push({
|
errors.push({
|
||||||
path: "/",
|
path: "/",
|
||||||
message: "Security schemes must be defined when using security",
|
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[],
|
errors: ValidationError[],
|
||||||
warnings: ValidationError[]
|
warnings: ValidationError[]
|
||||||
): void => {
|
): void => {
|
||||||
// This would implement example validation logic
|
log.validationStep("Validating examples in specification");
|
||||||
// For now, just a placeholder
|
|
||||||
|
let exampleCount = 0;
|
||||||
|
|
||||||
if (spec.paths) {
|
if (spec.paths) {
|
||||||
|
log.validationStep("Analyzing examples in paths");
|
||||||
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
||||||
if (typeof pathItem === "object" && pathItem !== null) {
|
if (typeof pathItem === "object" && pathItem !== null) {
|
||||||
for (const [method, operation] of Object.entries(pathItem)) {
|
for (const [method, operation] of Object.entries(pathItem)) {
|
||||||
@@ -180,6 +240,7 @@ const validateExamples = (
|
|||||||
operation !== null &&
|
operation !== null &&
|
||||||
"responses" in operation
|
"responses" in operation
|
||||||
) {
|
) {
|
||||||
|
log.endpointStep("Validating examples", method, path);
|
||||||
// Check response examples
|
// Check response examples
|
||||||
for (const [statusCode, response] of Object.entries(
|
for (const [statusCode, response] of Object.entries(
|
||||||
operation.responses
|
operation.responses
|
||||||
@@ -189,6 +250,11 @@ const validateExamples = (
|
|||||||
response !== null &&
|
response !== null &&
|
||||||
"examples" in response
|
"examples" in response
|
||||||
) {
|
) {
|
||||||
|
log.validationStep(
|
||||||
|
"Found examples in response",
|
||||||
|
`${method} ${path} ${statusCode}`
|
||||||
|
);
|
||||||
|
exampleCount++;
|
||||||
// Validate examples here
|
// 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[],
|
errors: ValidationError[],
|
||||||
warnings: ValidationError[]
|
warnings: ValidationError[]
|
||||||
): void => {
|
): void => {
|
||||||
|
log.validationStep("Validating references in specification");
|
||||||
|
|
||||||
// This would implement reference validation logic
|
// This would implement reference validation logic
|
||||||
// Check for broken $ref references
|
// Check for broken $ref references
|
||||||
const refs = findReferences(spec);
|
const refs = findReferences(spec);
|
||||||
|
let validRefs = 0;
|
||||||
|
let brokenRefs = 0;
|
||||||
|
|
||||||
for (const ref of refs) {
|
for (const ref of refs) {
|
||||||
if (!resolveReference(spec, ref)) {
|
if (!resolveReference(spec, ref)) {
|
||||||
|
log.referenceStep("Broken reference found", ref.value, `at ${ref.path}`);
|
||||||
errors.push({
|
errors.push({
|
||||||
path: ref.path,
|
path: ref.path,
|
||||||
message: `Broken reference: ${ref.value}`,
|
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,
|
version: OpenAPIVersion,
|
||||||
options: ValidationOptions = {}
|
options: ValidationOptions = {}
|
||||||
): ValidationResult => {
|
): ValidationResult => {
|
||||||
|
log.startOperation("Validating OpenAPI specification");
|
||||||
|
log.validationStep("Initializing validation", `Version: ${version}`);
|
||||||
|
|
||||||
const normalizedVersion = normalizeVersion(version);
|
const normalizedVersion = normalizeVersion(version);
|
||||||
|
log.validationStep("Normalized version", normalizedVersion);
|
||||||
|
|
||||||
const schema = schemas.get(normalizedVersion);
|
const schema = schemas.get(normalizedVersion);
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
|
log.error("No schema available for version", {
|
||||||
|
version,
|
||||||
|
normalizedVersion,
|
||||||
|
});
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`No schema available for OpenAPI version: ${version} (normalized to ${normalizedVersion})`
|
`No schema available for OpenAPI version: ${version} (normalized to ${normalizedVersion})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.validationStep("Compiling schema for validation");
|
||||||
const validate = ajv.compile(schema);
|
const validate = ajv.compile(schema);
|
||||||
|
log.validationStep("Running schema validation");
|
||||||
const valid = validate(spec);
|
const valid = validate(spec);
|
||||||
|
|
||||||
const errors: ValidationError[] = [];
|
const errors: ValidationError[] = [];
|
||||||
const warnings: ValidationError[] = [];
|
const warnings: ValidationError[] = [];
|
||||||
|
|
||||||
if (!valid && validate.errors) {
|
if (!valid && validate.errors) {
|
||||||
|
log.validationStep(
|
||||||
|
"Schema validation found errors",
|
||||||
|
`${validate.errors.length} errors`
|
||||||
|
);
|
||||||
for (const error of validate.errors) {
|
for (const error of validate.errors) {
|
||||||
const validationError: ValidationError = {
|
const validationError: ValidationError = {
|
||||||
path: error.instancePath || error.schemaPath || "/",
|
path: error.instancePath || error.schemaPath || "/",
|
||||||
@@ -253,31 +353,52 @@ export const validateOpenAPISpec = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (error.keyword === "required" || error.keyword === "type") {
|
if (error.keyword === "required" || error.keyword === "type") {
|
||||||
|
log.validationStep(
|
||||||
|
"Schema validation error",
|
||||||
|
`${error.keyword}: ${validationError.message}`
|
||||||
|
);
|
||||||
errors.push(validationError);
|
errors.push(validationError);
|
||||||
} else {
|
} else {
|
||||||
|
log.validationStep(
|
||||||
|
"Schema validation warning",
|
||||||
|
`${error.keyword}: ${validationError.message}`
|
||||||
|
);
|
||||||
warnings.push(validationError);
|
warnings.push(validationError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.validationStep("Schema validation passed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional custom validations
|
// Additional custom validations
|
||||||
if (options.strict) {
|
if (options.strict) {
|
||||||
|
log.validationStep("Running strict validation");
|
||||||
performStrictValidation(spec, version, errors, warnings);
|
performStrictValidation(spec, version, errors, warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.validateExamples) {
|
if (options.validateExamples) {
|
||||||
|
log.validationStep("Running example validation");
|
||||||
validateExamples(spec, version, errors, warnings);
|
validateExamples(spec, version, errors, warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.validateReferences) {
|
if (options.validateReferences) {
|
||||||
|
log.validationStep("Running reference validation");
|
||||||
validateReferences(spec, version, errors, warnings);
|
validateReferences(spec, version, errors, warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const result = {
|
||||||
valid: errors.length === 0,
|
valid: errors.length === 0,
|
||||||
errors,
|
errors,
|
||||||
warnings,
|
warnings,
|
||||||
spec,
|
spec,
|
||||||
version,
|
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,
|
validateMultipleRecursively,
|
||||||
analyzeReferences,
|
analyzeReferences,
|
||||||
} from "./recursive-validator.js";
|
} from "./recursive-validator.js";
|
||||||
|
import {
|
||||||
|
analyzeSpecification,
|
||||||
|
generateDetailedSummary,
|
||||||
|
generateJSONSummary,
|
||||||
|
} from "./summary-analyzer.js";
|
||||||
|
import { log } from "./logger.js";
|
||||||
import type {
|
import type {
|
||||||
ParsedSpec,
|
ParsedSpec,
|
||||||
ValidationResult,
|
ValidationResult,
|
||||||
@@ -36,11 +42,26 @@ export const validate = async (
|
|||||||
if (Array.isArray(source)) {
|
if (Array.isArray(source)) {
|
||||||
const results: ValidationResult[] = [];
|
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 {
|
try {
|
||||||
const result = await validateSingle(singleSource, options, config);
|
const result = await validateSingle(singleSource, options, config);
|
||||||
results.push(result);
|
results.push(result);
|
||||||
|
log.info(
|
||||||
|
`✅ Validated: ${singleSource} - ${
|
||||||
|
result.valid ? "Valid" : "Invalid"
|
||||||
|
}`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
`❌ Failed: ${singleSource} - ${
|
||||||
|
error instanceof Error ? error.message : "Unknown error"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
// Create error result for failed parsing
|
// Create error result for failed parsing
|
||||||
const errorResult: ValidationResult = {
|
const errorResult: ValidationResult = {
|
||||||
valid: false,
|
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;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single specification validation
|
// 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,
|
source,
|
||||||
validationOptions
|
validationOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valid: recursiveResult.valid,
|
valid: recursiveResult.valid,
|
||||||
errors: recursiveResult.errors,
|
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
|
* Parse an OpenAPI specification without validation
|
||||||
*/
|
*/
|
||||||
export const parse = async (source: string): Promise<ParsedSpec> => {
|
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 = {},
|
validationOptions: ValidationOptions = {},
|
||||||
config: VarsityConfig = defaultConfig
|
config: VarsityConfig = defaultConfig
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
|
log.startOperation("Generating validation report");
|
||||||
|
log.fileOperation("Generating report", source);
|
||||||
|
|
||||||
const result = await validate(source, validationOptions, config);
|
const result = await validate(source, validationOptions, config);
|
||||||
// Since source is a string, result will be ValidationResult, not ValidationResult[]
|
// 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
|
* Analyze references in an OpenAPI specification
|
||||||
*/
|
*/
|
||||||
export const analyzeDocumentReferences = async (source: string) => {
|
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 { parseOpenAPISpec, validateBasicStructure } from "./parser.js";
|
||||||
export { validateOpenAPISpec } from "./validator.js";
|
export { validateOpenAPISpec } from "./validator.js";
|
||||||
export { generateReport, saveReport } from "./reporter.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 {
|
export type {
|
||||||
ParsedSpec,
|
ParsedSpec,
|
||||||
@@ -226,7 +388,11 @@ export type {
|
|||||||
VarsityConfig,
|
VarsityConfig,
|
||||||
OpenAPIVersion,
|
OpenAPIVersion,
|
||||||
CLIResult,
|
CLIResult,
|
||||||
|
RecursiveValidationResult,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
|
// Export types from other modules
|
||||||
|
export type { ResolvedReference, ReferenceContext } from "./ref-resolver.js";
|
||||||
|
|
||||||
// Default export - create a default instance
|
// Default export - create a default instance
|
||||||
export default createVarsity();
|
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