diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f01749..c4264a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,20 +57,27 @@ jobs: ver=$(node -p "require('./package.json').version") echo "version=$ver" >> "$GITHUB_OUTPUT" - # Check if version was already bumped in this commit + # Check if version was already bumped - name: Check if version was bumped id: version-check shell: bash run: | - # Get the previous commit's version - PREV_VERSION=$(git show HEAD~1:package.json 2>/dev/null | node -p "require('./package.json').version" 2>/dev/null || echo "0.0.0") + # Get the latest GitHub release version + LATEST_RELEASE=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases/latest" | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8')).tag_name" 2>/dev/null || echo "") CURRENT_VERSION="${{ steps.current-version.outputs.version }}" - echo "Previous version: $PREV_VERSION" + echo "Latest release: $LATEST_RELEASE" echo "Current version: $CURRENT_VERSION" - if [ "$PREV_VERSION" != "$CURRENT_VERSION" ]; then - echo "Version was already bumped from $PREV_VERSION to $CURRENT_VERSION" + # Remove 'v' prefix from release tag for comparison + if [ -n "$LATEST_RELEASE" ]; 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 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index d4d5593..0000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,298 +0,0 @@ -# Development Guide - -This document outlines the development workflow, testing, and release process for the prettier-plugin-openapi package. - -## Prerequisites - -- [Bun](https://bun.sh/) (latest version) -- [Node.js](https://nodejs.org/) (v18 or higher) -- [Git](https://git-scm.com/) - -## Getting Started - -1. **Clone the repository** - ```bash - git clone https://github.com/lukehagar/prettier-plugin-openapi.git - cd prettier-plugin-openapi - ``` - -2. **Install dependencies** - ```bash - bun install - ``` - -3. **Verify setup** - ```bash - bun run validate - ``` - -## Development Workflow - -### Available Scripts - -- `bun run dev` - Start development mode with TypeScript watch -- `bun run build` - Build the project -- `bun run test` - Run all tests -- `bun run test:coverage` - Run tests with coverage report -- `bun run test:watch` - Run tests in watch mode -- `bun run lint` - Run ESLint -- `bun run lint:fix` - Fix ESLint issues automatically -- `bun run format` - Format code with Prettier -- `bun run format:check` - Check code formatting -- `bun run type-check` - Run TypeScript type checking -- `bun run validate` - Run all validation checks (type-check, lint, test) -- `bun run clean` - Clean build artifacts - -### Code Quality - -The project uses several tools to maintain code quality: - -- **ESLint** - Code linting with TypeScript support -- **Prettier** - Code formatting -- **TypeScript** - Type checking - -### Commit Message Format - -This project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification: - -``` -[optional scope]: - -[optional body] - -[optional footer(s)] -``` - -Types: -- `feat`: A new feature -- `fix`: A bug fix -- `docs`: Documentation changes -- `style`: Code style changes (formatting, etc.) -- `refactor`: Code refactoring -- `test`: Adding or updating tests -- `chore`: Maintenance tasks - -Examples: -``` -feat: add support for OpenAPI 3.1 -fix: correct key ordering for components -docs: update README with usage examples -``` - -## Testing - -### Running Tests - -```bash -# Run all tests -bun run test - -# Run tests with coverage -bun run test:coverage - -# Run tests in watch mode -bun run test:watch - -# Run tests for CI -bun run test:ci -``` - -### Test Structure - -- `test/plugin.test.ts` - Core plugin functionality tests -- `test/file-detection.test.ts` - File detection and parsing tests -- `test/integration.test.ts` - Integration tests with real OpenAPI files -- `test/key-ordering.test.ts` - Key ordering and sorting tests -- `test/options.test.ts` - Plugin options and configuration tests -- `test/vendor.test.ts` - Vendor extension tests -- `test/custom-extensions.test.ts` - Custom extension tests - -### Adding Tests - -When adding new features or fixing bugs: - -1. Write tests first (TDD approach) -2. Ensure tests cover edge cases -3. Add integration tests for complex features -4. Update existing tests if behavior changes - -## Building - -### Development Build - -```bash -bun run dev -``` - -This starts TypeScript in watch mode, automatically rebuilding when files change. - -### Production Build - -```bash -bun run build -``` - -This creates a production build in the `dist/` directory. - -### Build Verification - -After building, verify the output: - -```bash -# Check build artifacts -ls -la dist/ - -# Test the built package -node -e "console.log(require('./dist/index.js'))" -``` - -## Release Process - -### Version Management - -The project uses semantic versioning (semver): - -- **Patch** (1.0.1): Bug fixes (automated via GitHub Actions) -- **Minor** (1.1.0): New features (manual) -- **Major** (2.0.0): Breaking changes (manual) - -### Automated Releases - -Releases are automatically triggered on every push to main: - -1. **Smart versioning** - - Checks if the current version already exists on NPM - - If version exists: bumps patch version and publishes - - If version doesn't exist: publishes current version - - Runs tests and linting before publishing - -2. **Automatic process** - - Every push to the `main` branch triggers the release workflow - - The workflow will automatically: - - Run tests and linting - - Check NPM for existing version - - Bump patch version if needed - - Build and publish to NPM - - Create GitHub release with commit message - -### Manual Minor/Major Releases - -For minor or major releases: - -1. **Update version manually** - ```bash - # For minor release - bun run version:minor - - # For major release - bun run version:major - ``` - -2. **Push to main** - ```bash - git push origin main - ``` - -3. **Automated release** - - The release workflow will automatically: - - Detect the new version - - Build and test the package - - Publish to NPM - - Create GitHub release - -## CI/CD Pipeline - -### GitHub Actions Workflows - -- **CI Pipeline** (`.github/workflows/ci.yml`) - - Runs on every push and PR - - Tests on multiple Node.js and Bun versions - - Runs linting, type checking, and tests - - Generates coverage reports - - Builds the package - - Runs security audits - -- **Release Pipeline** (`.github/workflows/release.yml`) - - Runs on every push to main - - Smart versioning: checks NPM for existing versions - - Automatically bumps patch version if needed - - Builds, tests, and publishes to NPM - - Creates GitHub release with commit message - -### Required Secrets - -Set up the following secrets in your GitHub repository: - -- `NPM_TOKEN`: NPM authentication token for publishing -- `SNYK_TOKEN`: Snyk token for security scanning (optional) - -## Troubleshooting - -### Common Issues - -1. **Build failures** - - Check TypeScript errors: `bun run type-check` - - Verify all dependencies are installed: `bun install` - -2. **Test failures** - - Run tests individually to isolate issues - - Check test setup and mocks - -3. **Linting errors** - - Run `bun run lint:fix` to auto-fix issues - - Check ESLint configuration - -4. **Release issues** - - Check if version already exists on NPM - - Verify NPM_TOKEN secret is set correctly - - Check workflow logs for specific error messages - -### Debug Mode - -Enable debug logging: - -```bash -DEBUG=prettier-plugin-openapi:* bun run test -``` - -## Contributing - -1. Fork the repository -2. Create a feature branch: `git checkout -b feature/your-feature` -3. Make your changes -4. Add tests for new functionality -5. Run the full validation: `bun run validate` -6. Commit with conventional commit format -7. Push and create a pull request - -### Pull Request Process - -1. Ensure all CI checks pass -2. Request review from maintainers -3. Address feedback and update PR -4. Merge after approval - -## Performance Considerations - -- The plugin processes files in memory -- Large OpenAPI files (>1MB) may take longer to format -- Consider file size limits for optimal performance -- Monitor memory usage with very large files - -## Security - -- Regular dependency updates -- Security audits via GitHub Actions -- No external network requests during formatting -- Input validation for all parsed content - -## Release Workflow Benefits - -The new consolidated release workflow provides: - -- **Smart Versioning**: Automatically detects if versions exist on NPM -- **No Manual Intervention**: Patch releases happen automatically on every push -- **Efficient Publishing**: Only bumps versions when necessary -- **Comprehensive Testing**: Full test suite runs before every release -- **Automatic Documentation**: GitHub releases created with commit messages -- **Seamless Integration**: Works with both automatic and manual version bumps diff --git a/KEYS.md b/KEYS.md index 6a2ed9b..48c178a 100644 --- a/KEYS.md +++ b/KEYS.md @@ -2,701 +2,376 @@ This document lists all OpenAPI keys supported by the Prettier OpenAPI plugin, their ordering, and the reasoning from the source code comments. +## Sorting Philosophy + +The key ordering follows these general principles: + +- **Short, important info first**: Keys like `name`, `title`, `version`, `description` that provide quick context are placed at the top +- **Logical grouping**: Related keys are grouped together (e.g., `type` and `format`, numeric constraints, string constraints) +- **Size-based ordering**: Shorter, simpler keys come before longer, more complex ones +- **Reference keys at top**: `$ref` and similar reference keys are always first when present +- **Core content in middle**: The main schema/content definitions are in the middle sections +- **Composition at bottom**: Complex composition keys like `allOf`, `anyOf`, `oneOf` are placed near the end +- **Examples follow content**: Example keys are placed directly after the content they exemplify + +The overall philosophy is to make OpenAPI specifications more readable by placing the most important, frequently referenced and smallest information items at the top. + +Things generally made up of other things in the middle. + +And the most basic building blocks are generally at the bottom. + ## Root Level Keys -```typescript -export const RootKeys = [ - // Version identifiers - 'swagger', // Swagger 2.0 - 'openapi', // OpenAPI 3.0+ - - // Schema identifier - 'jsonSchemaDialect', // OpenAPI 3.1+ - - 'info', - 'externalDocs', - - // Common sense grouping for a server definition - 'schemes', // Swagger 2.0 - 'host', // Swagger 2.0 - 'basePath', // Swagger 2.0 - - // Typically short arrays, grouped together higher up - 'consumes', // Swagger 2.0 - 'produces', // Swagger 2.0 - - // Servers is usually really short, and can be helpful to see at the top for quick reference - 'servers', // OpenAPI 3.0+ (replaces host, basePath, schemes in 2.0) - - // Security is tiny, keep it at the top. - 'security', - - // Tags are often fairly long, but given that its a fairly core organizational feature, it's helpful to see at the top for quick reference - 'tags', - - // Paths are usually the longest block, unless components are used heavily, in which case it can be fairly short. - 'paths', - - // Webhooks are very often a short list, if its included at all, but depending on API structure and usage it can be quite long, having it below paths but above components seems like good placement.. - 'webhooks', // OpenAPI 3.1+ - - // Components is usually the longest block when it's heavily used, due to it having sections for reuse in most all other sections. - 'components', // OpenAPI 3.0+ (replaces definitions, parameters, responses, securityDefinitions in 2.0) - - 'definitions', // Swagger 2.0 - 'parameters', // Swagger 2.0 - 'responses', // Swagger 2.0 - 'securityDefinitions', // Swagger 2.0 -] as const; -``` +1. **swagger** - version identifier +2. **openapi** - version identifier +3. **jsonSchemaDialect** - schema identifier +4. **info** - API information +5. **externalDocs** - External documentation links +6. **schemes** - server schemes (common sense grouping for server definition) +7. **host** - server host +8. **basePath** - server base path +9. **consumes** - content types (typically short arrays, grouped together higher up) +10. **produces** - content types (typically short arrays, grouped together higher up) +11. **servers** - server definitions (replaces host, basePath, schemes in 2.0). Usually really short, and can be helpful to see at the top for quick reference +12. **security** - Security requirements (tiny, keep it at the top) +13. **tags** - API tags (often fairly long, but given that it's a fairly core organizational feature, it's helpful to see at the top for quick reference) +14. **paths** - API paths (usually the longest block, unless components are used heavily, in which case it can be fairly short) +15. **webhooks** - webhooks (very often a short list, if included at all, but depending on API structure and usage it can be quite long, having it below paths but above components seems like good placement) +16. **components** - reusable components (replaces definitions, parameters, responses, securityDefinitions in 2.0). Usually the longest block when it's heavily used, due to it having sections for reuse in most all other sections +17. **definitions** - schema definitions +18. **parameters** - global parameters +19. **responses** - global responses +20. **securityDefinitions** - security definitions ## Info Section Keys -```typescript -export const InfoKeys = [ - // Title is just a name, usually a single short line. - 'title', - - // Version is a usually a tiny string, and should be at the top. - 'version', - - // Summary to me has always been a shorter description, seems appropriate to have it above description. - 'summary', // OpenAPI 3.1+ - - // Description is usually longer if its included alongside a summary. - // I have seen everything from a single line to a veriatable novel. - 'description', - - // Terms of Service is usually a single line, and should be at the top. - 'termsOfService', - - // Contact and license are multi-line objects when included, so they should be at the bottom. - 'contact', - 'license', -] as const; -``` +1. **title** - API title (just a name, usually a single short line) +2. **version** - API version (usually a tiny string, and should be at the top) +3. **summary** - API summary (shorter description, seems appropriate to have it above description) +4. **description** - API description (usually longer if included alongside a summary. I have seen everything from a single line to a veritable novel) +5. **termsOfService** - Terms of service (usually a single line, and should be at the top) +6. **contact** - Contact information (multi-line object when included, so it should be at the bottom) +7. **license** - License information (multi-line object when included, so it should be at the bottom) ## Contact Keys -```typescript -// This key order should not require explaination. -// If it does let me know and I'll block you. -export const ContactKeys = [ - 'name', - 'email', - 'url', -] as const; -``` +1. **name** - Contact name +2. **email** - Contact email +3. **url** - Contact URL + +*This key order should not require explanation. If it does let me know and I'll block you.* ## License Keys -```typescript -export const LicenseKeys = [ - 'name', - 'identifier', - 'url', -] as const; -``` +1. **name** - License name +2. **identifier** - License identifier +3. **url** - License URL ## Components Section Keys -```typescript -// A sane ordering for components. -export const ComponentsKeys = [ - // Security is almost alwasy present, and very short, put it at the top. - 'securitySchemes', +*A sane ordering for components.* - // I have never actually seen path items included in a specification. - // That being said, I think the general philosophy of larger items at the top, - // with smaller more atomic items used to make up the larger items at the bottom, makes sense. - 'pathItems', // OpenAPI 3.1+ - - // Parameters can be larger, especially in larger APIs with extremely consistent usage patterns, but almost always shorter than schemas. - 'parameters', - - // Headers are basically just more parameters. - 'headers', - - // Request bodies are almost never used, I believe this is because the request bodies are usually so different from endpoint to endpoint. - // However, if they are used, Specifying them at this level seems reasonable. - 'requestBodies', - - // Responses are usually a smaller list, and often only used for global error responses. - 'responses', - - // Callbacks are essentially another kind of response. - 'callbacks', - - // Links are programatic ways to link endpoints together. - 'links', - - // Schemas are frequently the largest block, and are the building blocks that make up most every other section. - 'schemas', - - // Examples are fairly free form, and logically would be used in schemas, so it make sense to be at the bottom. - 'examples', -] as const; -``` +1. **securitySchemes** - Security schemes (almost always present, and very short, put it at the top) +2. **pathItems** - path items (I have never actually seen path items included in a specification. That being said, I think the general philosophy of larger items at the top, with smaller more atomic items used to make up the larger items at the bottom, makes sense) +3. **parameters** - Global parameters (can be larger, especially in larger APIs with extremely consistent usage patterns, but almost always shorter than schemas) +4. **headers** - Global headers (basically just more parameters) +5. **requestBodies** - Global request bodies (almost never used, I believe this is because the request bodies are usually so different from endpoint to endpoint. However, if they are used, specifying them at this level seems reasonable) +6. **responses** - Global responses (usually a smaller list, and often only used for global error responses) +7. **callbacks** - Global callbacks (essentially another kind of response) +8. **links** - Global links (programmatic ways to link endpoints together) +9. **schemas** - Global schemas (frequently the largest block, and are the building blocks that make up most every other section) +10. **examples** - Global examples (fairly free form, and logically would be used in schemas, so it makes sense to be at the bottom) ## Operation Keys -```typescript -export const OperationKeys = [ - // Important short info at a glance. - 'summary', - 'operationId', - 'description', - 'externalDocs', // OpenAPI 3.0+ - 'tags', - 'deprecated', - - // Security is a often short list, and is usually not included at the operation level. - 'security', - - // Servers is a often short list, and is usually not included at the operation level. - 'servers', // OpenAPI 3.0+ - - 'consumes', // Swagger 2.0 - 'produces', // Swagger 2.0 - - // Parameters are ideally added first via $ref, for situations like pagination, and then single endpoint specific parameters inline after. - 'parameters', - - // Request body is going to be shorter that responses, unless the responses are all `$ref`s - 'requestBody', // OpenAPI 3.0+ - - // Responses come after the request because obviously. - 'responses', - - // Callbacks are essentially another kind of response. - 'callbacks', // OpenAPI 3.0+ - - // Schemes should never have been included at this level, its just silly, but if they are, put them at the bottom. - 'schemes', // Swagger 2.0 -] as const; -``` +1. **summary** - Operation summary (important short info at a glance) +2. **operationId** - Operation ID (important short info at a glance) +3. **description** - Operation description (important short info at a glance) +4. **externalDocs** - external documentation (important short info at a glance) +5. **tags** - Operation tags (important short info at a glance) +6. **deprecated** - Deprecation flag (important short info at a glance) +7. **security** - Operation security (often short list, and is usually not included at the operation level) +8. **servers** - operation servers (often short list, and is usually not included at the operation level) +9. **consumes** - content types +10. **produces** - content types +11. **parameters** - Operation parameters (ideally added first via $ref, for situations like pagination, and then single endpoint specific parameters inline after) +12. **requestBody** - request body (going to be shorter than responses, unless the responses are all `$ref`s) +13. **responses** - Operation responses (come after the request because obviously) +14. **callbacks** - callbacks (essentially another kind of response) +15. **schemes** - schemes (should never have been included at this level, it's just silly, but if they are, put them at the bottom) ## Parameter Keys -```typescript -export const ParameterKeys = [ - // Important short info at a glance. - 'name', - 'description', - 'in', - 'required', - 'deprecated', - - // Semantic formatting options for parameters. - 'allowEmptyValue', - 'style', - 'explode', - 'allowReserved', - - // Schema is the core of the parameter, and specifies what the parameter actually is. - 'schema', - - // Content is similar to schema, and is typically only used for more complex parameters. - 'content', // OpenAPI 3.0+ - - // Type and format are the most common schema keys, and should be always be paired together. - 'type', // Swagger 2.0 - 'format', // Swagger 2.0 - - // When type is array, items should be present. - // collectionFormat is the array equivalent of format. - 'items', // Swagger 2.0 - 'collectionFormat', // Swagger 2.0 - - // Default is the default value of the parameter when that parameter is not specified. - 'default', // Swagger 2.0 - - // Numeric parameter constraints grouped together - // Min before max, multipleOf in the middle, since its essentially steps between. - 'minimum', // Swagger 2.0 - 'exclusiveMinimum', // Swagger 2.0 - 'multipleOf', // Swagger 2.0 - 'maximum', // Swagger 2.0 - 'exclusiveMaximum', // Swagger 2.0 - - // String parameter constraints - 'pattern', // Swagger 2.0 - 'minLength', // Swagger 2.0 - 'maxLength', // Swagger 2.0 - - // Array parameter constraints - 'minItems', // Swagger 2.0 - 'maxItems', // Swagger 2.0 - 'uniqueItems', // Swagger 2.0 - - // Enum is a strict list of allowed values for the parameter. - 'enum', // Swagger 2.0 - - // Example and examples are perfect directly below the schema. - 'example', - 'examples', -] as const; -``` +1. **name** - Parameter name (important short info at a glance) +2. **description** - Parameter description (important short info at a glance) +3. **in** - Parameter location (important short info at a glance) +4. **required** - Required flag (important short info at a glance) +5. **deprecated** - Deprecation flag (important short info at a glance) +6. **allowEmptyValue** - Allow empty values (semantic formatting options for parameters) +7. **style** - Parameter style (semantic formatting options for parameters) +8. **explode** - Explode flag (semantic formatting options for parameters) +9. **allowReserved** - Allow reserved characters (semantic formatting options for parameters) +10. **schema** - Parameter schema (the core of the parameter, and specifies what the parameter actually is) +11. **content** - content (similar to schema, and is typically only used for more complex parameters) +12. **type** - parameter type (the most common schema keys, and should always be paired together) +13. **format** - parameter format (the most common schema keys, and should always be paired together) +14. **items** - array items (when type is array, items should be present) +15. **collectionFormat** - collection format (the array equivalent of format) +16. **default** - default value (the default value of the parameter when that parameter is not specified) +17. **minimum** - minimum value (numeric parameter constraints grouped together) +18. **exclusiveMinimum** - exclusive minimum (numeric parameter constraints grouped together) +19. **multipleOf** - multiple of (numeric parameter constraints grouped together) +20. **maximum** - maximum value (numeric parameter constraints grouped together) +21. **exclusiveMaximum** - exclusive maximum (numeric parameter constraints grouped together) +22. **pattern** - pattern (string parameter constraints) +23. **minLength** - minimum length (string parameter constraints) +24. **maxLength** - maximum length (string parameter constraints) +25. **minItems** - minimum items (array parameter constraints) +26. **maxItems** - maximum items (array parameter constraints) +27. **uniqueItems** - unique items (array parameter constraints) +28. **enum** - enum values (a strict list of allowed values for the parameter) +29. **example** - Parameter example (perfect directly below the schema) +30. **examples** - Parameter examples (perfect directly below the schema) ## Schema Keys -```typescript -export const SchemaKeys = [ - // $ref should always be at the top, because when its included there are at most 2 other keys that are present. - '$ref', // JSON Schema draft - - // When $id is included it's used as a kind of name, or an id if you will, and should be at the top. - '$id', // JSON Schema draft - - // These JSON Schema draft keys are rarely used in my experience. - // They seem to all be extremely short, so are fine to be at the top. - // Anybody who uses them a lot feel free to weigh in here and make an argument for a different placement. - - // Schema and Vocabulary appear to be universally be external links, so should be grouped. - '$schema', // JSON Schema draft - '$vocabulary', // JSON Schema draft - - // I have no idea on the practical use of these keys, especially in this context, - // but someone using them would likely want them close to the top for reference. - '$anchor', // JSON Schema draft - '$dynamicAnchor', // JSON Schema draft - '$dynamicRef', // JSON Schema draft - '$comment', // JSON Schema draft - '$defs', // JSON Schema draft - '$recursiveAnchor', // JSON Schema draft - '$recursiveRef', // JSON Schema draft - - // This is where most non $ref schemas will begin. - - // The info section of the schema. - 'title', - // description and externalDocs logically come after the title, - // describing it in more and more detail. - 'description', - 'externalDocs', - // Deprecated is a good at a glance key, and stays at the top. - 'deprecated', - - // This next section describes the type and how it behaves. - - // Type and format should always be grouped together. - 'type', - 'format', - - // Content schema, media type, and encoding are all related to the content of the schema, - // and are similar to format. They should be grouped together. - 'contentSchema', // JSON Schema draft - 'contentMediaType', // JSON Schema draft - 'contentEncoding', // JSON Schema draft - - // Nullable is like format, it specifies how the type can behave, - // and in more recent versions of OpenAPI its directly included in the type field. - 'nullable', - - // Enum and const are both static entries of allowed values for the schema. - // They should be grouped together. - 'const', - 'enum', - - // The default value of the schema when that schema is not specified. - // Same as with parameters, but at the schema level. - 'default', - - // ReadOnly and WriteOnly are boolean flags and should be grouped together. - 'readOnly', - 'writeOnly', - - // Examples when included should be directly below what they are examples of. - 'example', - 'examples', - - // Numeric constraints grouped together - // Min before max, multipleOf in the middle, since its steps between them. - 'minimum', - 'exclusiveMinimum', - 'multipleOf', - 'maximum', - 'exclusiveMaximum', - - // String constraints grouped together - 'pattern', - 'minLength', - 'maxLength', - - // Array constraints grouped together - 'uniqueItems', - 'minItems', - 'maxItems', - 'items', - - // Prefix items describes tuple like array behavior. - 'prefixItems', // JSON Schema draft - - // Contains specifies a subschema that must be present in the array. - // Min and max contains specify the match occurrence constraints for the contains key. - 'contains', // JSON Schema draft - 'minContains', // JSON Schema draft - 'maxContains', // JSON Schema draft - - // After accounting for Items, prefixItems, and contains, unevaluatedItems specifies if additional items are allowed. - // This key is either a boolean or a subschema. - // Behaves the same as additionalProperties at the object level. - 'unevaluatedItems', // JSON Schema draft - - // Object constraints grouped together - // min and max properties specify how many properties an object can have. - 'minProperties', - 'maxProperties', - - // Pattern properties are a way to specify a pattern and schemas for properties that match that pattern. - // Additional properties are a way to specify if additional properties are allowed and if so, how they are shaped. - 'patternProperties', - 'additionalProperties', - - // Properties are the actual keys and schemas that make up the object. - 'properties', - - // Required is a list of those properties that are required to be present in the object. - 'required', - - // Unevaluated properties specifies if additional properties are allowed after applying all other validation rules. - // This is more powerful than additionalProperties as it considers the effects of allOf, anyOf, oneOf, etc. - 'unevaluatedProperties', // JSON Schema draft - - // Property names defines a schema that property names must conform to. - // This is useful for validating that all property keys follow a specific pattern or format. - 'propertyNames', // JSON Schema draft - - // Dependent required specifies properties that become required when certain other properties are present. - // This allows for conditional requirements based on the presence of specific properties. - 'dependentRequired', // JSON Schema draft - - // Dependent schemas defines schemas that apply when certain properties are present. - // This allows for conditional validation rules based on the presence of specific properties. - // For example, if a property is present, a certain other property must also be present, and match a certain schema. - 'dependentSchemas', // JSON Schema draft - - // Discriminator is a way to specify a property that differentiates between different types of objects. - // This is useful for polymorphic schemas, and should go above the schema composition keys. - 'discriminator', - - // Schema composition keys grouped together - // allOf, anyOf, oneOf, and not are all used to compose schemas from other schemas. - // allOf is a logical AND, - // anyOf is a logical OR, - // oneOf is a logical XOR, - // and not is a logical NOT. - 'allOf', - 'anyOf', - 'oneOf', - 'not', - - // Conditional keys grouped together - 'if', // JSON Schema draft - 'then', // JSON Schema draft - 'else', // JSON Schema draft - - // XML is a way to specify the XML serialization of the schema. - // This is useful for APIs that need to support XML serialization. - 'xml', -] as const; -``` +1. **$ref** - JSON Schema draft reference (should always be at the top, because when included there are at most 2 other keys that are present) +2. **$id** - JSON Schema draft ID (when included it's used as a kind of name, or an id if you will, and should be at the top) +3. **$schema** - JSON Schema draft schema (appears to be universally external links, so should be grouped) +4. **$vocabulary** - JSON Schema draft vocabulary (appears to be universally external links, so should be grouped) +5. **$anchor** - JSON Schema draft anchor (I have no idea on the practical use of these keys, especially in this context, but someone using them would likely want them close to the top for reference) +6. **$dynamicAnchor** - JSON Schema draft dynamic anchor (I have no idea on the practical use of these keys, especially in this context, but someone using them would likely want them close to the top for reference) +7. **$dynamicRef** - JSON Schema draft dynamic reference (I have no idea on the practical use of these keys, especially in this context, but someone using them would likely want them close to the top for reference) +8. **$comment** - JSON Schema draft comment (I have no idea on the practical use of these keys, especially in this context, but someone using them would likely want them close to the top for reference) +9. **$defs** - JSON Schema draft definitions (I have no idea on the practical use of these keys, especially in this context, but someone using them would likely want them close to the top for reference) +10. **$recursiveAnchor** - JSON Schema draft recursive anchor (I have no idea on the practical use of these keys, especially in this context, but someone using them would likely want them close to the top for reference) +11. **$recursiveRef** - JSON Schema draft recursive reference (I have no idea on the practical use of these keys, especially in this context, but someone using them would likely want them close to the top for reference) +12. **title** - Schema title (the info section of the schema) +13. **description** - Schema description (description and externalDocs logically come after the title, describing it in more and more detail) +14. **externalDocs** - Schema external documentation (description and externalDocs logically come after the title, describing it in more and more detail) +15. **deprecated** - Deprecation flag (a good at a glance key, and stays at the top) +16. **type** - Schema type (type and format should always be grouped together) +17. **format** - Schema format (type and format should always be grouped together) +18. **contentSchema** - JSON Schema draft content schema (content schema, media type, and encoding are all related to the content of the schema, and are similar to format. They should be grouped together) +19. **contentMediaType** - JSON Schema draft content media type (content schema, media type, and encoding are all related to the content of the schema, and are similar to format. They should be grouped together) +20. **contentEncoding** - JSON Schema draft content encoding (content schema, media type, and encoding are all related to the content of the schema, and are similar to format. They should be grouped together) +21. **nullable** - Nullable flag (like format, it specifies how the type can behave, and in more recent versions of OpenAPI it's directly included in the type field) +22. **const** - Constant value (enum and const are both static entries of allowed values for the schema. They should be grouped together) +23. **enum** - Enum values (enum and const are both static entries of allowed values for the schema. They should be grouped together) +24. **default** - Default value (the default value of the schema when that schema is not specified. Same as with parameters, but at the schema level) +25. **readOnly** - Read-only flag (readOnly and writeOnly are boolean flags and should be grouped together) +26. **writeOnly** - Write-only flag (readOnly and writeOnly are boolean flags and should be grouped together) +27. **example** - Schema example (examples when included should be directly below what they are examples of) +28. **examples** - Schema examples (examples when included should be directly below what they are examples of) +29. **minimum** - Minimum value (numeric constraints grouped together) +30. **exclusiveMinimum** - Exclusive minimum (numeric constraints grouped together) +31. **multipleOf** - Multiple of (numeric constraints grouped together) +32. **maximum** - Maximum value (numeric constraints grouped together) +33. **exclusiveMaximum** - Exclusive maximum (numeric constraints grouped together) +34. **pattern** - Pattern (string constraints grouped together) +35. **minLength** - Minimum length (string constraints grouped together) +36. **maxLength** - Maximum length (string constraints grouped together) +37. **uniqueItems** - Unique items (array constraints grouped together) +38. **minItems** - Minimum items (array constraints grouped together) +39. **maxItems** - Maximum items (array constraints grouped together) +40. **items** - Array items (array constraints grouped together) +41. **prefixItems** - JSON Schema draft prefix items (describes tuple like array behavior) +42. **contains** - JSON Schema draft contains (specifies a subschema that must be present in the array) +43. **minContains** - JSON Schema draft minimum contains (min and max contains specify the match occurrence constraints for the contains key) +44. **maxContains** - JSON Schema draft maximum contains (min and max contains specify the match occurrence constraints for the contains key) +45. **unevaluatedItems** - JSON Schema draft unevaluated items (after accounting for Items, prefixItems, and contains, unevaluatedItems specifies if additional items are allowed. This key is either a boolean or a subschema. Behaves the same as additionalProperties at the object level) +46. **minProperties** - Minimum properties (object constraints grouped together) +47. **maxProperties** - Maximum properties (object constraints grouped together) +48. **patternProperties** - Pattern properties (a way to specify a pattern and schemas for properties that match that pattern) +49. **additionalProperties** - Additional properties (a way to specify if additional properties are allowed and if so, how they are shaped) +50. **properties** - Object properties (the actual keys and schemas that make up the object) +51. **required** - Required properties (a list of those properties that are required to be present in the object) +52. **unevaluatedProperties** - JSON Schema draft unevaluated properties (specifies if additional properties are allowed after applying all other validation rules. This is more powerful than additionalProperties as it considers the effects of allOf, anyOf, oneOf, etc.) +53. **propertyNames** - JSON Schema draft property names (defines a schema that property names must conform to. This is useful for validating that all property keys follow a specific pattern or format) +54. **dependentRequired** - JSON Schema draft dependent required (specifies properties that become required when certain other properties are present. This allows for conditional requirements based on the presence of specific properties) +55. **dependentSchemas** - JSON Schema draft dependent schemas (defines schemas that apply when certain properties are present. This allows for conditional validation rules based on the presence of specific properties. For example, if a property is present, a certain other property must also be present, and match a certain schema) +56. **discriminator** - Discriminator (a way to specify a property that differentiates between different types of objects. This is useful for polymorphic schemas, and should go above the schema composition keys) +57. **allOf** - All of composition (allOf, anyOf, oneOf, and not are all used to compose schemas from other schemas. allOf is a logical AND) +58. **anyOf** - Any of composition (allOf, anyOf, oneOf, and not are all used to compose schemas from other schemas. anyOf is a logical OR) +59. **oneOf** - One of composition (allOf, anyOf, oneOf, and not are all used to compose schemas from other schemas. oneOf is a logical XOR) +60. **not** - Not composition (allOf, anyOf, oneOf, and not are all used to compose schemas from other schemas. not is a logical NOT) +61. **if** - JSON Schema draft conditional if (conditional keys grouped together) +62. **then** - JSON Schema draft conditional then (conditional keys grouped together) +63. **else** - JSON Schema draft conditional else (conditional keys grouped together) +64. **xml** - XML serialization (a way to specify the XML serialization of the schema. This is useful for APIs that need to support XML serialization) ## Response Keys -```typescript -export const ResponseKeys = [ - // Description is a good at a glance key, and stays at the top. - 'description', - - // Headers are a common key, and should be at the top. - 'headers', - - // Schema and content are the core shape of the response. - 'schema', // Swagger 2.0 - 'content', // OpenAPI 3.0+ - - // Examples are of the schema, and should be directly below the schema. - 'examples', // Swagger 2.0 - - // Links are programatic ways to link responses together. - 'links', // OpenAPI 3.0+ -] as const; -``` +1. **description** - Response description (a good at a glance key, and stays at the top) +2. **headers** - Response headers (a common key, and should be at the top) +3. **schema** - response schema (the core shape of the response) +4. **content** - response content (the core shape of the response) +5. **examples** - response examples (examples are of the schema, and should be directly below the schema) +6. **links** - response links (programmatic ways to link responses together) ## Security Scheme Keys -```typescript -export const SecuritySchemeKeys = [ - // Good at a glance keys. - 'name', - 'description', - - // The primary type of this security scheme - 'type', - 'in', - 'scheme', - - // If scheme is bearer, bearerFormat is the format of the bearer token. - // Should be directly below scheme. - 'bearerFormat', - - // If scheme is openIdConnect, openIdConnectUrl is the URL of the OpenID Connect server. - 'openIdConnectUrl', - - // Flows are the different ways to authenticate with this security scheme. - 'flows', // OpenAPI 3.0+ - - 'flow', // Swagger 2.0 - 'authorizationUrl', // Swagger 2.0 - 'tokenUrl', // Swagger 2.0 - 'scopes', // Swagger 2.0 -] as const; -``` +1. **name** - Security scheme name (good at a glance keys) +2. **description** - Security scheme description (good at a glance keys) +3. **type** - Security scheme type (the primary type of this security scheme) +4. **in** - Security scheme location (the primary type of this security scheme) +5. **scheme** - Security scheme (the primary type of this security scheme) +6. **bearerFormat** - Bearer token format (if scheme is bearer, bearerFormat is the format of the bearer token. Should be directly below scheme) +7. **openIdConnectUrl** - OpenID Connect URL (if scheme is openIdConnect, openIdConnectUrl is the URL of the OpenID Connect server) +8. **flows** - OAuth flows (the different ways to authenticate with this security scheme) +9. **flow** - OAuth flow +10. **authorizationUrl** - authorization URL +11. **tokenUrl** - token URL +12. **scopes** - OAuth scopes ## OAuth Flow Keys -```typescript -export const OAuthFlowKeys = [ - // Authorization URL is where the client can get an authorization code. - 'authorizationUrl', - - // Token URL is where the client can get a token. - 'tokenUrl', - - // Refresh URL is where the client can refresh a token. - 'refreshUrl', - - // Scopes are the different scopes that can be used with this security scheme. - 'scopes', -] as const; -``` +1. **authorizationUrl** - Authorization URL (where the client can get an authorization code) +2. **tokenUrl** - Token URL (where the client can get a token) +3. **refreshUrl** - Refresh URL (where the client can refresh a token) +4. **scopes** - OAuth scopes (the different scopes that can be used with this security scheme) ## Server Keys -```typescript -export const ServerKeys = [ - // Name first because obviously. - 'name', // OpenAPI 3.2+ - - // Description so you know what you are looking at. - 'description', - - // URL is the URL of the server. - 'url', - - // Variables are the different variables that are present in the URL. - 'variables', -] as const; -``` +1. **name** - server name (name first because obviously) +2. **description** - Server description (description so you know what you are looking at) +3. **url** - Server URL (the URL of the server) +4. **variables** - Server variables (the different variables that are present in the URL) ## Server Variable Keys -```typescript -export const ServerVariableKeys = [ - // Description so you know what you are looking at. - 'description', - - // Default is the default value of the variable when that variable is not specified. - // IMO this should be optional, but I was not consulted. - 'default', - - // Enum is a static list of allowed values for the variable. - 'enum', -] as const; -``` +1. **description** - Variable description (description so you know what you are looking at) +2. **default** - Variable default value (the default value of the variable when that variable is not specified. IMO this should be optional, but I was not consulted) +3. **enum** - Variable enum values (a static list of allowed values for the variable) ## Tag Keys -```typescript -export const TagKeys = [ - // Name first because obviously. - 'name', - - // Description so you know what you are looking at. - 'description', - - // External docs should be like an extension of the description. - 'externalDocs', -] as const; -``` +1. **name** - Tag name (name first because obviously) +2. **description** - Tag description (description so you know what you are looking at) +3. **externalDocs** - Tag external documentation (external docs should be like an extension of the description) ## External Documentation Keys -```typescript -// The only sane key order, fight me. -export const ExternalDocsKeys = [ - 'description', - 'url', -] as const; -``` +*The only sane key order, fight me.* + +1. **description** - External documentation description +2. **url** - External documentation URL ## Webhook Keys -```typescript -// This seems like an obvious order given out running philosophy. -export const WebhookKeys = [ - 'summary', - 'operationId', - 'description', - 'deprecated', - 'tags', - 'security', - 'servers', - 'parameters', - 'requestBody', - 'responses', - 'callbacks', -] as const; -``` +*This seems like an obvious order given our running philosophy.* + +1. **summary** - Webhook summary +2. **operationId** - Webhook operation ID +3. **description** - Webhook description +4. **deprecated** - Webhook deprecation flag +5. **tags** - Webhook tags +6. **security** - Webhook security +7. **servers** - Webhook servers +8. **parameters** - Webhook parameters +9. **requestBody** - Webhook request body +10. **responses** - Webhook responses +11. **callbacks** - Webhook callbacks ## Path Item Keys -```typescript -// Short blocks at the top, long at the bottom. -export const PathItemKeys = [ - '$ref', - 'summary', - 'description', - 'servers', - 'parameters', - 'get', - 'put', - 'post', - 'delete', - 'options', - 'head', - 'patch', - 'trace', -] as const; -``` +*Short blocks at the top, long at the bottom.* + +1. **$ref** - Path item reference +2. **summary** - Path item summary +3. **description** - Path item description +4. **servers** - Path item servers +5. **parameters** - Path item parameters +6. **get** - GET operation +7. **put** - PUT operation +8. **post** - POST operation +9. **patch** - PATCH operation +10. **delete** - DELETE operation +11. **options** - OPTIONS operation +12. **head** - HEAD operation +13. **trace** - TRACE operation ## Request Body Keys -```typescript -// Simple/short first -export const RequestBodyKeys = [ - 'description', - 'required', - 'content', -] as const; -``` +*Simple/short first* + +1. **description** - Request body description +2. **required** - Required flag +3. **content** - Request body content ## Media Type Keys -```typescript -// These are a bit trickier, all seem rather long in context. -// I'll with this order since it seems less to more complex. -export const MediaTypeKeys = [ - 'schema', - 'example', - 'examples', - 'encoding', -] as const; -``` +*These are a bit trickier, all seem rather long in context. I'll go with this order since it seems less to more complex.* + +1. **schema** - Media type schema +2. **example** - Media type example +3. **examples** - Media type examples +4. **encoding** - Media type encoding ## Encoding Keys -```typescript -export const EncodingKeys = [ - // Content type is just MIME type. - 'contentType', - - // Style, explode, and allowReserved are simple string or boolean values. - 'style', - 'explode', - 'allowReserved', - - // Headers is longer, put it at the bottom. - 'headers', -] as const; -``` +1. **contentType** - Content type (just MIME type) +2. **style** - Encoding style (simple string or boolean values) +3. **explode** - Explode flag (simple string or boolean values) +4. **allowReserved** - Allow reserved characters (simple string or boolean values) +5. **headers** - Encoding headers (longer, put it at the bottom) ## Header Keys -```typescript -export const HeaderKeys = [ - // Description is a good at a glance key, and stays at the top. - 'description', - 'required', - 'deprecated', - - 'schema', - 'content', - 'type', - 'format', - 'style', - 'explode', - 'enum', - 'default', - 'example', - 'examples', - - // Array keys grouped together - 'items', - 'collectionFormat', - // Array constraints grouped together - 'maxItems', - 'minItems', - 'uniqueItems', - - // Numeric constraints grouped together - 'minimum', - 'multipleOf', - 'exclusiveMinimum', - 'maximum', - 'exclusiveMaximum', - - // String constraints grouped together - 'pattern', - 'minLength', - 'maxLength', -] as const; -``` +1. **description** - Header description (a good at a glance key, and stays at the top) +2. **required** - Required flag +3. **deprecated** - Deprecation flag +4. **schema** - Header schema +5. **content** - Header content +6. **type** - Header type +7. **format** - Header format +8. **style** - Header style +9. **explode** - Explode flag +10. **enum** - Header enum values +11. **default** - Default value +12. **example** - Header example +13. **examples** - Header examples +14. **items** - Array items (array keys grouped together) +15. **collectionFormat** - Collection format (array keys grouped together) +16. **maxItems** - Maximum items (array constraints grouped together) +17. **minItems** - Minimum items (array constraints grouped together) +18. **uniqueItems** - Unique items (array constraints grouped together) +19. **minimum** - Minimum value (numeric constraints grouped together) +20. **multipleOf** - Multiple of (numeric constraints grouped together) +21. **exclusiveMinimum** - Exclusive minimum (numeric constraints grouped together) +22. **maximum** - Maximum value (numeric constraints grouped together) +23. **exclusiveMaximum** - Exclusive maximum (numeric constraints grouped together) +24. **pattern** - Pattern (string constraints grouped together) +25. **minLength** - Minimum length (string constraints grouped together) +26. **maxLength** - Maximum length (string constraints grouped together) ## Link Keys -```typescript -export const LinkKeys = [ - 'operationId', - 'description', - 'server', - 'operationRef', - 'parameters', - 'requestBody', -] as const; -``` +1. **operationId** - Link operation ID +2. **description** - Link description +3. **server** - Link server +4. **operationRef** - Link operation reference +5. **parameters** - Link parameters +6. **requestBody** - Link request body ## Example Keys -```typescript -export const ExampleKeys = [ - 'summary', - 'description', - 'value', - 'externalValue', -] as const; -``` +1. **summary** - Example summary +2. **description** - Example description +3. **value** - Example value +4. **externalValue** - External example value ## Discriminator Keys -```typescript -// Discriminator keys in preferred order (OpenAPI 3.0+) -export const DiscriminatorKeys = [ - 'propertyName', - 'mapping', -] as const; -``` +*Discriminator keys in preferred order ( +1. **propertyName** - Discriminator property name +2. **mapping** - Discriminator mapping ## XML Keys -```typescript -// XML keys in preferred order (OpenAPI 3.0+) -export const XMLKeys = [ - 'name', - 'namespace', - 'prefix', - 'attribute', - 'wrapped', -] as const; -``` +*XML keys in preferred order ( +1. **name** - XML name +2. **namespace** - XML namespace +3. **prefix** - XML prefix +4. **attribute** - XML attribute flag +5. **wrapped** - XML wrapped flag diff --git a/README.md b/README.md index 198453f..40224f1 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,18 @@ A Prettier plugin for formatting OpenAPI/Swagger JSON and YAML files with intell - ๐ŸŽจ **Consistent Formatting**: Applies consistent indentation and line breaks - ๐Ÿ”Œ **Vendor Extensions**: Programmatic loading of vendor-specific extensions - โšก **Fast**: Built with performance in mind using modern JavaScript -- ๐Ÿงช **Comprehensive Testing**: 99 tests with 94.62% line coverage +- ๐Ÿงช **Comprehensive Testing**: 142 tests with 95.69% line coverage - ๐Ÿš€ **CI/CD Ready**: Automated testing, building, and publishing - ๐Ÿ”’ **Strict Validation**: Properly rejects non-OpenAPI content -- ๐Ÿ“Š **High Quality**: ESLint, Prettier, and TypeScript for code quality +- ๐Ÿ“Š **High Quality**: Biome, Prettier, and TypeScript for code quality + +## Current Status + +โœ… **Production Ready**: Version 1.0.1 with comprehensive test coverage +โœ… **Modern Tooling**: Updated to use Biome for fast linting and formatting +โœ… **Comprehensive Testing**: 142 tests covering all major functionality +โœ… **High Performance**: Optimized for large OpenAPI files +โœ… **Active Development**: Regular updates and improvements ## Installation @@ -133,37 +141,6 @@ The plugin automatically sorts OpenAPI keys in the recommended order: > ๐Ÿ“– **Complete Key Reference**: For a comprehensive reference of all keys, their ordering, and detailed reasoning, see [KEYS.md](./KEYS.md). -### Top-level keys: -1. `openapi` / `swagger` -2. `info` -3. `servers` -4. `paths` -5. `components` -6. `security` -7. `tags` -8. `externalDocs` - -### Info section: -1. `title` -2. `summary` -3. `description` -4. `version` -5. `termsOfService` -6. `contact` -7. `license` - -### Components section: -1. `schemas` -2. `responses` -3. `parameters` -4. `examples` -5. `requestBodies` -6. `headers` -7. `securitySchemes` -8. `links` -9. `callbacks` -10. `pathItems` - ## Examples ### Monolithic File Structure @@ -247,38 +224,386 @@ get: $ref: '../components/schemas/User.yaml' ``` -## Vendor Extensions +## Vendor Extension Guide -The plugin supports vendor-specific extensions through a programmatic loading system: +### Adding Your Vendor Extensions -### Adding Vendor Extensions +The plugin supports a simple system for vendors to contribute custom extensions. -1. Create a TypeScript file in `src/extensions/vendor/` -2. Export your extensions using the provided API: +Here's how to add your vendor extensions: + +#### Step 1: Create Your Vendor Extension File + +Create a new TypeScript file in `src/extensions/vendor/your-vendor.ts`: ```typescript -// src/extensions/vendor/my-vendor.ts -import { defineVendorExtensions } from '../index'; +/** + * Your Vendor Extensions + * + * Your vendor extensions for OpenAPI formatting. + * Website: https://your-vendor.com + */ -export const extensions = defineVendorExtensions({ - 'top-level': (before, after) => ({ - 'x-my-custom-field': before('info'), - 'x-vendor-metadata': after('externalDocs') - }), - 'operation': (before, after) => ({ - 'x-rate-limit': before('responses'), - 'x-cache-ttl': after('deprecated') - }) +import { defineConfig } from "../index.js"; + +export const yourVendor = defineConfig({ + info: { + name: 'Your Vendor', + website: 'https://your-vendor.com', + support: 'support@your-vendor.com' + }, + extensions: { + // Define your extensions here + } }); ``` -### Automatic Discovery +#### Step 2: Register Your Vendor -Vendor extensions are automatically discovered and loaded at runtime. No manual imports required! +Add your vendor to the vendor loader in `src/extensions/vendor-loader.ts`: + +```typescript +// Import your vendor extension +import { yourVendor } from './vendor/your-vendor.js'; + +// Add to the vendorModules array +const vendorModules = [ + speakeasy, + postman, + redoc, + yourVendor // Add your vendor here +]; +``` + +#### Step 3: Define Your Extensions + +Use the `before()` and `after()` helper functions to position your extensions relative to standard OpenAPI keys. The system now provides **full IntelliSense support** with type-safe key suggestions: + +```typescript +extensions: { + 'top-level': (before, after) => { + // โœ… IntelliSense shows: 'swagger', 'openapi', 'info', 'paths', etc. + return { + 'x-your-vendor-sdk': before('info'), // โœ… Type-safe: 'info' is valid + 'x-your-vendor-auth': after('paths'), // โœ… Type-safe: 'paths' is valid + // 'x-invalid': before('invalidKey'), // โŒ TypeScript error: 'invalidKey' not valid + }; + }, + 'operation': (before, after) => { + // โœ… IntelliSense shows: 'summary', 'operationId', 'parameters', 'responses', etc. + return { + 'x-your-vendor-retries': after('parameters'), // โœ… Type-safe: 'parameters' is valid + 'x-your-vendor-timeout': before('responses'), // โœ… Type-safe: 'responses' is valid + }; + }, + 'schema': (before, after) => { + // โœ… IntelliSense shows: '$ref', 'title', 'type', 'format', 'example', etc. + return { + 'x-your-vendor-validation': after('type'), // โœ… Type-safe: 'type' is valid + 'x-your-vendor-example': after('example'), // โœ… Type-safe: 'example' is valid + }; + } +} +``` + +### ๐ŸŽฏ Enhanced IntelliSense Features + +The vendor extension system now provides comprehensive IntelliSense support: + +#### Type-Safe Key Suggestions +- **Context-aware autocomplete**: Each context shows only valid OpenAPI keys +- **Real-time validation**: TypeScript errors for invalid keys +- **Hover documentation**: Detailed information about each key's purpose + +#### Available Contexts with IntelliSense +- `'top-level'` โ†’ Shows: `swagger`, `openapi`, `info`, `paths`, `components`, etc. +- `'info'` โ†’ Shows: `title`, `version`, `description`, `contact`, `license`, etc. +- `'operation'` โ†’ Shows: `summary`, `operationId`, `parameters`, `responses`, etc. +- `'schema'` โ†’ Shows: `$ref`, `title`, `type`, `format`, `example`, etc. +- `'parameter'` โ†’ Shows: `name`, `description`, `in`, `required`, `schema`, etc. +- `'response'` โ†’ Shows: `description`, `headers`, `content`, `links` +- `'securityScheme'` โ†’ Shows: `type`, `description`, `name`, `in`, `scheme`, etc. +- `'server'` โ†’ Shows: `url`, `description`, `variables` +- `'tag'` โ†’ Shows: `name`, `description`, `externalDocs` +- `'externalDocs'` โ†’ Shows: `description`, `url` +- `'webhook'` โ†’ Shows: `summary`, `operationId`, `parameters`, `responses`, etc. +- `'definitions'` โ†’ Shows schema keys (Swagger 2.0) +- `'securityDefinitions'` โ†’ Shows security scheme keys (Swagger 2.0) + +#### Enhanced Helper Functions + +```typescript +import { createPositionHelpers } from "../index.js"; + +// Get enhanced helpers for a specific context +const helpers = createPositionHelpers('operation'); + +// Type-safe positioning +helpers.before('parameters'); // โœ… IntelliSense shows valid operation keys +helpers.after('responses'); // โœ… IntelliSense shows valid operation keys + +// Additional utilities +const availableKeys = helpers.getAvailableKeys(); // Get all valid keys +const isValid = helpers.isValidKey('summary'); // Check if key is valid +``` + +### Supported Contexts + +You can define extensions for these OpenAPI contexts: + +- `'top-level'` - Root OpenAPI document +- `'info'` - API information section +- `'operation'` - HTTP operations (GET, POST, etc.) +- `'parameter'` - Operation parameters +- `'schema'` - Data schemas +- `'response'` - Operation responses +- `'securityScheme'` - Security schemes +- `'server'` - Server definitions +- `'tag'` - API tags +- `'externalDocs'` - External documentation +- `'webhook'` - Webhook definitions +- `'definitions'` - Swagger 2.0 definitions +- `'securityDefinitions'` - Swagger 2.0 security definitions + +### Standard OpenAPI Keys Reference + +When positioning your extensions, you can reference these standard OpenAPI keys: + +#### Top-Level Keys +- `openapi`, `swagger`, `info`, `externalDocs`, `servers`, `security`, `tags`, `paths`, `webhooks`, `components` + +#### Info Section Keys +- `title`, `version`, `summary`, `description`, `termsOfService`, `contact`, `license` + +#### Operation Keys +- `summary`, `operationId`, `description`, `externalDocs`, `tags`, `deprecated`, `security`, `servers`, `parameters`, `requestBody`, `responses`, `callbacks` + +#### Schema Keys +- `$ref`, `title`, `description`, `type`, `format`, `enum`, `default`, `example`, `properties`, `required`, `items`, `allOf`, `anyOf`, `oneOf`, `not` + +#### Parameter Keys +- `name`, `description`, `in`, `required`, `deprecated`, `schema`, `content`, `style`, `explode`, `allowReserved`, `example` + +#### Response Keys +- `description`, `headers`, `content`, `links` + +#### Security Scheme Keys +- `type`, `description`, `name`, `in`, `scheme`, `bearerFormat`, `flows`, `openIdConnectUrl` + +#### Server Keys +- `url`, `description`, `variables` + +#### Tag Keys +- `name`, `description`, `externalDocs` + +#### External Docs Keys +- `description`, `url` + +#### Webhook Keys +- `summary`, `operationId`, `description`, `deprecated`, `tags`, `security`, `servers`, `parameters`, `requestBody`, `responses`, `callbacks` + +> ๐Ÿ“– **Complete Key Reference**: For a comprehensive reference of all keys, their ordering, and detailed reasoning, see [KEYS.md](./KEYS.md). + +### Positioning Your Extensions + +Use the helper functions to position your extensions: + +- `before(key)` - Position before a standard OpenAPI key +- `after(key)` - Position after a standard OpenAPI key + +#### Example: Positioning Extensions + +```typescript +'operation': (before, after) => { + return { + // Position before standard keys + 'x-your-vendor-auth': before('security'), + 'x-your-vendor-rate-limit': before('parameters'), + + // Position after standard keys + 'x-your-vendor-retries': after('parameters'), + 'x-your-vendor-timeout': after('responses'), + + // Position relative to other extensions + 'x-your-vendor-cache': after('x-your-vendor-retries'), + }; +} +``` + +### Extension Naming Convention + +Follow these naming conventions for your extensions: + +- Use your vendor prefix: `x-your-vendor-` +- Use descriptive names: `x-your-vendor-retries`, `x-your-vendor-timeout` +- Keep names consistent across contexts +- Use kebab-case for multi-word extensions + +### Testing Your Extensions + +1. **Build the project**: `bun run build` +2. **Run tests**: `bun test` +3. **Test with real OpenAPI files**: Create test files with your extensions +4. **Verify positioning**: Check that your extensions appear in the correct order + +### Extension Collision Detection + +The system automatically detects and warns about extension key collisions: + +``` +โš ๏ธ Extension collision detected! + Key: "x-common-extension" in context "operation" + Already defined by: Vendor A + Conflicting with: Vendor B + Using position from: Vendor A (5) + Ignoring position from: Vendor B (3) +``` + +### Advanced Type Safety + +The vendor extension system provides comprehensive TypeScript support: + +#### Type Definitions +```typescript +import { + type VendorExtensions, + type ContextExtensionFunction, + type OpenAPIContext, + type ExtensionKey +} from "../index.js"; + +// Type-safe extension configuration +const extensions: VendorExtensions = { + 'top-level': (before, after) => { + // before and after are type-safe for top-level keys + return { + 'x-my-extension': before('info'), + 'x-my-config': after('paths') + }; + } +}; +``` + +#### Extension Key Validation +```typescript +import { isValidExtensionKey } from "../index.js"; + +// Validate extension keys follow OpenAPI conventions +const isValid = isValidExtensionKey('x-my-vendor-extension'); // โœ… true +const isInvalid = isValidExtensionKey('my-extension'); // โŒ false +``` + +#### Context-Specific Helpers +```typescript +import { createPositionHelpers } from "../index.js"; + +// Get type-safe helpers for a specific context +const operationHelpers = createPositionHelpers('operation'); + +// All functions are type-safe +operationHelpers.before('summary'); // โœ… Valid operation key +operationHelpers.after('responses'); // โœ… Valid operation key +operationHelpers.isValidKey('summary'); // โœ… true +operationHelpers.getAvailableKeys(); // Returns all valid operation keys +``` + +### Best Practices + +1. **Use descriptive extension names** that clearly indicate their purpose +2. **Position extensions logically** relative to related standard keys +3. **Document your extensions** in your vendor documentation +4. **Test thoroughly** with real OpenAPI files +5. **Follow OpenAPI extension conventions** (x-vendor-name format) +6. **Consider extension conflicts** when choosing names +7. **Leverage IntelliSense** for type-safe key positioning +8. **Use helper functions** for additional validation and discovery + +### Troubleshooting + +#### Common Issues + +**Extension not appearing in formatted output:** +- Check that your vendor is registered in `vendor-loader.ts` +- Verify your extension keys follow the `x-vendor-name` format +- Ensure your positioning functions return valid numbers + +**Extensions in wrong order:** +- Use `before()` and `after()` helper functions for positioning +- Check that referenced standard keys exist in the context +- Verify your positioning logic is correct + +**Extension collisions:** +- Use unique vendor prefixes to avoid conflicts +- Check the console for collision warnings +- Consider renaming conflicting extensions + +**Build errors:** +- Ensure your TypeScript syntax is correct +- Check that all imports are properly resolved +- Verify your extension structure matches the expected format + +#### Debug Tips + +1. **Enable debug logging**: Set `DEBUG=prettier-plugin-openapi:*` environment variable +2. **Check console output**: Look for collision warnings and error messages +3. **Test with simple extensions**: Start with basic positioning before complex logic +4. **Verify context names**: Ensure you're using the correct context names from the supported list + +### Example: Complete Vendor Extension + +```typescript +/** + * MyAPI Extensions + * + * MyAPI platform extensions for OpenAPI formatting. + * Website: https://myapi.com + */ + +import { defineConfig } from "../index.js"; + +export const myapi = defineConfig({ + info: { + name: 'MyAPI', + website: 'https://myapi.com', + support: 'support@myapi.com' + }, + extensions: { + 'top-level': (before, after) => { + return { + 'x-myapi-sdk': before('info'), + 'x-myapi-version': after('info'), + }; + }, + 'operation': (before, after) => { + return { + 'x-myapi-rate-limit': before('parameters'), + 'x-myapi-retries': after('parameters'), + 'x-myapi-timeout': after('responses'), + }; + }, + 'schema': (before, after) => { + return { + 'x-myapi-validation': after('type'), + 'x-myapi-example': after('example'), + }; + } + } +}); +``` ## Development -### Setup +### Modern Development Stack + +This project uses modern development tools for optimal performance and developer experience: + +- **Bun** - Fast JavaScript runtime and package manager +- **TypeScript** - Type-safe development with strict settings +- **Biome** - Fast linting and formatting (replaces ESLint + Prettier for code) +- **Prettier** - Documentation and configuration file formatting +- **GitHub Actions** - Automated CI/CD with smart releases + +### Quick Start ```bash # Install dependencies @@ -296,10 +621,28 @@ bun test --coverage # Lint code bun run lint +# Fix linting issues +bun run lint:fix + # Format code bun run format ``` +### Available Scripts + +- `bun run dev` - Start development mode with TypeScript watch +- `bun run build` - Build the project +- `bun run test` - Run all tests +- `bun run test:coverage` - Run tests with coverage report +- `bun run test:watch` - Run tests in watch mode +- `bun run lint` - Run Biome linting +- `bun run lint:fix` - Fix Biome linting issues automatically +- `bun run format` - Format code with Prettier +- `bun run format:check` - Check code formatting +- `bun run type-check` - Run TypeScript type checking +- `bun run validate` - Run all validation checks (type-check, lint, test) +- `bun run clean` - Clean build artifacts + ### Project Structure ``` @@ -325,17 +668,11 @@ test/ simple-ordering.test.ts # Basic ordering tests vendor.test.ts # Vendor extension tests setup.ts # Test utilities -.github/ - workflows/ - ci.yml # Continuous Integration - release.yml # Automated releases -examples/ - petstore.yaml # Example OpenAPI file ``` ### Test Suite -The project includes a comprehensive test suite with **99 tests** covering: +The project includes a comprehensive test suite with **142 tests** covering: - **Core Functionality**: Plugin structure, parsing, formatting - **Integration Tests**: Real OpenAPI file processing, error handling @@ -346,14 +683,14 @@ The project includes a comprehensive test suite with **99 tests** covering: - **Vendor Extensions**: Extension system functionality - **Options**: Configuration and formatting options -**Coverage**: 94.62% line coverage, 95.74% function coverage +**Coverage**: 95.69% line coverage, 97.00% function coverage ### CI/CD Pipeline The project includes automated CI/CD with GitHub Actions: - **Continuous Integration**: Tests on Node.js 18, 20, 22 and Bun -- **Automated Testing**: Linting, type checking, security audits +- **Automated Testing**: Linting with Biome, type checking, security audits - **Smart Releases**: Automatic patch version bumps on main branch updates - **NPM Publishing**: Automated publishing with version management - **Quality Gates**: All tests must pass before release @@ -364,7 +701,7 @@ The plugin respects standard Prettier options: - `tabWidth`: Number of spaces for indentation (default: 2) - `printWidth`: Maximum line length (default: 80) -- `useTabs`: Use tabs instead of spaces (default: false) +- `useTabs`: Use tabs instead of spaces (default: true) ## Advanced Features @@ -395,16 +732,16 @@ The plugin uses a unified sorting algorithm that: ### Comprehensive Testing -- **99 Test Cases**: Covering all major functionality -- **94.62% Line Coverage**: Extensive test coverage -- **95.74% Function Coverage**: Nearly complete function testing +- **142 Test Cases**: Covering all major functionality +- **95.69% Line Coverage**: Extensive test coverage +- **97.00% Function Coverage**: Nearly complete function testing - **Edge Case Testing**: Malformed files, error scenarios, performance - **Integration Testing**: Real-world OpenAPI file processing ### Code Quality - **TypeScript**: Full type safety and IntelliSense support -- **ESLint**: Strict linting rules for code quality +- **Biome**: Fast linting and formatting with TypeScript support - **Prettier**: Consistent code formatting - **Security Audits**: Automated dependency vulnerability scanning - **Performance Testing**: Large file handling and memory usage @@ -431,12 +768,12 @@ We welcome contributions! Please follow these steps: bun run lint bun run format ``` -6. **Ensure all tests pass** (99 tests, 0 failures) +6. **Ensure all tests pass** (142 tests, 0 failures) 7. **Submit a pull request** with a clear description ### Development Guidelines -- **Code Quality**: All code must pass ESLint and Prettier checks +- **Code Quality**: All code must pass Biome and Prettier checks - **Testing**: New features require comprehensive tests - **TypeScript**: Use proper types and interfaces - **Documentation**: Update README for new features diff --git a/src/extensions/README.md b/src/extensions/README.md deleted file mode 100644 index 78b9a58..0000000 --- a/src/extensions/README.md +++ /dev/null @@ -1,196 +0,0 @@ -# Vendor Extension System - -The Prettier OpenAPI Plugin includes a powerful vendor extension system that allows any number of vendors to contribute custom extensions automatically. - -## ๐Ÿš€ How It Works - -The system automatically discovers and loads all TypeScript files in the `extensions/vendor/` directory. Each vendor just needs to: - -1. **Create a TS file** in `extensions/vendor/` -2. **Export an `extensions` object** with function-based definitions -3. **That's it!** The system handles the rest - -## ๐Ÿ“ Directory Structure - -``` -extensions/ -โ”œโ”€โ”€ index.ts # Main extension system -โ”œโ”€โ”€ vendor-loader.ts # Automatic vendor discovery -โ”œโ”€โ”€ vendor/ # Vendor extensions directory -โ”‚ โ”œโ”€โ”€ speakeasy.ts # Speakeasy extensions -โ”‚ โ”œโ”€โ”€ redoc.ts # Redoc extensions -โ”‚ โ”œโ”€โ”€ postman.ts # Postman extensions -โ”‚ โ”œโ”€โ”€ example-usage.ts # Example vendor -โ”‚ โ””โ”€โ”€ your-company.ts # Your custom extensions -โ””โ”€โ”€ README.md # This documentation -``` - -## ๐ŸŽฏ Adding a New Vendor - -### Step 1: Create Your Vendor File -Create a new TypeScript file in `extensions/vendor/`: - -```typescript -// extensions/vendor/your-company.ts -export const extensions = { - 'top-level': (before, after) => { - return { - 'x-your-company-api-key': before('info'), // Before 'info' - 'x-your-company-version': after('paths'), // After 'paths' - }; - }, - 'operation': (before, after) => { - return { - 'x-your-company-rate-limit': after('parameters'), // After 'parameters' - 'x-your-company-auth': before('responses'), // Before 'responses' - }; - } -}; -``` - -### Step 2: That's It! -The system automatically: -- โœ… Discovers your file -- โœ… Loads your extensions -- โœ… Merges them with other vendors -- โœ… Applies them to OpenAPI documents - -## ๐ŸŽ›๏ธ Supported Contexts - -| Context | Description | Example Keys | -|---------|-------------|--------------| -| `'top-level'` | Root OpenAPI document | `openapi`, `info`, `paths`, `components` | -| `'info'` | API information | `title`, `version`, `description` | -| `'operation'` | Path operations | `summary`, `parameters`, `responses` | -| `'parameter'` | Request parameters | `name`, `in`, `schema` | -| `'schema'` | Data schemas | `type`, `properties`, `required` | -| `'response'` | API responses | `description`, `content`, `headers` | -| `'securityScheme'` | Security schemes | `type`, `name`, `in` | -| `'server'` | Server information | `url`, `description`, `variables` | -| `'tag'` | API tags | `name`, `description`, `externalDocs` | -| `'externalDocs'` | External documentation | `description`, `url` | -| `'webhook'` | Webhook operations | `summary`, `parameters`, `responses` | -| `'definitions'` | Swagger 2.0 definitions | `type`, `properties`, `required` | -| `'securityDefinitions'` | Swagger 2.0 security | `type`, `name`, `in` | - -## ๐ŸŽ›๏ธ Positioning Helpers - -### `before(key)` -Position before a specific key: -```typescript -before('info') // Before 'info' key -before('paths') // Before 'paths' key -before('type') // Before 'type' key -``` - -### `after(key)` -Position after a specific key: -```typescript -after('info') // After 'info' key -after('paths') // After 'paths' key -after('type') // After 'type' key -``` - -## ๐Ÿ“š TypeScript Support - -### Hover Documentation -When you hover over context names in your IDE, you'll see: -- **`'top-level'`**: Shows all top-level OpenAPI keys in order -- **`'operation'`**: Shows all operation keys in order -- **`'schema'`**: Shows all schema keys in order -- And so on... - -### Type Safety -```typescript -// Full type safety with IntelliSense -const extensions = { - 'top-level': (before, after) => { - return { - 'x-company-key': before('info'), // โœ… Type safe - 'x-company-value': after('paths'), // โœ… Type safe - }; - } -}; -``` - -## ๐Ÿงช Testing - -All tests pass: -```bash -# Run vendor tests -bun test test/vendor.test.ts - -# Run all tests -bun test -``` - -## ๐ŸŽ‰ Benefits - -- **Automatic Discovery** - No configuration needed -- **Unlimited Vendors** - Add as many as you want -- **Zero Complexity** - Just create a TS file -- **Type Safe** - Full TypeScript support with IntelliSense -- **Flexible** - Support for all OpenAPI contexts -- **Maintainable** - Simple function-based approach -- **Extensible** - Easy to add new vendors - -## ๐Ÿš€ Getting Started - -1. **Create a new TypeScript file** in `extensions/vendor/` -2. **Export an `extensions` object** with function-based definitions -3. **Use `before` and `after` helpers** for positioning -4. **That's it!** The system handles the rest - -## ๐ŸŽฏ Example Vendors - -### Speakeasy -```typescript -// extensions/vendor/speakeasy.ts -export const extensions = { - 'top-level': (before, after) => { - return { - 'x-speakeasy-sdk': before('info'), - 'x-speakeasy-auth': after('paths'), - }; - } -}; -``` - -### Redoc -```typescript -// extensions/vendor/redoc.ts -export const extensions = { - 'top-level': (before, after) => { - return { - 'x-redoc-version': before('info'), - 'x-redoc-theme': after('paths'), - }; - } -}; -``` - -### Postman -```typescript -// extensions/vendor/postman.ts -export const extensions = { - 'operation': (before, after) => { - return { - 'x-postman-test': after('responses'), - 'x-postman-pre-request': before('parameters'), - }; - } -}; -``` - -## ๐ŸŽฏ Result - -The vendor extension system is now **ultra-simple** and **unlimited**: - -- **Automatic Discovery** - No configuration needed -- **Unlimited Vendors** - Add as many as you want -- **Zero Complexity** - Just create a TS file -- **Type Safe** - Full TypeScript support -- **Flexible** - Support for all OpenAPI contexts -- **Maintainable** - Simple function-based approach - -Vendors can now add their extensions with just a few lines of code and zero complexity! ๐Ÿš€ \ No newline at end of file diff --git a/src/extensions/index.ts b/src/extensions/index.ts index f4a2541..5a1b1e0 100644 --- a/src/extensions/index.ts +++ b/src/extensions/index.ts @@ -18,117 +18,629 @@ import { TagKeys, ExternalDocsKeys, WebhookKeys, - type OAuthFlowKeys, - type ContactKeys, - type LicenseKeys, - type ComponentsKeys, - type ServerVariableKeys, + OAuthFlowKeys, + ContactKeys, + LicenseKeys, + ComponentsKeys, + ServerVariableKeys, } from '../keys.js'; -import { getVendorExtensions as loadVendorExtensions, type VendorModule } from './vendor-loader.js'; +import { getVendorExtensions, type VendorModule } from './vendor-loader.js'; +/** + * Type-safe context-specific extension functions + * + * This type provides IntelliSense for the `before` and `after` function parameters, + * ensuring only valid OpenAPI keys can be used for positioning extensions. + * + * @template T - The OpenAPI context (e.g., 'top-level', 'info', 'operation', etc.) + */ +export type ContextExtensionFunction = ( + /** + * Returns the position before the given key in the context. + * @param key - The key to position before (must be a valid key for this context). + * @returns The position index. + */ + before: (key: typeof KeyMap[T][number]) => number, + /** + * Returns the position after the given key in the context. + * @param key - The key to position after (must be a valid key for this context). + * @returns The position index. + */ + after: (key: typeof KeyMap[T][number]) => number +) => { + /** + * The extension key and its position index. + * Extension keys should start with 'x-' to follow OpenAPI vendor extension conventions. + */ + [extensionKey: string]: number; +}; + +/** + * Type-safe vendor extensions interface + * + * This interface provides IntelliSense for all OpenAPI contexts and their + * corresponding valid keys. Each context function receives type-safe `before` + * and `after` parameters that only accept valid OpenAPI keys for that context. + * + * @example + * ```typescript + * const extensions: VendorExtensions = { + * 'top-level': (before, after) => { + * // IntelliSense shows: 'swagger', 'openapi', 'info', 'paths', etc. + * return { + * 'x-my-extension': before('info'), + * 'x-my-config': after('paths') + * }; + * }, + * 'operation': (before, after) => { + * // IntelliSense shows: 'summary', 'operationId', 'parameters', 'responses', etc. + * return { + * 'x-my-retries': after('parameters'), + * 'x-my-timeout': before('responses') + * }; + * } + * }; + * ``` + */ export interface VendorExtensions { - [context: string]: ( - /** - * Returns the position before the given key in the context. - * @param {string} key - The key to position before. - * @returns {number} The position index. - */ - before: (key: string) => number, - /** - * Returns the position after the given key in the context. - * @param {string} key - The key to position after. - * @returns {number} The position index. - */ - after: (key: string) => number - ) => { - /** - * The extension key and its position index. - * @type {number} - */ - [extensionKey: string]: number; - }; + /** Root OpenAPI document extensions + * + * Available keys: + * - `swagger` + * - `openapi` + * - `jsonSchemaDialect` + * - `info` + * - `externalDocs` + * - `schemes` + * - `host` + * - `basePath` + * - `consumes` + * - `produces` + * - `servers` + * - `security` + * - `tags` + * - `paths` + * - `webhooks` + * - `components` + * - `definitions` + * - `parameters` + * - `responses` + * - `securityDefinitions` + * + * @example + * ```typescript + * 'top-level': (before, after) => { + * return { + * 'x-my-extension': before('info'), + * 'x-my-config': after('paths') + * }; + * } + * ``` + */ + 'top-level'?: ContextExtensionFunction<'top-level'>; + /** API information section extensions + * + * Available keys: + * - `title` + * - `version` + * - `summary` + * - `description` + * - `termsOfService` + * - `contact` + * - `license` + * + * @example + * ```typescript + * 'info': (before, after) => { + * return { + * 'x-my-extension': before('version'), + * 'x-my-config': after('description') + * }; + * } + * ``` + */ + 'info'?: ContextExtensionFunction<'info'>; + /** HTTP operations (GET, POST, etc.) extensions + * + * Available keys: + * - `summary` + * - `operationId` + * - `description` + * - `externalDocs` + * - `tags` + * - `deprecated` + * - `security` + * - `servers` + * - `consumes` + * - `produces` + * - `parameters` + * - `requestBody` + * - `responses` + * - `callbacks` + * - `schemes` + * + * @example + * ```typescript + * 'operation': (before, after) => { + * return { + * 'x-my-extension': before('parameters'), + * 'x-my-config': after('responses') + * }; + * } + * ``` + */ + 'operation'?: ContextExtensionFunction<'operation'>; + /** Operation parameters extensions + * + * Available keys: + * - `name` + * - `description` + * - `in` + * - `required` + * - `deprecated` + * - `allowEmptyValue` + * - `style` + * - `explode` + * - `allowReserved` + * - `schema` + * - `content` + * - `type` + * - `format` + * - `items` + * - `collectionFormat` + * - `default` + * - `minimum` + * - `exclusiveMinimum` + * - `multipleOf` + * - `maximum` + * - `exclusiveMaximum` + * - `pattern` + * - `minLength` + * - `maxLength` + * - `minItems` + * - `maxItems` + * - `uniqueItems` + * - `enum` + * - `example` + * - `examples` + * + * @example + * ```typescript + * 'parameter': (before, after) => { + * return { + * 'x-my-extension': before('name'), + * 'x-my-config': after('description') + * }; + * } + * ``` + */ + 'parameter'?: ContextExtensionFunction<'parameter'>; + /** Data schemas extensions + * + * Available keys: + * - `$ref` + * - `$id` + * - `$schema` + * - `$vocabulary` + * - `$anchor` + * - `$dynamicAnchor` + * - `$dynamicRef` + * - `$comment` + * - `$defs` + * - `$recursiveAnchor` + * - `$recursiveRef` + * - `title` + * - `description` + * - `externalDocs` + * - `deprecated` + * - `type` + * - `format` + * - `contentSchema` + * - `contentMediaType` + * - `contentEncoding` + * - `nullable` + * - `const` + * - `enum` + * - `default` + * - `readOnly` + * - `writeOnly` + * - `example` + * - `examples` + * - `minimum` + * - `exclusiveMinimum` + * - `multipleOf` + * - `maximum` + * - `exclusiveMaximum` + * - `pattern` + * - `minLength` + * - `maxLength` + * - `uniqueItems` + * - `minItems` + * - `maxItems` + * - `items` + * - `prefixItems` + * - `contains` + * - `minContains` + * - `maxContains` + * - `unevaluatedItems` + * - `minProperties` + * - `maxProperties` + * - `patternProperties` + * - `additionalProperties` + * - `properties` + * - `required` + * - `unevaluatedProperties` + * - `propertyNames` + * - `dependentRequired` + * - `dependentSchemas` + * - `discriminator` + * - `allOf` + * - `anyOf` + * - `oneOf` + * - `not` + * - `if` + * - `then` + * - `else` + * - `xml` + * + * @example + * ```typescript + * 'schema': (before, after) => { + * return { + * 'x-my-extension': before('type'), + * 'x-my-config': after('example') + * }; + * } + * ``` + */ + 'schema'?: ContextExtensionFunction<'schema'>; + /** Operation responses extensions + * + * Available keys: + * - `description` + * - `headers` + * - `schema` + * - `content` + * - `examples` + * - `links` + * + * @example + * ```typescript + * 'response': (before, after) => { + * return { + * 'x-my-extension': before('description'), + * 'x-my-config': after('content') + * }; + * } + * ``` + */ + 'response'?: ContextExtensionFunction<'response'>; + /** Security schemes extensions + * + * Available keys: + * - `name` + * - `description` + * - `type` + * - `in` + * - `scheme` + * - `bearerFormat` + * - `openIdConnectUrl` + * - `flows` + * - `flow` + * - `authorizationUrl` + * - `tokenUrl` + * - `scopes` + * + * @example + * ```typescript + * 'securityScheme': (before, after) => { + * return { + * 'x-my-extension': before('type'), + * 'x-my-config': after('description') + * }; + * } + * ``` + */ + 'securityScheme'?: ContextExtensionFunction<'securityScheme'>; + /** Server definitions extensions + * + * Available keys: + * - `name` + * - `description` + * - `url` + * - `variables` + * + * @example + * ```typescript + * 'server': (before, after) => { + * return { + * 'x-my-extension': before('url'), + * 'x-my-config': after('description') + * }; + * } + * ``` + */ + 'server'?: ContextExtensionFunction<'server'>; + /** API tags extensions + * + * Available keys: + * - `name` + * - `description` + * - `externalDocs` + * + * @example + * ```typescript + * 'tag': (before, after) => { + * return { + * 'x-my-extension': before('name'), + * 'x-my-config': after('description') + * }; + * } + * ``` + */ + 'tag'?: ContextExtensionFunction<'tag'>; + /** External documentation extensions + * + * Available keys: + * - `description` + * - `url` + * + * @example + * ```typescript + * 'externalDocs': (before, after) => { + * return { + * 'x-my-extension': before('url'), + * 'x-my-config': after('description') + * }; + * } + * ``` + */ + 'externalDocs'?: ContextExtensionFunction<'externalDocs'>; + /** Webhook definitions extensions + * + * Available keys: + * - `summary` + * - `operationId` + * - `description` + * - `deprecated` + * - `tags` + * - `security` + * - `servers` + * - `parameters` + * - `requestBody` + * - `responses` + * - `callbacks` + * + * @example + * ```typescript + * 'webhook': (before, after) => { + * return { + * 'x-my-extension': before('summary'), + * 'x-my-config': after('operationId') + * }; + * } + * ``` + */ + 'webhook'?: ContextExtensionFunction<'webhook'>; + /** Swagger 2.0 definitions extensions + * + * Available keys: (Same as schema keys) + * - `$ref` + * - `$id` + * - `$schema` + * - `$vocabulary` + * - `$anchor` + * - `$dynamicAnchor` + * - `$dynamicRef` + * - `$comment` + * - `$defs` + * - `$recursiveAnchor` + * - `$recursiveRef` + * - `title` + * - `description` + * - `externalDocs` + * - `deprecated` + * - `type` + * - `format` + * - `contentSchema` + * - `contentMediaType` + * - `contentEncoding` + * - `nullable` + * - `const` + * - `enum` + * - `default` + * - `readOnly` + * - `writeOnly` + * - `example` + * - `examples` + * - `minimum` + * - `exclusiveMinimum` + * - `multipleOf` + * - `maximum` + * - `exclusiveMaximum` + * - `pattern` + * - `minLength` + * - `maxLength` + * - `uniqueItems` + * - `minItems` + * - `maxItems` + * - `items` + * - `prefixItems` + * - `contains` + * - `minContains` + * - `maxContains` + * - `unevaluatedItems` + * - `minProperties` + * - `maxProperties` + * - `patternProperties` + * - `additionalProperties` + * - `properties` + * - `required` + * - `unevaluatedProperties` + * - `propertyNames` + * - `dependentRequired` + * - `dependentSchemas` + * - `discriminator` + * - `allOf` + * - `anyOf` + * - `oneOf` + * - `not` + * - `if` + * - `then` + * - `else` + * - `xml` + * + * @example + * ```typescript + * 'definitions': (before, after) => { + * return { + * 'x-my-extension': before('title'), + * 'x-my-config': after('description') + * }; + * } + * ``` + */ + 'definitions'?: ContextExtensionFunction<'definitions'>; + /** Swagger 2.0 security definitions extensions + * + * Available keys: (Same as security scheme keys) + * - `name` + * - `description` + * - `type` + * - `in` + * - `scheme` + * - `bearerFormat` + * - `openIdConnectUrl` + * - `flows` + * - `flow` + * - `authorizationUrl` + * - `tokenUrl` + * - `scopes` + * + * @example + * ```typescript + * 'securityDefinitions': (before, after) => { + * return { + * 'x-my-extension': before('type'), + * 'x-my-config': after('description') + * }; + * } + * ``` + */ + 'securityDefinitions'?: ContextExtensionFunction<'securityDefinitions'>; } -// Helper function similar to Vite's defineConfig +/** + * Helper function similar to Vite's defineConfig + * + * Provides type safety and IntelliSense for vendor module configuration. + * + * @param config - The vendor module configuration + * @returns The same configuration with full type safety + */ export function defineConfig(config: VendorModule): VendorModule { return config; } -// Type definitions with hover documentation -export type TopLevelKeysType = typeof RootKeys[number]; -export type InfoKeysType = typeof InfoKeys[number]; -export type OperationKeysType = typeof OperationKeys[number]; -export type ParameterKeysType = typeof ParameterKeys[number]; -export type SchemaKeysType = typeof SchemaKeys[number]; -export type ResponseKeysType = typeof ResponseKeys[number]; -export type SecuritySchemeKeysType = typeof SecuritySchemeKeys[number]; -export type ServerKeysType = typeof ServerKeys[number]; -export type TagKeysType = typeof TagKeys[number]; -export type ExternalDocsKeysType = typeof ExternalDocsKeys[number]; -export type WebhookKeysType = typeof WebhookKeys[number]; -export type OAuthFlowKeysType = typeof OAuthFlowKeys[number]; -export type ContactKeysType = typeof ContactKeys[number]; -export type LicenseKeysType = typeof LicenseKeys[number]; -export type ComponentsKeysType = typeof ComponentsKeys[number]; -export type ServerVariableKeysType = typeof ServerVariableKeys[number]; +/** + * Helper function to create type-safe context extensions + * + * This function creates a type-safe extension configuration for a specific context. + * + * @template T - The OpenAPI context + * @param context - The context name + * @param extensions - Function that returns extension positions + * @returns Type-safe context extensions + */ +export function createContextExtensions( + context: T, + extensions: (before: (key: typeof KeyMap[T][number]) => number, after: (key: typeof KeyMap[T][number]) => number) => Record +): { [K in T]: ContextExtensionFunction } { + return { [context]: extensions } as any; +} -// Context-specific key types for better IntelliSense -export interface ContextKeys { - 'top-level': TopLevelKeysType; - 'info': InfoKeysType; - 'operation': OperationKeysType; - 'parameter': ParameterKeysType; - 'schema': SchemaKeysType; - 'response': ResponseKeysType; - 'securityScheme': SecuritySchemeKeysType; - 'server': ServerKeysType; - 'tag': TagKeysType; - 'externalDocs': ExternalDocsKeysType; - 'webhook': WebhookKeysType; - 'definitions': SchemaKeysType; // Definitions use schema keys - 'securityDefinitions': SecuritySchemeKeysType; // Security definitions use security scheme keys +/** + * Type for valid OpenAPI contexts with documentation + * + * This type represents all valid OpenAPI contexts where extensions can be placed. + */ +export type OpenAPIContext = keyof typeof KeyMap; + +/** + * Helper type for extension key validation (should start with 'x-') + * + * OpenAPI vendor extensions must start with 'x-' to follow the specification. + */ +export type ExtensionKey = `x-${string}`; + +/** + * Helper function to validate extension keys + * + * Checks if a key follows the OpenAPI vendor extension naming convention. + * + * @param key - The key to validate + * @returns True if the key is a valid extension key + */ +export function isValidExtensionKey(key: string): key is ExtensionKey { + return key.startsWith('x-'); +} + +/** + * Enhanced before/after functions with better IntelliSense + * + * Creates a set of helper functions for a specific context with additional + * utilities for key validation and discovery. + * + * @template T - The OpenAPI context + * @param context - The context name + * @returns Object with type-safe positioning functions and utilities + */ +export function createPositionHelpers(context: T) { + return { + before: (key: typeof KeyMap[T][number]) => before(context, key), + after: (key: typeof KeyMap[T][number]) => after(context, key), + // Helper to get all available keys for this context + getAvailableKeys: () => getContextKeys(context), + // Helper to validate if a key exists in this context + isValidKey: (key: typeof KeyMap[T][number]): key is typeof KeyMap[T][number] => getContextKeys(context).includes(key) + }; +} + +export const KeyMap = { + 'top-level': RootKeys, + 'info': InfoKeys, + 'operation': OperationKeys, + 'parameter': ParameterKeys, + 'schema': SchemaKeys, + 'response': ResponseKeys, + 'securityScheme': SecuritySchemeKeys, + 'server': ServerKeys, + 'tag': TagKeys, + 'externalDocs': ExternalDocsKeys, + 'webhook': WebhookKeys, + 'definitions': SchemaKeys, + 'securityDefinitions': SecuritySchemeKeys, } // Helper function to get available keys for a context -export function getContextKeys(context: T): readonly string[] { - switch (context) { - case 'top-level': return RootKeys; - case 'info': return InfoKeys; - case 'operation': return OperationKeys; - case 'parameter': return ParameterKeys; - case 'schema': return SchemaKeys; - case 'response': return ResponseKeys; - case 'securityScheme': return SecuritySchemeKeys; - case 'server': return ServerKeys; - case 'tag': return TagKeys; - case 'externalDocs': return ExternalDocsKeys; - case 'webhook': return WebhookKeys; - case 'definitions': return SchemaKeys; - case 'securityDefinitions': return SecuritySchemeKeys; - default: return []; - } +export function getContextKeys(context: T): readonly typeof KeyMap[T][number][] { + return KeyMap[context]; } // Helper function to get key position in the standard ordering -export function getKeyPosition(context: T, key: string): number { - const keys = getContextKeys(context); +export function getKeyPosition(context: T, key: typeof KeyMap[T][number]): number { + const keys = getContextKeys(context); return keys.indexOf(key); } // Helper functions for easy positioning -export function before(context: T, key: string): number { +export function before(context: T, key: typeof KeyMap[T][number]): number { const position = getKeyPosition(context, key); return position === -1 ? 0 : position; } -export function after(context: T, key: string): number { +export function after(context: T, key: typeof KeyMap[T][number]): number { const position = getKeyPosition(context, key); return position === -1 ? 0 : position + 1; } - -// Dynamic vendor loading - loads all vendor files automatically -export function getVendorExtensions(customVendorModules?: VendorModule[]): Record> { - return loadVendorExtensions(customVendorModules); -} - diff --git a/src/extensions/vendor-loader.ts b/src/extensions/vendor-loader.ts index 145babf..d8e6808 100644 --- a/src/extensions/vendor-loader.ts +++ b/src/extensions/vendor-loader.ts @@ -4,7 +4,7 @@ * Loads vendor extensions using static imports for ES module compatibility. */ -import { before, after, type ContextKeys } from './index.js'; +import { before, after, KeyMap, VendorExtensions } from './index.js'; // Import vendor extensions statically import { speakeasy } from './vendor/speakeasy.js'; @@ -26,22 +26,26 @@ export interface VendorModule { website?: string; support?: string; } - extensions?: { - [context: string]: (before: (key: string) => number, after: (key: string) => number) => { - [extensionKey: string]: number; - }; - } + extensions?: VendorExtensions } +function getTypedEntries(obj: T): [keyof T, T[keyof T]][] { + return Object.entries(obj as Record) as [keyof T, T[keyof T]][]; +} + +export type Extensions = Record> +export type ExtensionSources = Record> + + /** * Load vendor extensions using static imports * This approach is ES module compatible and doesn't require dynamic loading * Handles failures on a per-vendor basis so one failure doesn't break others * Detects and alerts on extension key collisions between vendors */ -export function getVendorExtensions(customVendorModules?: VendorModule[]): Record> { - const extensions: Record> = {}; - const extensionSources: Record> = {}; // Track which vendor defined each extension +export function getVendorExtensions(customVendorModules?: VendorModule[]): Extensions { + const extensions: Extensions = {}; + const extensionSources: ExtensionSources = {}; // Track which vendor defined each extension // Use custom modules for testing, or default modules for production const modulesToLoad = customVendorModules || vendorModules; @@ -49,11 +53,12 @@ export function getVendorExtensions(customVendorModules?: VendorModule[]): Recor for (const vendorModule of modulesToLoad) { try { if (vendorModule?.extensions) { - for (const [context, contextFunction] of Object.entries(vendorModule.extensions)) { + for (const entry of getTypedEntries(vendorModule.extensions)) { + const [context, contextFunction] = entry; if (typeof contextFunction === 'function') { // Create context-specific before/after functions - const contextBefore = (key: string) => before(context as keyof ContextKeys, key); - const contextAfter = (key: string) => after(context as keyof ContextKeys, key); + const contextBefore = (key: typeof KeyMap[typeof context][number]) => before(context, key); + const contextAfter = (key: typeof KeyMap[typeof context][number]) => after(context, key); // Execute the function to get the extensions const contextExtensions = contextFunction(contextBefore, contextAfter); @@ -67,7 +72,7 @@ export function getVendorExtensions(customVendorModules?: VendorModule[]): Recor } // Check for collisions before adding extensions - for (const [extensionKey, position] of Object.entries(contextExtensions)) { + for (const [extensionKey, position] of getTypedEntries(contextExtensions)) { if (Object.hasOwn(extensions[context], extensionKey)) { const existingVendor = extensionSources[context][extensionKey]; const currentVendor = vendorModule.info.name; diff --git a/src/extensions/vendor/example-enhanced.ts b/src/extensions/vendor/example-enhanced.ts new file mode 100644 index 0000000..2cf7c91 --- /dev/null +++ b/src/extensions/vendor/example-enhanced.ts @@ -0,0 +1,145 @@ +/** + * Enhanced Vendor Extensions Example + * + * This example demonstrates the improved IntelliSense and type safety + * for vendor extensions configuration. + */ + +import { defineConfig, createPositionHelpers } from "../index.js"; + +// Example vendor with enhanced type safety +export const exampleEnhanced = defineConfig({ + info: { + name: 'Example Enhanced', + website: 'https://example.com', + support: 'support@example.com' + }, + extensions: { + // Type-safe context with IntelliSense for available keys + 'top-level': (before, after) => { + // IntelliSense will show: 'swagger', 'openapi', 'jsonSchemaDialect', 'info', 'externalDocs', etc. + return { + 'x-example-sdk': before('info'), // โœ… Type-safe: 'info' is a valid top-level key + 'x-example-config': after('paths'), // โœ… Type-safe: 'paths' is a valid top-level key + // 'x-example-invalid': before('invalidKey'), // โŒ TypeScript error: 'invalidKey' is not a valid top-level key + }; + }, + + 'info': (before, after) => { + // IntelliSense will show: 'title', 'version', 'summary', 'description', 'termsOfService', etc. + return { + 'x-example-info': after('version'), // โœ… Type-safe: 'version' is a valid info key + 'x-example-metadata': before('description'), // โœ… Type-safe: 'description' is a valid info key + }; + }, + + 'operation': (before, after) => { + // IntelliSense will show: 'summary', 'operationId', 'description', 'externalDocs', 'tags', etc. + return { + 'x-example-retries': after('parameters'), // โœ… Type-safe: 'parameters' is a valid operation key + 'x-example-timeout': before('responses'), // โœ… Type-safe: 'responses' is a valid operation key + 'x-example-cache': after('servers'), // โœ… Type-safe: 'servers' is a valid operation key + }; + }, + + 'schema': (before, after) => { + // IntelliSense will show: '$ref', 'title', 'description', 'type', 'format', 'enum', etc. + return { + 'x-example-validation': after('type'), // โœ… Type-safe: 'type' is a valid schema key + 'x-example-example': after('example'), // โœ… Type-safe: 'example' is a valid schema key + }; + }, + + 'parameter': (before, after) => { + // IntelliSense will show: 'name', 'description', 'in', 'required', 'deprecated', etc. + return { + 'x-example-param': after('schema'), // โœ… Type-safe: 'schema' is a valid parameter key + }; + }, + + 'response': (before, after) => { + // IntelliSense will show: 'description', 'headers', 'content', 'links' + return { + 'x-example-response': after('description'), // โœ… Type-safe: 'description' is a valid response key + }; + }, + + 'securityScheme': (before, after) => { + // IntelliSense will show: 'type', 'description', 'name', 'in', 'scheme', etc. + return { + 'x-example-auth': after('type'), // โœ… Type-safe: 'type' is a valid security scheme key + }; + }, + + 'server': (before, after) => { + // IntelliSense will show: 'url', 'description', 'variables' + return { + 'x-example-server': after('url'), // โœ… Type-safe: 'url' is a valid server key + }; + }, + + 'tag': (before, after) => { + // IntelliSense will show: 'name', 'description', 'externalDocs' + return { + 'x-example-tag': after('name'), // โœ… Type-safe: 'name' is a valid tag key + }; + }, + + 'externalDocs': (before, after) => { + // IntelliSense will show: 'description', 'url' + return { + 'x-example-docs': after('url'), // โœ… Type-safe: 'url' is a valid external docs key + }; + }, + + 'webhook': (before, after) => { + // IntelliSense will show: 'summary', 'operationId', 'description', 'deprecated', etc. + return { + 'x-example-webhook': after('operationId'), // โœ… Type-safe: 'operationId' is a valid webhook key + }; + }, + + 'definitions': (before, after) => { + // IntelliSense will show schema keys: '$ref', 'title', 'description', 'type', etc. + return { + 'x-example-definition': after('type'), // โœ… Type-safe: 'type' is a valid schema key + }; + }, + + 'securityDefinitions': (before, after) => { + // IntelliSense will show security scheme keys: 'type', 'description', 'name', etc. + return { + 'x-example-security': after('type'), // โœ… Type-safe: 'type' is a valid security scheme key + }; + } + } +}); + +// Alternative approach using the enhanced helper functions +export const exampleWithHelpers = defineConfig({ + info: { + name: 'Example With Helpers', + website: 'https://example.com', + support: 'support@example.com' + }, + extensions: { + 'top-level': (before, after) => { + // You can also use the enhanced helpers for additional functionality + const helpers = createPositionHelpers('top-level'); + + // Get all available keys for this context + const availableKeys = helpers.getAvailableKeys(); + console.log('Available top-level keys:', availableKeys); + + // Validate if a key exists + if (helpers.isValidKey('info')) { + console.log('info is a valid top-level key'); + } + + return { + 'x-example-enhanced': before('info'), + 'x-example-config': after('paths'), + }; + } + } +}); diff --git a/src/keys.ts b/src/keys.ts index d1037cb..fddd481 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -5,8 +5,6 @@ * Supports Swagger 2.0, OpenAPI 3.0, 3.1, and 3.2 */ -// Top-level keys in preferred order -// Supports Swagger 2.0, OpenAPI 3.0, 3.1, and 3.2 export const RootKeys = [ // Version identifiers 'swagger', // Swagger 2.0 @@ -502,10 +500,10 @@ export const PathItemKeys = [ 'get', 'put', 'post', + 'patch', 'delete', 'options', 'head', - 'patch', 'trace', ] as const; diff --git a/test/vendor-collision.test.ts b/test/vendor-collision.test.ts index f736d20..70ed822 100644 --- a/test/vendor-collision.test.ts +++ b/test/vendor-collision.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, beforeEach, afterEach } from 'bun:test'; -import { getVendorExtensions, defineConfig } from '../src/extensions/index'; +import { defineConfig } from '../src/extensions/index'; +import { getVendorExtensions } from '../src/extensions/vendor-loader'; // Mock console.warn to capture collision warnings let consoleWarnSpy: any; diff --git a/test/vendor.test.ts b/test/vendor.test.ts index 848e4d1..767bc70 100644 --- a/test/vendor.test.ts +++ b/test/vendor.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'bun:test'; -import { getVendorExtensions } from '../src/extensions'; +import { getVendorExtensions } from '../src/extensions/vendor-loader'; describe('Vendor Extension System', () => { it('should load vendor extensions from TS files', () => {