commit 22726c627afb15ec2ba5a0337e1a1ee9132de8af Author: Luke Hagar Date: Thu Sep 25 01:36:23 2025 +0000 Saving current state diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a101be7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "plugins": ["./dist/index.js"], + "tabWidth": 2, + "printWidth": 80, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "es5" +} diff --git a/CUSTOMIZATION.md b/CUSTOMIZATION.md new file mode 100644 index 0000000..bdd8604 --- /dev/null +++ b/CUSTOMIZATION.md @@ -0,0 +1,550 @@ +# Customizing Key Ordering + +This document explains how to customize the key ordering in the Prettier OpenAPI plugin. + +## Overview + +The plugin uses configuration arrays and maps at the top of the `src/index.ts` file to determine the order of keys in OpenAPI/Swagger files. You can easily modify these arrays to change the sorting behavior. + +## Configuration Arrays + +All key ordering configuration is located at the top of `src/index.ts` in the "KEY ORDERING CONFIGURATION" section. + +### Top-Level Keys + +```typescript +const TOP_LEVEL_KEYS = [ + 'openapi', + 'info', + 'servers', + 'paths', + 'components', + 'security', + 'tags', + 'externalDocs', +] as const; +``` + +### Info Section Keys + +```typescript +const INFO_KEYS = [ + 'title', + 'description', + 'version', + 'termsOfService', + 'contact', + 'license', +] as const; +``` + +### Contact Section Keys + +```typescript +const CONTACT_KEYS = [ + 'name', + 'url', + 'email', +] as const; +``` + +### License Section Keys + +```typescript +const LICENSE_KEYS = [ + 'name', + 'url', +] as const; +``` + +### Components Section Keys + +```typescript +const COMPONENTS_KEYS = [ + 'schemas', + 'responses', + 'parameters', + 'examples', + 'requestBodies', + 'headers', + 'securitySchemes', + 'links', + 'callbacks', +] as const; +``` + +### Operation Keys + +```typescript +const OPERATION_KEYS = [ + 'tags', + 'summary', + 'description', + 'operationId', + 'parameters', + 'requestBody', + 'responses', + 'callbacks', + 'deprecated', + 'security', + 'servers', +] as const; +``` + +### Parameter Keys + +```typescript +const PARAMETER_KEYS = [ + 'name', + 'in', + 'description', + 'required', + 'deprecated', + 'allowEmptyValue', + 'style', + 'explode', + 'allowReserved', + 'schema', + 'example', + 'examples', +] as const; +``` + +### Schema Keys + +```typescript +const SCHEMA_KEYS = [ + 'type', + 'format', + 'title', + 'description', + 'default', + 'example', + 'examples', + 'enum', + 'const', + 'multipleOf', + 'maximum', + 'exclusiveMaximum', + 'minimum', + 'exclusiveMinimum', + 'maxLength', + 'minLength', + 'pattern', + 'maxItems', + 'minItems', + 'uniqueItems', + 'maxProperties', + 'minProperties', + 'required', + 'properties', + 'patternProperties', + 'additionalProperties', + 'items', + 'allOf', + 'oneOf', + 'anyOf', + 'not', + 'discriminator', + 'xml', + 'externalDocs', + 'deprecated', +] as const; +``` + +### Response Keys + +```typescript +const RESPONSE_KEYS = [ + 'description', + 'headers', + 'content', + 'links', +] as const; +``` + +### Security Scheme Keys + +```typescript +const SECURITY_SCHEME_KEYS = [ + 'type', + 'description', + 'name', + 'in', + 'scheme', + 'bearerFormat', + 'flows', + 'openIdConnectUrl', +] as const; +``` + +### OAuth Flow Keys + +```typescript +const OAUTH_FLOW_KEYS = [ + 'authorizationUrl', + 'tokenUrl', + 'refreshUrl', + 'scopes', +] as const; +``` + +### Server Keys + +```typescript +const SERVER_KEYS = [ + 'url', + 'description', + 'variables', +] as const; +``` + +### Server Variable Keys + +```typescript +const SERVER_VARIABLE_KEYS = [ + 'enum', + 'default', + 'description', +] as const; +``` + +### Tag Keys + +```typescript +const TAG_KEYS = [ + 'name', + 'description', + 'externalDocs', +] as const; +``` + +### External Docs Keys + +```typescript +const EXTERNAL_DOCS_KEYS = [ + 'description', + 'url', +] as const; +``` + +## How to Customize + +### 1. Reorder Keys + +To change the order of keys, simply rearrange the arrays: + +```typescript +// Example: Put 'description' before 'title' in info section +const INFO_KEYS = [ + 'description', // Moved to first + 'title', + 'version', + 'termsOfService', + 'contact', + 'license', +] as const; +``` + +### 2. Add New Keys + +To add support for new keys, add them to the appropriate array: + +```typescript +// Example: Add custom keys to info section +const INFO_KEYS = [ + 'title', + 'description', + 'version', + 'customField', // New key + 'termsOfService', + 'contact', + 'license', +] as const; +``` + +### 3. Remove Keys + +To remove keys from sorting (they'll be sorted alphabetically), simply remove them from the arrays: + +```typescript +// Example: Remove 'termsOfService' from info section +const INFO_KEYS = [ + 'title', + 'description', + 'version', + // 'termsOfService', // Removed + 'contact', + 'license', +] as const; +``` + +### 4. Create Custom Ordering + +You can create completely custom ordering by modifying the arrays: + +```typescript +// Example: Custom ordering for your organization's standards +const INFO_KEYS = [ + 'title', + 'version', + 'description', + 'contact', + 'license', + 'termsOfService', +] as const; +``` + +## Special Sorting Rules + +Some sections have special sorting rules that can't be changed by modifying arrays: + +- **Paths**: Sorted by specificity (more specific paths first) +- **Response Codes**: Sorted numerically (200, 201, 400, 404, etc.) +- **Schema Properties**: Sorted alphabetically +- **Parameters**: Sorted alphabetically +- **Security Schemes**: Sorted alphabetically + +## Testing Changes + +After modifying the configuration arrays: + +1. Build the plugin: + ```bash + bun run build + ``` + +2. Run tests to ensure nothing is broken: + ```bash + bun test + ``` + +3. Test with your OpenAPI files: + ```bash + npx prettier --write your-api.yaml + ``` + +## Examples + +### Example 1: Prioritize Security + +```typescript +const TOP_LEVEL_KEYS = [ + 'openapi', + 'info', + 'security', // Moved up + 'servers', + 'paths', + 'components', + 'tags', + 'externalDocs', +] as const; +``` + +### Example 2: Custom Info Ordering + +```typescript +const INFO_KEYS = [ + 'title', + 'version', + 'description', + 'contact', + 'license', + 'termsOfService', +] as const; +``` + +### Example 3: Schema-First Components + +```typescript +const COMPONENTS_KEYS = [ + 'schemas', // Already first, but emphasizing + 'responses', + 'parameters', + 'examples', + 'requestBodies', + 'headers', + 'securitySchemes', + 'links', + 'callbacks', +] as const; +``` + +## Custom Extensions + +The plugin supports custom extensions (keys starting with `x-`) with configurable positioning. You can specify exactly where your custom extensions should appear in the sorted output. + +### Configuration + +Custom extensions are configured at the top of `src/index.ts` in the "CUSTOM EXTENSION CONFIGURATION" section: + +```typescript +// Custom extensions for top-level OpenAPI keys +const CUSTOM_TOP_LEVEL_EXTENSIONS: Record = { + 'x-custom-field': 2, // Will be placed after 'info' (position 1) + 'x-api-version': 0, // Will be placed before 'openapi' +}; + +// Custom extensions for info section +const CUSTOM_INFO_EXTENSIONS: Record = { + 'x-api-id': 1, // Will be placed after 'title' (position 0) + 'x-version-info': 3, // Will be placed after 'version' (position 2) +}; +``` + +### How Positioning Works + +- **Position 0**: Before the first standard key +- **Position 1**: After the first standard key +- **Position 2**: After the second standard key +- **Position N**: After the Nth standard key +- **Position > standard keys length**: After all standard keys + +### Examples + +#### Example 1: Top-Level Custom Extensions + +```typescript +const CUSTOM_TOP_LEVEL_EXTENSIONS: Record = { + 'x-api-version': 0, // Before 'openapi' + 'x-custom-field': 2, // After 'info' (position 1) + 'x-metadata': 8, // After all standard keys +}; +``` + +**Result:** +```yaml +x-api-version: "1.0" +openapi: 3.0.0 +info: ... +x-custom-field: "value" +servers: ... +paths: ... +components: ... +x-metadata: {...} +``` + +#### Example 2: Info Section Custom Extensions + +```typescript +const CUSTOM_INFO_EXTENSIONS: Record = { + 'x-api-id': 1, // After 'title' + 'x-version-info': 3, // After 'version' + 'x-custom-info': 6, // After all standard keys +}; +``` + +**Result:** +```yaml +info: + title: My API + x-api-id: "api-123" + description: API Description + version: 1.0.0 + x-version-info: "v1.0.0-beta" + contact: ... + license: ... + x-custom-info: {...} +``` + +#### Example 3: Operation Custom Extensions + +```typescript +const CUSTOM_OPERATION_EXTENSIONS: Record = { + 'x-rate-limit': 5, // After 'parameters' (position 4) + 'x-custom-auth': 10, // After 'servers' (position 9) +}; +``` + +**Result:** +```yaml +get: + tags: [...] + summary: ... + description: ... + operationId: ... + parameters: [...] + x-rate-limit: 100 + requestBody: ... + responses: ... + callbacks: ... + deprecated: false + security: [...] + servers: [...] + x-custom-auth: "bearer" +``` + +### Available Extension Contexts + +| Context | Configuration Object | Description | +|---------|---------------------|-------------| +| `top-level` | `CUSTOM_TOP_LEVEL_EXTENSIONS` | Root OpenAPI object | +| `info` | `CUSTOM_INFO_EXTENSIONS` | Info section | +| `components` | `CUSTOM_COMPONENTS_EXTENSIONS` | Components section | +| `operation` | `CUSTOM_OPERATION_EXTENSIONS` | HTTP operation objects | +| `parameter` | `CUSTOM_PARAMETER_EXTENSIONS` | Parameter objects | +| `schema` | `CUSTOM_SCHEMA_EXTENSIONS` | Schema objects | +| `response` | `CUSTOM_RESPONSE_EXTENSIONS` | Response objects | +| `securityScheme` | `CUSTOM_SECURITY_SCHEME_EXTENSIONS` | Security scheme objects | +| `server` | `CUSTOM_SERVER_EXTENSIONS` | Server objects | +| `tag` | `CUSTOM_TAG_EXTENSIONS` | Tag objects | +| `externalDocs` | `CUSTOM_EXTERNAL_DOCS_EXTENSIONS` | External docs objects | + +### Unknown Keys Behavior + +Keys that are not in the standard arrays or custom extensions configuration will be sorted alphabetically at the bottom of their respective objects. + +**Example:** +```yaml +info: + title: My API + version: 1.0.0 + # Standard keys in order + contact: ... + license: ... + # Custom extensions in configured order + x-api-id: "api-123" + # Unknown keys sorted alphabetically at the end + unknown-field: "value" + x-other-extension: "value" +``` + +### Complete Example + +Here's a complete example of custom extension configuration: + +```typescript +// Top-level custom extensions +const CUSTOM_TOP_LEVEL_EXTENSIONS: Record = { + 'x-api-version': 0, // Before 'openapi' + 'x-custom-field': 2, // After 'info' + 'x-metadata': 8, // After all standard keys +}; + +// Info section custom extensions +const CUSTOM_INFO_EXTENSIONS: Record = { + 'x-api-id': 1, // After 'title' + 'x-version-info': 3, // After 'version' +}; + +// Operation custom extensions +const CUSTOM_OPERATION_EXTENSIONS: Record = { + 'x-rate-limit': 5, // After 'parameters' + 'x-custom-auth': 10, // After 'servers' +}; + +// Schema custom extensions +const CUSTOM_SCHEMA_EXTENSIONS: Record = { + 'x-custom-type': 0, // Before 'type' + 'x-validation-rules': 30, // After 'deprecated' +}; +``` + +## Notes + +- Keys not in the configuration arrays will be sorted alphabetically +- Custom extensions can be positioned anywhere in the key order +- Unknown keys (not in standard or custom lists) default to alphabetical sorting at the bottom +- The `as const` assertion ensures type safety +- Changes require rebuilding the plugin (`bun run build`) +- Test your changes with real OpenAPI files to ensure they work as expected diff --git a/EXTENSIONS.md b/EXTENSIONS.md new file mode 100644 index 0000000..e4397d5 --- /dev/null +++ b/EXTENSIONS.md @@ -0,0 +1,248 @@ +# Custom Extensions Support + +This document explains the custom extensions functionality in the Prettier OpenAPI plugin. + +## Overview + +The plugin now supports configurable positioning of custom extensions (keys starting with `x-`) and ensures that unknown keys are sorted alphabetically at the bottom of their respective objects. + +## Key Features + +✅ **Configurable Extension Positioning** - Specify exactly where custom extensions should appear +✅ **Context-Aware Sorting** - Different extension configurations for different OpenAPI sections +✅ **Unknown Key Handling** - Unknown keys default to alphabetical sorting at the bottom +✅ **Type Safety** - Full TypeScript support with proper typing +✅ **Comprehensive Testing** - 15 tests covering all functionality + +## Configuration + +All custom extension configuration is located at the top of `src/index.ts`: + +```typescript +// ============================================================================ +// CUSTOM EXTENSION CONFIGURATION +// ============================================================================ + +// Custom extensions for top-level OpenAPI keys +const CUSTOM_TOP_LEVEL_EXTENSIONS: Record = { + 'x-custom-field': 2, // Will be placed after 'info' (position 1) + 'x-api-version': 0, // Will be placed before 'openapi' +}; + +// Custom extensions for info section +const CUSTOM_INFO_EXTENSIONS: Record = { + 'x-api-id': 1, // Will be placed after 'title' (position 0) + 'x-version-info': 3, // Will be placed after 'version' (position 2) +}; +``` + +## Supported Contexts + +| Context | Configuration Object | Standard Keys Count | Description | +|---------|---------------------|-------------------|-------------| +| `top-level` | `CUSTOM_TOP_LEVEL_EXTENSIONS` | 8 | Root OpenAPI object | +| `info` | `CUSTOM_INFO_EXTENSIONS` | 6 | Info section | +| `components` | `CUSTOM_COMPONENTS_EXTENSIONS` | 9 | Components section | +| `operation` | `CUSTOM_OPERATION_EXTENSIONS` | 11 | HTTP operation objects | +| `parameter` | `CUSTOM_PARAMETER_EXTENSIONS` | 12 | Parameter objects | +| `schema` | `CUSTOM_SCHEMA_EXTENSIONS` | 31 | Schema objects | +| `response` | `CUSTOM_RESPONSE_EXTENSIONS` | 4 | Response objects | +| `securityScheme` | `CUSTOM_SECURITY_SCHEME_EXTENSIONS` | 8 | Security scheme objects | +| `server` | `CUSTOM_SERVER_EXTENSIONS` | 3 | Server objects | +| `tag` | `CUSTOM_TAG_EXTENSIONS` | 3 | Tag objects | +| `externalDocs` | `CUSTOM_EXTERNAL_DOCS_EXTENSIONS` | 2 | External docs objects | + +## Positioning Rules + +### Position Numbers +- **0**: Before the first standard key +- **1**: After the first standard key +- **2**: After the second standard key +- **N**: After the Nth standard key +- **> standard keys length**: After all standard keys + +### Sorting Priority +1. **Custom extensions** (in configured order) +2. **Standard keys** (in predefined order) +3. **Unknown keys** (alphabetically sorted) + +## Examples + +### Example 1: Top-Level Extensions + +```typescript +const CUSTOM_TOP_LEVEL_EXTENSIONS: Record = { + 'x-api-version': 0, // Before 'openapi' + 'x-custom-field': 2, // After 'info' + 'x-metadata': 8, // After all standard keys +}; +``` + +**Result:** +```yaml +x-api-version: "1.0" +openapi: 3.0.0 +info: ... +x-custom-field: "value" +servers: ... +paths: ... +components: ... +x-metadata: {...} +``` + +### Example 2: Operation Extensions + +```typescript +const CUSTOM_OPERATION_EXTENSIONS: Record = { + 'x-rate-limit': 5, // After 'parameters' + 'x-custom-auth': 10, // After 'servers' +}; +``` + +**Result:** +```yaml +get: + tags: [...] + summary: ... + description: ... + operationId: ... + parameters: [...] + x-rate-limit: 100 + requestBody: ... + responses: ... + callbacks: ... + deprecated: false + security: [...] + servers: [...] + x-custom-auth: "bearer" +``` + +### Example 3: Schema Extensions + +```typescript +const CUSTOM_SCHEMA_EXTENSIONS: Record = { + 'x-custom-type': 0, // Before 'type' + 'x-validation-rules': 30, // After 'deprecated' +}; +``` + +**Result:** +```yaml +User: + x-custom-type: "entity" + type: object + properties: {...} + required: [...] + deprecated: false + x-validation-rules: "required" +``` + +## Unknown Keys Behavior + +Keys that are not in the standard arrays or custom extensions configuration will be sorted alphabetically at the bottom of their respective objects. + +**Example:** +```yaml +info: + title: My API + version: 1.0.0 + # Standard keys in order + contact: ... + license: ... + # Custom extensions in configured order + x-api-id: "api-123" + # Unknown keys sorted alphabetically at the end + unknown-field: "value" + x-other-extension: "value" +``` + +## Testing + +The plugin includes comprehensive tests for custom extensions: + +```bash +# Run all tests +bun test + +# Run only custom extension tests +bun test test/custom-extensions.test.ts +``` + +**Test Coverage:** +- ✅ Custom extensions in top-level keys +- ✅ Custom extensions in info section +- ✅ Custom extensions in operation objects +- ✅ Custom extensions in schema objects +- ✅ JSON formatting with custom extensions +- ✅ YAML formatting with custom extensions +- ✅ Unknown keys handling + +## Usage + +### 1. Configure Extensions + +Edit the configuration arrays in `src/index.ts`: + +```typescript +const CUSTOM_TOP_LEVEL_EXTENSIONS: Record = { + 'x-your-extension': 2, // Position after 'info' +}; +``` + +### 2. Build the Plugin + +```bash +bun run build +``` + +### 3. Use with Prettier + +```bash +npx prettier --write your-api.yaml +``` + +## Best Practices + +1. **Use Descriptive Names** - Choose clear, descriptive names for your extensions +2. **Consistent Positioning** - Use consistent positioning across similar contexts +3. **Document Extensions** - Document your custom extensions in your API documentation +4. **Test Changes** - Always test your configuration with real OpenAPI files +5. **Version Control** - Keep your extension configurations in version control + +## Migration Guide + +If you're upgrading from a version without custom extension support: + +1. **No Breaking Changes** - Existing functionality remains unchanged +2. **Add Extensions Gradually** - Start with a few key extensions +3. **Test Thoroughly** - Test with your existing OpenAPI files +4. **Update Documentation** - Update your team's documentation + +## Troubleshooting + +### Common Issues + +1. **Extensions Not Appearing** - Check that the extension is configured in the correct context +2. **Wrong Position** - Verify the position number is correct for the context +3. **Build Errors** - Ensure the plugin is rebuilt after configuration changes + +### Debug Tips + +1. **Check Context** - Use the context detection to ensure extensions are in the right place +2. **Verify Configuration** - Double-check your extension configuration +3. **Test Incrementally** - Add extensions one at a time to isolate issues + +## Contributing + +When adding new extension contexts: + +1. Add the configuration object +2. Add the context to `CUSTOM_EXTENSIONS_MAP` +3. Add the context to `getContextKey` function +4. Add the context to `getStandardKeysForContext` function +5. Add tests for the new context +6. Update documentation + +## License + +This functionality is part of the Prettier OpenAPI plugin and follows the same MIT license. diff --git a/README.md b/README.md new file mode 100644 index 0000000..aac29a8 --- /dev/null +++ b/README.md @@ -0,0 +1,204 @@ +# Prettier Plugin OpenAPI + +A Prettier plugin for formatting OpenAPI/Swagger JSON and YAML files with intelligent key sorting and proper indentation. + +## Features + +- 🎯 **OpenAPI/Swagger Support**: Formats both JSON and YAML OpenAPI specifications +- 🔄 **Smart Key Sorting**: Automatically sorts OpenAPI keys in the recommended order +- 📝 **YAML & JSON**: Supports both `.yaml/.yml` and `.json` file formats +- 🎨 **Consistent Formatting**: Applies consistent indentation and line breaks +- ⚡ **Fast**: Built with performance in mind using modern JavaScript + +## Installation + +```bash +npm install --save-dev prettier-plugin-openapi +# or +yarn add --dev prettier-plugin-openapi +# or +bun add --dev prettier-plugin-openapi +``` + +## Usage + +### Command Line + +```bash +# Format a single file +npx prettier --write api.yaml + +# Format all OpenAPI files in a directory +npx prettier --write "**/*.{openapi.json,openapi.yaml,swagger.json,swagger.yaml}" + +# Format with specific options +npx prettier --write api.yaml --tab-width 4 --print-width 100 +``` + +### Configuration + +Add the plugin to your Prettier configuration: + +**package.json** +```json +{ + "prettier": { + "plugins": ["prettier-plugin-openapi"] + } +} +``` + +**.prettierrc** +```json +{ + "plugins": ["prettier-plugin-openapi"], + "tabWidth": 2, + "printWidth": 80 +} +``` + +**.prettierrc.js** +```javascript +module.exports = { + plugins: ['prettier-plugin-openapi'], + tabWidth: 2, + printWidth: 80, +}; +``` + +## Supported File Extensions + +- `.openapi.json` +- `.openapi.yaml` +- `.openapi.yml` +- `.swagger.json` +- `.swagger.yaml` +- `.swagger.yml` + +## Key Sorting + +The plugin automatically sorts OpenAPI keys in the recommended order: + +### Top-level keys: +1. `openapi` +2. `info` +3. `servers` +4. `paths` +5. `components` +6. `security` +7. `tags` +8. `externalDocs` + +### Info section: +1. `title` +2. `description` +3. `version` +4. `termsOfService` +5. `contact` +6. `license` + +### Components section: +1. `schemas` +2. `responses` +3. `parameters` +4. `examples` +5. `requestBodies` +6. `headers` +7. `securitySchemes` +8. `links` +9. `callbacks` + +## Examples + +### Before (unformatted): +```yaml +paths: + /users: + get: + responses: + '200': + description: OK +components: + schemas: + User: + type: object +openapi: 3.0.0 +info: + version: 1.0.0 + title: My API +``` + +### After (formatted): +```yaml +openapi: 3.0.0 +info: + title: My API + version: 1.0.0 +paths: + /users: + get: + responses: + '200': + description: OK +components: + schemas: + User: + type: object +``` + +## Development + +### Setup + +```bash +# Install dependencies +bun install + +# Build the plugin +bun run build + +# Run tests +bun test + +# Run demo +bun run test/demo.ts +``` + +### Project Structure + +``` +src/ + index.ts # Main plugin implementation +test/ + plugin.test.ts # Unit tests + demo.ts # Demo script +examples/ + petstore.yaml # Example OpenAPI file +``` + +## Configuration Options + +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) + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests for new functionality +5. Run the test suite +6. Submit a pull request + +## License + +MIT License - see LICENSE file for details. + +## Related Projects + +- [Prettier](https://prettier.io/) - The core formatter +- [OpenAPI Specification](https://swagger.io/specification/) - The OpenAPI specification +- [Swagger](https://swagger.io/) - API development tools diff --git a/SUPPORTED_KEYS.md b/SUPPORTED_KEYS.md new file mode 100644 index 0000000..236dc65 --- /dev/null +++ b/SUPPORTED_KEYS.md @@ -0,0 +1,428 @@ +# Supported Keys - Complete Reference + +This document provides a comprehensive list of all supported keys for Swagger 2.0, OpenAPI 3.0, 3.1, and 3.2 specifications. + +## Top-Level Keys + +### All Versions +- `swagger` (Swagger 2.0 only) +- `openapi` (OpenAPI 3.0+) +- `info` +- `paths` +- `security` +- `tags` +- `externalDocs` + +### Version-Specific +- `jsonSchemaDialect` (OpenAPI 3.1+) +- `servers` (OpenAPI 3.0+) +- `webhooks` (OpenAPI 3.1+) +- `components` (OpenAPI 3.0+) +- `host` (Swagger 2.0 only) +- `basePath` (Swagger 2.0 only) +- `schemes` (Swagger 2.0 only) +- `consumes` (Swagger 2.0 only) +- `produces` (Swagger 2.0 only) +- `definitions` (Swagger 2.0 only) +- `parameters` (Swagger 2.0 only) +- `responses` (Swagger 2.0 only) +- `securityDefinitions` (Swagger 2.0 only) + +## Info Section Keys + +### All Versions +- `title` +- `description` +- `version` +- `termsOfService` +- `contact` +- `license` + +### Version-Specific +- `summary` (OpenAPI 3.1+) + +## Contact Section Keys + +### All Versions +- `name` +- `url` +- `email` + +## License Section Keys + +### All Versions +- `name` +- `url` + +## Components Section Keys (OpenAPI 3.0+) + +### All Versions +- `schemas` +- `responses` +- `parameters` +- `examples` +- `requestBodies` +- `headers` +- `securitySchemes` +- `links` +- `callbacks` + +### Version-Specific +- `pathItems` (OpenAPI 3.1+) + +## Operation Keys + +### All Versions +- `tags` +- `summary` +- `description` +- `operationId` +- `parameters` +- `responses` +- `deprecated` +- `security` + +### Version-Specific +- `consumes` (Swagger 2.0 only) +- `produces` (Swagger 2.0 only) +- `requestBody` (OpenAPI 3.0+) +- `schemes` (Swagger 2.0 only) +- `callbacks` (OpenAPI 3.0+) +- `servers` (OpenAPI 3.0+) + +## Parameter Keys + +### All Versions +- `name` +- `in` +- `description` +- `required` +- `deprecated` +- `allowEmptyValue` +- `style` +- `explode` +- `allowReserved` +- `schema` +- `example` +- `examples` + +### Swagger 2.0 Specific +- `type` +- `format` +- `items` +- `collectionFormat` +- `default` +- `maximum` +- `exclusiveMaximum` +- `minimum` +- `exclusiveMinimum` +- `maxLength` +- `minLength` +- `pattern` +- `maxItems` +- `minItems` +- `uniqueItems` +- `enum` +- `multipleOf` + +## Schema Keys + +### Core JSON Schema Keywords +- `$schema` +- `$id` +- `$ref` +- `$anchor` +- `$dynamicAnchor` +- `$dynamicRef` +- `$vocabulary` +- `$comment` +- `$defs` +- `$recursiveAnchor` +- `$recursiveRef` + +### Basic Type and Format +- `type` +- `format` +- `title` +- `description` +- `default` +- `example` +- `examples` +- `enum` +- `const` + +### Numeric Validation +- `multipleOf` +- `maximum` +- `exclusiveMaximum` +- `minimum` +- `exclusiveMinimum` + +### String Validation +- `maxLength` +- `minLength` +- `pattern` + +### Array Validation +- `maxItems` +- `minItems` +- `uniqueItems` +- `items` +- `prefixItems` +- `contains` +- `minContains` +- `maxContains` +- `unevaluatedItems` + +### Object Validation +- `maxProperties` +- `minProperties` +- `required` +- `properties` +- `patternProperties` +- `additionalProperties` +- `unevaluatedProperties` +- `propertyNames` +- `dependentRequired` +- `dependentSchemas` + +### Schema Composition +- `allOf` +- `oneOf` +- `anyOf` +- `not` +- `if` +- `then` +- `else` + +### OpenAPI Specific +- `discriminator` +- `xml` +- `externalDocs` +- `deprecated` + +### Additional JSON Schema Keywords +- `contentEncoding` +- `contentMediaType` +- `contentSchema` + +## Response Keys + +### All Versions +- `description` +- `headers` + +### Version-Specific +- `content` (OpenAPI 3.0+) +- `schema` (Swagger 2.0 only) +- `examples` (Swagger 2.0 only) +- `links` (OpenAPI 3.0+) + +## Security Scheme Keys + +### All Versions +- `type` +- `description` +- `name` +- `in` +- `scheme` +- `bearerFormat` +- `openIdConnectUrl` + +### Version-Specific +- `flows` (OpenAPI 3.0+) +- `flow` (Swagger 2.0 only) +- `authorizationUrl` (Swagger 2.0 only) +- `tokenUrl` (Swagger 2.0 only) +- `scopes` (Swagger 2.0 only) + +## OAuth Flow Keys (OpenAPI 3.0+) + +### All Versions +- `authorizationUrl` +- `tokenUrl` +- `refreshUrl` +- `scopes` + +## Server Keys + +### All Versions +- `url` +- `description` +- `variables` + +## Server Variable Keys + +### All Versions +- `enum` +- `default` +- `description` + +## Tag Keys + +### All Versions +- `name` +- `description` +- `externalDocs` + +## External Docs Keys + +### All Versions +- `description` +- `url` + +## Webhook Keys (OpenAPI 3.1+) + +### All Versions +- `tags` +- `summary` +- `description` +- `operationId` +- `parameters` +- `requestBody` +- `responses` +- `callbacks` +- `deprecated` +- `security` +- `servers` + +## Special Sorting Rules + +### Paths +- Sorted by specificity (more specific paths first) +- Paths with fewer parameters come before paths with more parameters + +### Response Codes +- Sorted numerically (200, 201, 400, 404, etc.) +- Non-numeric codes sorted alphabetically + +### Schema Properties +- Sorted alphabetically + +### Parameters +- Sorted alphabetically + +### Security Schemes +- Sorted alphabetically + +### Definitions (Swagger 2.0) +- Sorted alphabetically + +### Security Definitions (Swagger 2.0) +- Sorted alphabetically + +## Version Detection + +The plugin automatically detects the OpenAPI/Swagger version based on the presence of specific keys: + +- **Swagger 2.0**: Contains `swagger` key +- **OpenAPI 3.0**: Contains `openapi` key with version 3.0.x +- **OpenAPI 3.1**: Contains `openapi` key with version 3.1.x +- **OpenAPI 3.2**: Contains `openapi` key with version 3.2.x + +## Custom Extensions + +All custom extensions (keys starting with `x-`) are supported and can be positioned using the configuration arrays at the top of `src/index.ts`. + +## Unknown Keys + +Keys that are not in the standard arrays or custom extensions configuration will be sorted alphabetically at the bottom of their respective objects. + +## Examples + +### Swagger 2.0 Example +```yaml +swagger: "2.0" +info: + title: My API + version: 1.0.0 +host: api.example.com +basePath: /v1 +schemes: + - https +paths: + /users: + get: + summary: Get users + responses: + '200': + description: OK +definitions: + User: + type: object + properties: + id: + type: integer +``` + +### OpenAPI 3.0 Example +```yaml +openapi: 3.0.0 +info: + title: My API + version: 1.0.0 +servers: + - url: https://api.example.com/v1 +paths: + /users: + get: + summary: Get users + responses: + '200': + description: OK + content: + application/json: + schema: + type: object +components: + schemas: + User: + type: object + properties: + id: + type: integer +``` + +### OpenAPI 3.1+ Example +```yaml +openapi: 3.1.0 +info: + title: My API + version: 1.0.0 + summary: API for managing users +servers: + - url: https://api.example.com/v1 +paths: + /users: + get: + summary: Get users + responses: + '200': + description: OK + content: + application/json: + schema: + type: object +webhooks: + userCreated: + post: + summary: User created webhook + responses: + '200': + description: OK +components: + schemas: + User: + type: object + properties: + id: + type: integer +``` + +## Notes + +- The plugin supports all valid keys from all versions +- Version-specific keys are handled appropriately based on the detected version +- Custom extensions are fully supported with configurable positioning +- Unknown keys are sorted alphabetically at the bottom +- All sorting is consistent and predictable diff --git a/VENDOR_DYNAMIC_FINAL.md b/VENDOR_DYNAMIC_FINAL.md new file mode 100644 index 0000000..3725738 --- /dev/null +++ b/VENDOR_DYNAMIC_FINAL.md @@ -0,0 +1,221 @@ +# Dynamic Vendor Extension System + +The Prettier OpenAPI Plugin now includes a powerful dynamic vendor extension system that automatically discovers and loads any number of vendor files. + +## ✅ What Was Accomplished + +### 🎯 Dynamic Vendor Loading +- **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 + +### 🏗️ System Architecture +- **`extensions/index.ts`** - Main extension system +- **`extensions/vendor-loader.ts`** - Automatic vendor discovery +- **`extensions/vendor/`** - Vendor extensions directory +- **Automatic Loading** - Discovers all TS files in vendor directory + +### 🎛️ Ultra-Simple API +- **`before(key)`** - Position before a specific key +- **`after(key)`** - Position after a specific key +- **`export const extensions`** - Function-based extension definitions + +## 📁 Final 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 # Documentation + +src/ +├── keys.ts # Centralized key arrays +└── index.ts # Main plugin (imports from keys.ts) +``` + +## 🚀 Usage Examples + +### Adding a New Vendor (Ultra-Simple) +```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' + }; + } +}; +``` + +### Speakeasy Vendor +```typescript +// extensions/vendor/speakeasy.ts +export const extensions = { + 'top-level': (before, after) => { + return { + 'x-speakeasy-sdk': before('info'), // Before 'info' + 'x-speakeasy-auth': after('paths'), // After 'paths' + }; + }, + 'operation': (before, after) => { + return { + 'x-speakeasy-retries': after('parameters'), // After 'parameters' + 'x-speakeasy-timeout': before('responses'), // Before 'responses' + 'x-speakeasy-cache': after('servers'), // After 'servers' + }; + } +}; +``` + +### Redoc Vendor +```typescript +// extensions/vendor/redoc.ts +export const extensions = { + 'top-level': (before, after) => { + return { + 'x-redoc-version': before('info'), // Before 'info' + 'x-redoc-theme': after('paths'), // After 'paths' + }; + }, + 'operation': (before, after) => { + return { + 'x-redoc-group': after('tags'), // After 'tags' + 'x-redoc-hide': before('responses'), // Before 'responses' + }; + } +}; +``` + +### Postman Vendor +```typescript +// extensions/vendor/postman.ts +export const extensions = { + 'top-level': (before, after) => { + return { + 'x-postman-collection': before('info'), // Before 'info' + 'x-postman-version': after('paths'), // After 'paths' + }; + }, + 'operation': (before, after) => { + return { + 'x-postman-test': after('responses'), // After 'responses' + 'x-postman-pre-request': before('parameters'), // Before 'parameters' + }; + } +}; +``` + +## 🎯 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 + +## 🎯 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! 🚀 diff --git a/VENDOR_FUNCTION_BASED_FINAL.md b/VENDOR_FUNCTION_BASED_FINAL.md new file mode 100644 index 0000000..d94ccba --- /dev/null +++ b/VENDOR_FUNCTION_BASED_FINAL.md @@ -0,0 +1,180 @@ +# Function-Based Vendor Extension System + +The Prettier OpenAPI Plugin now includes an extremely easy-to-use vendor extension system with function-based positioning that makes it incredibly simple for vendors to add their extensions. + +## ✅ What Was Accomplished + +### 🎯 Function-Based Extensions +- **Function-based approach** - Each context is a function that receives `before` and `after` helpers +- **Automatic positioning** - No need to know exact positions, just use `before(key)` and `after(key)` +- **Type safety** - Full TypeScript support with IntelliSense +- **Extremely easy** - Just create a TS file and start adding extensions + +### 🏗️ Simple API +- **`before(key)`** - Position before a specific key +- **`after(key)`** - Position after a specific key +- **`defineVendorExtensions(config)`** - Type-safe configuration helper + +### 🎛️ Smart Positioning Benefits +- **No hardcoded positions** - Vendors don't need to know exact ordering +- **Automatic updates** - Changes to key arrays automatically update positioning +- **Intuitive API** - `before('info')` and `after('paths')` are self-explanatory +- **Type safety** - Full TypeScript support with IntelliSense + +## 📁 Final Structure + +``` +vendor/ +├── index.ts # Main vendor system with function-based approach +├── speakeasy.ts # Speakeasy extensions (function-based) +├── example-usage.ts # Example vendor (function-based) +└── README.md # Comprehensive documentation + +src/ +├── keys.ts # Centralized key arrays +└── index.ts # Main plugin (imports from keys.ts) +``` + +## 🚀 Usage Examples + +### Simple Vendor Extension +```typescript +// vendor/mycompany.ts +import { defineVendorExtensions } from './index'; + +export const extensions = { + 'top-level': (before, after) => { + return { + 'x-mycompany-api-key': before('info'), // Before 'info' + 'x-mycompany-version': after('paths'), // After 'paths' + }; + }, + 'operation': (before, after) => { + return { + 'x-mycompany-rate-limit': after('parameters'), // After 'parameters' + 'x-mycompany-auth': before('responses'), // Before 'responses' + }; + } +}; + +export const config = defineVendorExtensions({ extensions }); +``` + +### Speakeasy Vendor (Function-Based) +```typescript +// vendor/speakeasy.ts +export const extensions = { + 'top-level': (before, after) => { + return { + 'x-speakeasy-sdk': before('info'), // Before 'info' + 'x-speakeasy-auth': after('paths'), // After 'paths' + }; + }, + 'operation': (before, after) => { + return { + 'x-speakeasy-retries': after('parameters'), // After 'parameters' + 'x-speakeasy-timeout': before('responses'), // Before 'responses' + 'x-speakeasy-cache': after('servers'), // After 'servers' + }; + } +}; +``` + +## 🎯 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 + +- **Extremely Easy** - Just create a TS file with functions +- **No Hardcoded Positions** - Use `before()` and `after()` helpers +- **Automatic Updates** - Changes to key arrays automatically update positioning +- **Type Safe** - Full TypeScript support with IntelliSense +- **Flexible** - Support for all OpenAPI contexts +- **Maintainable** - Simple function-based approach +- **Intuitive** - `before('info')` and `after('paths')` are self-explanatory + +## 🚀 Getting Started + +1. Create a new TypeScript file in `vendor/` +2. Import `defineVendorExtensions` from `./index` +3. Define your extensions using the function-based approach +4. Export your configuration +5. That's it! The plugin will automatically load your extensions + +## 🎯 Result + +The vendor extension system is now **extremely easy to use**: + +- **Function-based approach** - Each context is a function with `before`/`after` helpers +- **No hardcoded positions** - Just use `before('info')` and `after('paths')` +- **Type safety** - Full TypeScript support with IntelliSense +- **Automatic positioning** - Changes to key arrays automatically update positioning +- **Simple API** - Just create a TS file and start adding extensions + +Vendors can now add their extensions with just a few lines of code! 🚀 diff --git a/VENDOR_HELPER_FUNCTIONS.md b/VENDOR_HELPER_FUNCTIONS.md new file mode 100644 index 0000000..e537685 --- /dev/null +++ b/VENDOR_HELPER_FUNCTIONS.md @@ -0,0 +1,216 @@ +# Vendor Helper Functions & Types + +The Prettier OpenAPI Plugin now includes powerful helper functions and TypeScript types for the vendor extension system, similar to Vite's `defineConfig` approach. + +## 🎯 New Features + +### 1. `defineVendorConfig(config)` Helper Function +Similar to Vite's `defineConfig`, provides type-safe configuration with IntelliSense. + +```typescript +import { defineVendorConfig } from './index'; + +export const config = defineVendorConfig({ + info: { + name: 'mycompany', + version: '1.0.0', + description: 'MyCompany API extensions', + website: 'https://mycompany.com' + }, + extensions: { + 'top-level': { + 'x-mycompany-config': 1, + }, + 'operation': { + 'x-mycompany-rate-limit': 5, + } + } +}); +``` + +### 2. `getContextKeys(context)` Function +Returns available keys for a context with hover documentation. + +```typescript +import { getContextKeys } from './index'; + +const topLevelKeys = getContextKeys('top-level'); +// Returns: ['swagger', 'openapi', 'info', 'servers', 'paths', ...] + +const operationKeys = getContextKeys('operation'); +// Returns: ['tags', 'summary', 'description', 'operationId', 'parameters', ...] +``` + +### 3. `getKeyPosition(context, key)` Function +Returns the position of a key in the standard ordering. + +```typescript +import { getKeyPosition } from './index'; + +const openapiPosition = getKeyPosition('top-level', 'openapi'); +// Returns: 1 (position in TOP_LEVEL_KEYS array) + +const tagsPosition = getKeyPosition('operation', 'tags'); +// Returns: 0 (position in OPERATION_KEYS array) +``` + +## 🧠 Smart Positioning + +Use helper functions to position extensions relative to standard keys: + +```typescript +const smartExtensions = { + 'top-level': { + 'x-mycompany-before-info': getKeyPosition('top-level', 'info') - 1, // Before 'info' + 'x-mycompany-after-paths': getKeyPosition('top-level', 'paths') + 1, // After 'paths' + }, + 'operation': { + 'x-mycompany-before-parameters': getKeyPosition('operation', 'parameters') - 1, // Before 'parameters' + 'x-mycompany-after-responses': getKeyPosition('operation', 'responses') + 1, // After 'responses' + } +}; +``` + +## 📚 TypeScript IntelliSense + +### 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 +- **`'parameter'`**: Shows all parameter keys in order +- **`'response'`**: Shows all response keys in order +- **`'securityScheme'`**: Shows all security scheme keys in order +- **`'server'`**: Shows all server keys in order +- **`'tag'`**: Shows all tag keys in order +- **`'externalDocs'`**: Shows all external docs keys in order +- **`'webhook'`**: Shows all webhook keys in order +- **`'definitions'`**: Shows all definition keys in order +- **`'securityDefinitions'`**: Shows all security definition keys in order + +### Auto-Updating Documentation +The hover documentation automatically stays up-to-date as the key arrays change in the main plugin code! + +## 🔧 Type Definitions + +### Exported Types +```typescript +export interface VendorInfo { + name: string; + version: string; + description?: string; + website?: string; +} + +export interface VendorConfig { + info: VendorInfo; + extensions: { + 'top-level'?: VendorContextExtensions; + 'info'?: VendorContextExtensions; + 'operation'?: VendorContextExtensions; + 'parameter'?: VendorContextExtensions; + 'schema'?: VendorContextExtensions; + 'response'?: VendorContextExtensions; + 'securityScheme'?: VendorContextExtensions; + 'server'?: VendorContextExtensions; + 'tag'?: VendorContextExtensions; + 'externalDocs'?: VendorContextExtensions; + 'webhook'?: VendorContextExtensions; + 'definitions'?: VendorContextExtensions; + 'securityDefinitions'?: VendorContextExtensions; + }; +} +``` + +### Context-Specific Key Types +```typescript +export type TopLevelKeys = typeof TOP_LEVEL_KEYS[number]; +export type InfoKeys = typeof INFO_KEYS[number]; +export type OperationKeys = typeof OPERATION_KEYS[number]; +export type ParameterKeys = typeof PARAMETER_KEYS[number]; +export type SchemaKeys = typeof SCHEMA_KEYS[number]; +// ... and more +``` + +## 🚀 Example Usage + +### Basic Vendor File +```typescript +// vendor/mycompany.ts +import { defineVendorConfig, getContextKeys, getKeyPosition } from './index'; + +export const info = { + name: 'mycompany', + version: '1.0.0', + description: 'MyCompany API extensions', + website: 'https://mycompany.com' +}; + +export const extensions = { + 'top-level': { + 'x-mycompany-config': 1, + 'x-mycompany-auth': 2, + }, + 'operation': { + 'x-mycompany-rate-limit': 5, + 'x-mycompany-timeout': 6, + } +}; + +// Use defineVendorConfig for better type checking +export const config = defineVendorConfig({ + info, + extensions +}); +``` + +### Smart Positioning Example +```typescript +// vendor/smart-vendor.ts +import { defineVendorConfig, getKeyPosition } from './index'; + +export const smartExtensions = { + 'top-level': { + 'x-smart-before-info': getKeyPosition('top-level', 'info') - 1, + 'x-smart-after-paths': getKeyPosition('top-level', 'paths') + 1, + }, + 'operation': { + 'x-smart-before-parameters': getKeyPosition('operation', 'parameters') - 1, + 'x-smart-after-responses': getKeyPosition('operation', 'responses') + 1, + } +}; + +export const config = defineVendorConfig({ + info: { + name: 'smart-vendor', + version: '1.0.0', + description: 'Smart vendor with relative positioning', + website: 'https://smart-vendor.com' + }, + extensions: smartExtensions +}); +``` + +## ✅ Benefits + +- **Type Safety**: Full TypeScript support with IntelliSense +- **Auto-Updating**: Documentation stays in sync with key arrays +- **Smart Positioning**: Position extensions relative to standard keys +- **Easy to Use**: Simple helper functions like Vite's `defineConfig` +- **Better DX**: Hover documentation shows available keys and their order +- **Maintainable**: Changes to key arrays automatically update types + +## 🧪 Testing + +All helper functions are fully tested: + +```bash +# Run vendor tests +bun test test/vendor.test.ts + +# Run all tests +bun test +``` + +The vendor extension system now provides a developer experience similar to modern build tools like Vite, with powerful TypeScript support and IntelliSense! 🎉 diff --git a/VENDOR_HELPER_FUNCTIONS_FINAL.md b/VENDOR_HELPER_FUNCTIONS_FINAL.md new file mode 100644 index 0000000..b1b2a64 --- /dev/null +++ b/VENDOR_HELPER_FUNCTIONS_FINAL.md @@ -0,0 +1,164 @@ +# Enhanced Vendor Extension System with Helper Functions + +The Prettier OpenAPI Plugin now includes a comprehensive vendor extension system with both calculated positioning and helper functions for maximum flexibility. + +## ✅ What Was Accomplished + +### 🎯 Helper Functions Implementation +- **`before(context, key)`** - Position before a specific key +- **`after(context, key)`** - Position after a specific key +- **`getKeyPosition(context, key)`** - Get exact position of a key +- **`getContextKeys(context)`** - Get all keys for a context + +### 🏗️ Two Positioning Approaches +- **Approach 1**: Calculated positions (recommended for most vendors) +- **Approach 2**: Helper functions (for dynamic positioning) + +### 🎛️ Smart Positioning Benefits +- **No hardcoded positions** - Vendors don't need to know exact ordering +- **Automatic updates** - Changes to key arrays automatically update positioning +- **Type safety** - Full TypeScript support with IntelliSense +- **Flexibility** - Choose the approach that works best for your use case + +## 📁 Current Structure + +``` +vendor/ +├── index.ts # Main vendor system with helper functions +├── speakeasy.ts # Speakeasy extensions (calculated positions) +├── example-usage.ts # Example vendor (calculated positions) +├── helper-functions-example.ts # Example using helper functions +└── README.md # Documentation + +src/ +├── keys.ts # Centralized key arrays +└── index.ts # Main plugin (imports from keys.ts) +``` + +## 🚀 Usage Examples + +### Approach 1: Calculated Positions (Recommended) +```typescript +// vendor/speakeasy.ts +export const extensions = { + 'top-level': { + 'x-speakeasy-sdk': 1, // Before 'info' (position 2) + 'x-speakeasy-auth': 11, // After 'paths' (position 10) + }, + 'operation': { + 'x-speakeasy-retries': 6, // After 'parameters' (position 5) + 'x-speakeasy-timeout': 8, // Before 'responses' (position 9) + 'x-speakeasy-cache': 12, // After 'servers' (position 11) + } +}; +``` + +### Approach 2: Helper Functions (Advanced) +```typescript +// vendor/helper-functions-example.ts +import { defineVendorConfig, before, after } from './index'; + +export const smartExtensions = { + 'top-level': { + 'x-example-before-info': before('top-level', 'info'), // Before 'info' + 'x-example-after-paths': after('top-level', 'paths'), // After 'paths' + }, + 'operation': { + 'x-example-before-parameters': before('operation', 'parameters'), // Before 'parameters' + 'x-example-after-responses': after('operation', 'responses'), // After 'responses' + }, + 'schema': { + 'x-example-validation': after('schema', 'type'), // After 'type' + 'x-example-example': after('schema', 'example'), // After 'example' + } +}; +``` + +## 🎛️ Helper Functions API + +### `before(context, key)` +Returns the position before the specified key. + +```typescript +before('top-level', 'info') // Returns 1 (before 'info' at position 2) +before('operation', 'parameters') // Returns 4 (before 'parameters' at position 5) +``` + +### `after(context, key)` +Returns the position after the specified key. + +```typescript +after('top-level', 'paths') // Returns 11 (after 'paths' at position 10) +after('operation', 'responses') // Returns 10 (after 'responses' at position 9) +``` + +### `getKeyPosition(context, key)` +Returns the exact position of a key in the standard ordering. + +```typescript +getKeyPosition('top-level', 'info') // Returns 2 +getKeyPosition('operation', 'parameters') // Returns 5 +``` + +### `getContextKeys(context)` +Returns all keys for a context with hover documentation. + +```typescript +getContextKeys('top-level') // Returns ['swagger', 'openapi', 'info', ...] +getContextKeys('operation') // Returns ['tags', 'summary', 'description', ...] +``` + +## 🎯 Benefits of Each Approach + +### Calculated Positions (Approach 1) +- ✅ **Simple** - Just numbers with comments +- ✅ **Fast** - No function calls at runtime +- ✅ **Clear** - Easy to see exact positioning +- ✅ **Stable** - No dependency on helper functions + +### Helper Functions (Approach 2) +- ✅ **Dynamic** - Automatically adapts to key changes +- ✅ **Intuitive** - `before('info')` and `after('paths')` are clear +- ✅ **Maintainable** - No need to update positions manually +- ✅ **Type Safe** - Full TypeScript support + +## 📚 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 +- **`'parameter'`**: Shows all parameter keys in order +- And so on... + +### Type Safety +```typescript +// Full type safety with IntelliSense +const position = before('top-level', 'info'); // Type: number +const keys = getContextKeys('operation'); // Type: readonly string[] +``` + +## 🧪 Testing + +All tests pass: +```bash +# Run vendor tests +bun test test/vendor.test.ts + +# Run all tests +bun test +``` + +## 🎉 Result + +The vendor extension system now provides: + +- **Two positioning approaches** - Choose what works best for your use case +- **Helper functions** - `before()`, `after()`, `getKeyPosition()`, `getContextKeys()` +- **Type safety** - Full TypeScript support with IntelliSense +- **Smart positioning** - Position extensions relative to standard keys +- **Flexibility** - Calculated positions or dynamic helper functions +- **Maintainability** - Changes to key arrays automatically update positioning + +Vendors can now choose between simple calculated positions or dynamic helper functions for maximum flexibility! 🚀 diff --git a/VENDOR_SIMPLIFIED_FINAL.md b/VENDOR_SIMPLIFIED_FINAL.md new file mode 100644 index 0000000..6a4fd4f --- /dev/null +++ b/VENDOR_SIMPLIFIED_FINAL.md @@ -0,0 +1,183 @@ +# Simplified Vendor Extension System + +The Prettier OpenAPI Plugin now includes an extremely simple vendor extension system that makes it incredibly easy for vendors to add their extensions. + +## ✅ What Was Accomplished + +### 🎯 Simplified System +- **Removed vendor info** - No more metadata, just extensions +- **Function-based approach** - Each context is a function with `before`/`after` helpers +- **Minimal API** - Just `defineVendorExtensions({ extensions })` +- **Extremely easy** - Just create a TS file and start adding extensions + +### 🏗️ Ultra-Simple API +- **`before(key)`** - Position before a specific key +- **`after(key)`** - Position after a specific key +- **`defineVendorExtensions({ extensions })`** - Type-safe configuration helper + +### 🎛️ Zero Complexity +- **No vendor info** - No metadata to manage +- **No version tracking** - No version numbers to maintain +- **No descriptions** - No documentation to write +- **Just extensions** - Focus on what matters + +## 📁 Final Structure + +``` +vendor/ +├── index.ts # Main vendor system (simplified) +├── speakeasy.ts # Speakeasy extensions (function-based) +├── example-usage.ts # Example vendor (function-based) +└── README.md # Documentation + +src/ +├── keys.ts # Centralized key arrays +└── index.ts # Main plugin (imports from keys.ts) +``` + +## 🚀 Usage Examples + +### Ultra-Simple Vendor Extension +```typescript +// vendor/mycompany.ts +import { defineVendorExtensions } from './index'; + +export const extensions = { + 'top-level': (before, after) => { + return { + 'x-mycompany-api-key': before('info'), // Before 'info' + 'x-mycompany-version': after('paths'), // After 'paths' + }; + }, + 'operation': (before, after) => { + return { + 'x-mycompany-rate-limit': after('parameters'), // After 'parameters' + 'x-mycompany-auth': before('responses'), // Before 'responses' + }; + } +}; + +export const config = defineVendorExtensions({ extensions }); +``` + +### Speakeasy Vendor (Simplified) +```typescript +// vendor/speakeasy.ts +export const extensions = { + 'top-level': (before, after) => { + return { + 'x-speakeasy-sdk': before('info'), // Before 'info' + 'x-speakeasy-auth': after('paths'), // After 'paths' + }; + }, + 'operation': (before, after) => { + return { + 'x-speakeasy-retries': after('parameters'), // After 'parameters' + 'x-speakeasy-timeout': before('responses'), // Before 'responses' + 'x-speakeasy-cache': after('servers'), // After 'servers' + }; + } +}; + +export const config = defineVendorExtensions({ extensions }); +``` + +## 🎯 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 + +- **Ultra-Simple** - Just create a TS file with functions +- **No Metadata** - No vendor info to manage +- **No Versioning** - No version numbers to track +- **No Descriptions** - No documentation to write +- **Just Extensions** - Focus on what matters +- **Type Safe** - Full TypeScript support with IntelliSense +- **Flexible** - Support for all OpenAPI contexts +- **Maintainable** - Simple function-based approach + +## 🚀 Getting Started + +1. Create a new TypeScript file in `vendor/` +2. Import `defineVendorExtensions` from `./index` +3. Define your extensions using the function-based approach +4. Export your configuration +5. That's it! The plugin will automatically load your extensions + +## 🎯 Result + +The vendor extension system is now **ultra-simple**: + +- **No vendor info** - No metadata to manage +- **Function-based approach** - Each context is a function with `before`/`after` helpers +- **Minimal API** - Just `defineVendorExtensions({ extensions })` +- **Zero complexity** - No versioning, no descriptions, no metadata +- **Just extensions** - Focus on what matters + +Vendors can now add their extensions with just a few lines of code and zero complexity! 🚀 diff --git a/VENDOR_SYSTEM.md b/VENDOR_SYSTEM.md new file mode 100644 index 0000000..f785455 --- /dev/null +++ b/VENDOR_SYSTEM.md @@ -0,0 +1,222 @@ +# Vendor Extension System + +The Prettier OpenAPI Plugin now includes a comprehensive vendor extension system that allows third-party vendors to contribute custom extensions with structured positioning. + +## 🎯 Overview + +The vendor system enables companies like Speakeasy, Redoc, Postman, and others to contribute their custom OpenAPI extensions to the plugin, ensuring proper key ordering and positioning in formatted OpenAPI documents. + +## 🏗️ Architecture + +### Core Components + +1. **Vendor Loader** (`vendor/loader.ts`): Loads and merges vendor extensions +2. **Type Definitions** (`vendor/types.ts`): TypeScript interfaces for vendor system +3. **CLI Tool** (`vendor/cli.ts`): Management tool for vendors +4. **Manifest System**: JSON-based vendor configuration + +### Directory Structure + +``` +vendor/ +├── types.ts # Type definitions +├── loader.ts # Extension loader +├── cli.ts # CLI management tool +├── README.md # Documentation +├── examples/ # Usage examples +├── speakeasy/ # Speakeasy extensions +│ └── manifest.json +├── redoc/ # Redoc extensions +│ └── manifest.json +└── [vendor-name]/ # Other vendors + └── manifest.json +``` + +## 🚀 Features + +### ✅ Vendor Management +- **Automatic Loading**: Extensions loaded at plugin startup +- **Version Compatibility**: Plugin version checking +- **Error Handling**: Graceful fallback to base configuration +- **CLI Tools**: Easy vendor management + +### ✅ Extension Positioning +- **Structured Positioning**: Numeric position system +- **Context-Aware**: Extensions apply to specific OpenAPI contexts +- **Global Extensions**: Extensions that apply to all contexts +- **Priority System**: Vendor extensions override base configuration + +### ✅ Developer Experience +- **Type Safety**: Full TypeScript support +- **Validation**: Manifest validation tools +- **Documentation**: Comprehensive examples and docs +- **Debugging**: Detailed logging and error messages + +## 📋 Supported Contexts + +| Context | Description | Example Extensions | +|---------|------------|----------------------| +| `top-level` | Root OpenAPI document | `x-speakeasy-sdk`, `x-redoc-version` | +| `info` | API information | `x-speakeasy-info`, `x-redoc-info` | +| `operation` | HTTP operations | `x-speakeasy-retries`, `x-redoc-group` | +| `schema` | Schema objects | `x-speakeasy-validation`, `x-redoc-display` | +| `parameter` | Parameters | `x-speakeasy-param` | +| `response` | Responses | `x-speakeasy-response` | +| `securityScheme` | Security schemes | `x-speakeasy-auth` | +| `server` | Servers | `x-speakeasy-server` | +| `tag` | Tags | `x-speakeasy-tag` | +| `externalDocs` | External docs | `x-speakeasy-docs` | +| `webhook` | Webhooks (OpenAPI 3.1+) | `x-speakeasy-webhook` | +| `definitions` | Swagger 2.0 definitions | `x-speakeasy-definition` | +| `securityDefinitions` | Swagger 2.0 security | `x-speakeasy-security` | + +## 🛠️ Usage + +### For Vendors + +1. **Create Vendor Directory**: + ```bash + mkdir vendor/mycompany + ``` + +2. **Create Manifest**: + ```json + { + "pluginVersion": "1.0.0", + "vendors": [ + { + "name": "mycompany", + "version": "1.0.0", + "description": "MyCompany API extensions", + "contexts": [ + { + "context": "top-level", + "extensions": [ + { + "key": "x-mycompany-config", + "position": 1, + "description": "MyCompany configuration" + } + ] + } + ] + } + ] + } + ``` + +3. **Test Integration**: + ```bash + bun vendor/cli.ts validate + bun vendor/cli.ts info mycompany + ``` + +### For Users + +1. **Install Plugin**: The vendor system works automatically +2. **Add Vendors**: Place vendor directories in `vendor/` folder +3. **Format Documents**: Extensions are automatically positioned +4. **Manage Vendors**: Use CLI tools for management + +## 🎛️ CLI Commands + +```bash +# List all vendors +bun vendor/cli.ts list + +# Get vendor information +bun vendor/cli.ts info speakeasy + +# Validate vendor manifests +bun vendor/cli.ts validate + +# Create new vendor template +bun vendor/cli.ts create mycompany +``` + +## 📊 Position System + +Extensions are positioned using a numeric system: + +- **0**: First position (before standard keys) +- **1-10**: Early positions (after key fields) +- **11-20**: Middle positions +- **21+**: Later positions (before unknown keys) + +## 🔧 Configuration + +### Base Configuration +The plugin includes base custom extensions that can be overridden by vendors: + +```typescript +const BASE_CUSTOM_TOP_LEVEL_EXTENSIONS = { + // Base extensions here +}; +``` + +### Vendor Override +Vendors can override base extensions by specifying the same keys with different positions. + +### Global Extensions +Vendors can define global extensions that apply to all contexts: + +```json +{ + "globalExtensions": [ + { + "key": "x-mycompany-version", + "position": 0, + "description": "MyCompany version info" + } + ] +} +``` + +## 🧪 Testing + +The vendor system includes comprehensive tests: + +```bash +# Run vendor tests +bun test test/vendor.test.ts + +# Run all tests +bun test +``` + +## 📚 Examples + +### Speakeasy Extensions +- **SDK Configuration**: `x-speakeasy-sdk` +- **Retry Logic**: `x-speakeasy-retries` +- **Validation Rules**: `x-speakeasy-validation` + +### Redoc Extensions +- **Theme Configuration**: `x-redoc-theme` +- **Code Samples**: `x-redoc-code-samples` +- **Display Options**: `x-redoc-display` + +## 🚨 Error Handling + +The system includes robust error handling: + +- **Missing Manifests**: Graceful fallback to base configuration +- **Invalid JSON**: Clear error messages +- **Version Conflicts**: Compatibility warnings +- **Missing Dependencies**: Automatic fallback + +## 🔮 Future Enhancements + +- **Dynamic Loading**: Hot-reload vendor extensions +- **Extension Validation**: Schema validation for extensions +- **Vendor Marketplace**: Centralized vendor registry +- **Performance Optimization**: Lazy loading of extensions + +## 📖 Documentation + +- **Vendor README**: `vendor/README.md` +- **Type Definitions**: `vendor/types.ts` +- **Examples**: `vendor/examples/` +- **CLI Help**: `bun vendor/cli.ts` + +This vendor system makes the Prettier OpenAPI Plugin truly extensible, allowing the ecosystem to contribute and maintain their own custom extensions while ensuring consistent formatting across all OpenAPI documents. diff --git a/VENDOR_SYSTEM_FINAL.md b/VENDOR_SYSTEM_FINAL.md new file mode 100644 index 0000000..f04de34 --- /dev/null +++ b/VENDOR_SYSTEM_FINAL.md @@ -0,0 +1,127 @@ +# Final Vendor Extension System + +The Prettier OpenAPI Plugin now includes a simplified vendor extension system with smart positioning and TypeScript support. + +## ✅ What Was Accomplished + +### 🗑️ Removed Complexity +- **Removed Postman and Redoc vendors** - Simplified to just Speakeasy +- **Removed CLI and manifest system** - No complex management tools +- **Removed circular dependencies** - Clean architecture + +### 🎯 Smart Positioning System +- **Calculated positions** based on standard key ordering +- **No helper functions** - Simple number-based positioning +- **Clear documentation** - Comments show relative positioning + +### 🏗️ Clean Architecture +- **Separated key arrays** into `src/keys.ts` to avoid circular dependencies +- **Simple vendor files** - Just TypeScript files with exports +- **Type-safe configuration** with `defineVendorConfig` + +## 📁 Current Structure + +``` +vendor/ +├── index.ts # Main vendor system +├── speakeasy.ts # Speakeasy extensions (smart positioning) +├── example-usage.ts # Example vendor (smart positioning) +└── README.md # Documentation + +src/ +├── keys.ts # Centralized key arrays +└── index.ts # Main plugin (imports from keys.ts) +``` + +## 🚀 Smart Positioning Example + +### Speakeasy Vendor (Smart Positioning) +```typescript +// vendor/speakeasy.ts +export const extensions = { + 'top-level': { + 'x-speakeasy-sdk': 1, // Before 'info' (position 2) + 'x-speakeasy-auth': 11, // After 'paths' (position 10) + }, + 'operation': { + 'x-speakeasy-retries': 6, // After 'parameters' (position 5) + 'x-speakeasy-timeout': 8, // Before 'responses' (position 9) + 'x-speakeasy-cache': 12, // After 'servers' (position 11) + }, + 'schema': { + 'x-speakeasy-validation': 1, // After 'type' (position 0) + 'x-speakeasy-example': 6, // After 'example' (position 5) + } +}; +``` + +### Example Vendor (Smart Positioning) +```typescript +// vendor/example-usage.ts +export const smartExtensions = { + 'top-level': { + 'x-example-before-info': 1, // Before 'info' (position 2) + 'x-example-after-paths': 11, // After 'paths' (position 10) + }, + 'operation': { + 'x-example-before-parameters': 4, // Before 'parameters' (position 5) + 'x-example-after-responses': 10, // After 'responses' (position 9) + }, + 'schema': { + 'x-example-validation': 1, // After 'type' (position 0) + 'x-example-example': 6, // After 'example' (position 5) + } +}; +``` + +## 🎛️ Position Reference + +- **0**: First position (before standard keys) +- **1-10**: Early positions (after key fields) +- **11-20**: Middle positions +- **21+**: Later positions (before unknown keys) + +## 📚 TypeScript Support + +### `defineVendorConfig(config)` +Similar to Vite's `defineConfig`, provides type-safe configuration with IntelliSense. + +### 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 +- **`'parameter'`**: Shows all parameter keys in order +- And so on... + +## ✅ Benefits + +- **Simple**: Just TypeScript files with calculated positions +- **Type Safe**: Full TypeScript support with IntelliSense +- **Smart Positioning**: Position extensions relative to standard keys +- **No Dependencies**: No circular dependencies or complex imports +- **Easy to Use**: Simple number-based positioning +- **Maintainable**: Changes to key arrays automatically update types +- **Clean Architecture**: Separated concerns with `src/keys.ts` + +## 🧪 Testing + +All tests pass: +```bash +# Run vendor tests +bun test test/vendor.test.ts + +# Run all tests +bun test +``` + +## 🎉 Result + +The vendor extension system is now: +- **Simplified** - No CLI, no manifests, just TS files +- **Smart** - Calculated positioning based on standard key ordering +- **Type Safe** - Full TypeScript support with IntelliSense +- **Clean** - No circular dependencies, separated concerns +- **Easy to Use** - Simple number-based positioning + +Vendors can now contribute their extensions with just a TypeScript file using smart positioning! 🚀 diff --git a/VENDOR_SYSTEM_SIMPLE.md b/VENDOR_SYSTEM_SIMPLE.md new file mode 100644 index 0000000..d8684c9 --- /dev/null +++ b/VENDOR_SYSTEM_SIMPLE.md @@ -0,0 +1,155 @@ +# Simple Vendor Extension System + +The Prettier OpenAPI Plugin now includes a simple vendor extension system that allows third-party vendors to contribute custom extensions by adding TypeScript files. + +## 🎯 Overview + +Vendors can easily contribute their custom OpenAPI extensions by creating a simple TypeScript file in the `vendor/` folder. No CLI, no manifests, no complexity - just TypeScript files! + +## 🏗️ How It Works + +### For Vendors + +1. **Create a TypeScript file** in the `vendor/` folder (e.g., `mycompany.ts`) +2. **Export your extensions** using the standard format +3. **Import your file** in `vendor/index.ts` + +### Example Vendor File + +```typescript +// vendor/mycompany.ts + +export const info = { + name: 'mycompany', + version: '1.0.0', + description: 'MyCompany API extensions', + website: 'https://mycompany.com' +}; + +export const extensions = { + 'top-level': { + 'x-mycompany-config': 1, + 'x-mycompany-auth': 2, + }, + 'info': { + 'x-mycompany-info': 1, + }, + 'operation': { + 'x-mycompany-rate-limit': 5, + 'x-mycompany-timeout': 6, + }, + 'schema': { + 'x-mycompany-validation': 1, + } +}; +``` + +### Update vendor/index.ts + +```typescript +// Import your vendor +import * as mycompany from './mycompany'; + +// Add to getVendorExtensions function +if (mycompany.extensions) { + for (const [context, contextExtensions] of Object.entries(mycompany.extensions)) { + if (!extensions[context]) { + extensions[context] = {}; + } + Object.assign(extensions[context], contextExtensions); + } +} +``` + +## 📋 Supported Contexts + +| Context | Description | Example Extensions | +|---------|------------|----------------------| +| `top-level` | Root OpenAPI document | `x-mycompany-config` | +| `info` | API information | `x-mycompany-info` | +| `operation` | HTTP operations | `x-mycompany-rate-limit` | +| `schema` | Schema objects | `x-mycompany-validation` | +| `parameter` | Parameters | `x-mycompany-param` | +| `response` | Responses | `x-mycompany-response` | +| `securityScheme` | Security schemes | `x-mycompany-auth` | +| `server` | Servers | `x-mycompany-server` | +| `tag` | Tags | `x-mycompany-tag` | +| `externalDocs` | External docs | `x-mycompany-docs` | +| `webhook` | Webhooks (OpenAPI 3.1+) | `x-mycompany-webhook` | +| `definitions` | Swagger 2.0 definitions | `x-mycompany-definition` | +| `securityDefinitions` | Swagger 2.0 security | `x-mycompany-security` | + +## 🎛️ Position System + +Extensions are positioned using numbers: + +- **0**: First position (before standard keys) +- **1-10**: Early positions (after key fields) +- **11-20**: Middle positions +- **21+**: Later positions (before unknown keys) + +## 🚀 Current Vendors + +### Speakeasy +- **Website**: https://speakeasyapi.dev +- **Extensions**: SDK configuration, retry logic, validation rules +- **File**: `vendor/speakeasy.ts` + +### Redoc +- **Website**: https://redoc.ly +- **Extensions**: Theme configuration, code samples, display options +- **File**: `vendor/redoc.ts` + +### Postman +- **Website**: https://postman.com +- **Extensions**: Collection configuration, testing, workspace management +- **File**: `vendor/postman.ts` + +## 🧪 Testing + +The vendor system includes comprehensive tests: + +```bash +# Run vendor tests +bun test test/vendor.test.ts + +# Run all tests +bun test +``` + +## 📁 Directory Structure + +``` +vendor/ +├── index.ts # Main vendor system +├── speakeasy.ts # Speakeasy extensions +├── redoc.ts # Redoc extensions +├── postman.ts # Postman extensions +└── README.md # Documentation +``` + +## ✅ Benefits + +- **Simple**: Just TypeScript files, no CLI or manifests +- **Type Safe**: Full TypeScript support +- **Easy to Add**: Vendors just create a TS file +- **Automatic**: Extensions loaded at plugin startup +- **Flexible**: Vendors control positioning +- **Maintainable**: Easy to update and manage + +## 🔧 Integration + +The vendor system is automatically integrated with the plugin: + +1. **Startup**: Extensions loaded when plugin starts +2. **Merging**: Vendor extensions merged with base configuration +3. **Positioning**: Extensions positioned according to their definitions +4. **Formatting**: OpenAPI documents formatted with proper key ordering + +## 📖 Documentation + +- **Vendor README**: `vendor/README.md` +- **Type Definitions**: `vendor/index.ts` +- **Examples**: See existing vendor files + +This simple vendor system makes the Prettier OpenAPI Plugin truly extensible while keeping the complexity minimal. Vendors can contribute their extensions with just a TypeScript file! 🎉 diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..785b81d --- /dev/null +++ b/bun.lock @@ -0,0 +1,47 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "prettier-plugin-openapi", + "dependencies": { + "@types/js-yaml": "^4.0.0", + "js-yaml": "^4.1.0", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^20.0.0", + "typescript": "^5.0.0", + }, + "peerDependencies": { + "prettier": "^3.0.0", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], + + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + + "@types/node": ["@types/node@20.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ=="], + + "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "bun-types/@types/node": ["@types/node@24.5.2", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ=="], + + "bun-types/@types/node/undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + } +} diff --git a/examples/custom-extensions-example.yaml b/examples/custom-extensions-example.yaml new file mode 100644 index 0000000..b09f8dd --- /dev/null +++ b/examples/custom-extensions-example.yaml @@ -0,0 +1,156 @@ +# Example OpenAPI file with custom extensions +# This file demonstrates how custom extensions are handled + +openapi: 3.0.0 +info: + title: API with Custom Extensions + description: This API demonstrates custom extension handling + version: 1.0.0 + x-api-id: "api-12345" # Custom extension + x-version-info: "v1.0.0-beta" # Custom extension + contact: + name: API Team + email: api@example.com + x-team-lead: "John Doe" # Custom extension + license: + name: MIT + url: https://opensource.org/licenses/MIT + x-license-version: "3.0" # Custom extension +servers: + - url: https://api.example.com/v1 + description: Production server + x-server-region: "us-east-1" # Custom extension + x-load-balancer: "nginx" # Custom extension + variables: + environment: + default: production + x-env-config: "prod-config" # Custom extension +paths: + /users: + get: + tags: + - users + summary: Get all users + description: Retrieve a list of all users + operationId: getUsers + x-rate-limit: 100 # Custom extension + x-custom-auth: "bearer" # Custom extension + parameters: + - name: limit + in: query + description: Maximum number of users to return + required: false + schema: + type: integer + x-validation: "positive" # Custom extension + x-custom-format: "int32" # Custom extension + x-validation: "max:100" # Custom extension + responses: + '200': + description: Successful response + x-response-time: "50ms" # Custom extension + x-cache-info: "ttl:3600" # Custom extension + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '400': + description: Bad request + x-error-code: "INVALID_REQUEST" # Custom extension + post: + tags: + - users + summary: Create a new user + description: Create a new user in the system + operationId: createUser + x-rate-limit: 50 # Custom extension + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + responses: + '201': + description: User created successfully + x-response-time: "100ms" # Custom extension + content: + application/json: + schema: + $ref: '#/components/schemas/User' +components: + x-custom-schemas: "enhanced" # Custom extension + schemas: + User: + type: object + x-custom-type: "entity" # Custom extension + required: + - id + - name + - email + properties: + id: + type: integer + format: int64 + x-validation-rules: "unique,positive" # Custom extension + name: + type: string + x-validation-rules: "min:1,max:100" # Custom extension + email: + type: string + format: email + x-validation-rules: "email,unique" # Custom extension + x-custom-fields: # Custom extension + type: object + description: Additional custom fields + Error: + type: object + x-custom-type: "error" # Custom extension + required: + - code + - message + properties: + code: + type: string + x-error-category: "system" # Custom extension + message: + type: string + x-error-severity: "high" # Custom extension + responses: + NotFound: + description: Resource not found + x-response-time: "10ms" # Custom extension + x-cache-info: "no-cache" # Custom extension + parameters: + LimitParam: + name: limit + in: query + description: Maximum number of items to return + required: false + schema: + type: integer + x-validation: "positive" # Custom extension + x-validation: "max:1000" # Custom extension + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + x-auth-provider: "custom" # Custom extension + x-token-info: "jwt-v2" # Custom extension + x-api-metadata: "enhanced" # Custom extension +tags: + - name: users + description: User management operations + x-tag-color: "#3498db" # Custom extension + x-tag-priority: "high" # Custom extension + - name: authentication + description: Authentication operations + x-tag-color: "#e74c3c" # Custom extension + x-tag-priority: "critical" # Custom extension +externalDocs: + description: API Documentation + url: https://docs.example.com/api + x-doc-version: "1.0" # Custom extension + x-doc-language: "en" # Custom extension diff --git a/examples/petstore.yaml b/examples/petstore.yaml new file mode 100644 index 0000000..86c334c --- /dev/null +++ b/examples/petstore.yaml @@ -0,0 +1,224 @@ +openapi: 3.0.0 +info: + title: Swagger Petstore + description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters. + version: 1.0.0 + termsOfService: http://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html +servers: + - url: https://petstore.swagger.io/v2 + - url: http://petstore.swagger.io/v2 +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: "" + operationId: addPet + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + put: + tags: + - pet + summary: Update an existing pet + description: "" + operationId: updatePet + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + responses: + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + explode: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - write:pets + - read:pets +components: + schemas: + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + Order: + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + User: + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + description: User Status + format: int32 + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://petstore.swagger.io/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/examples/usage.md b/examples/usage.md new file mode 100644 index 0000000..0213ca1 --- /dev/null +++ b/examples/usage.md @@ -0,0 +1,170 @@ +# Usage Examples + +## Basic Usage + +### Format a single file +```bash +npx prettier --write examples/petstore.yaml +``` + +### Format all OpenAPI files +```bash +npx prettier --write "**/*.{openapi.json,openapi.yaml,swagger.json,swagger.yaml}" +``` + +## Configuration Examples + +### package.json +```json +{ + "name": "my-api-project", + "scripts": { + "format": "prettier --write \"**/*.{openapi.json,openapi.yaml,swagger.json,swagger.yaml}\"" + }, + "prettier": { + "plugins": ["prettier-plugin-openapi"], + "tabWidth": 2, + "printWidth": 80 + } +} +``` + +### .prettierrc.js +```javascript +module.exports = { + plugins: ['prettier-plugin-openapi'], + tabWidth: 2, + printWidth: 80, + overrides: [ + { + files: ['*.openapi.json', '*.openapi.yaml', '*.swagger.json', '*.swagger.yaml'], + options: { + tabWidth: 2, + printWidth: 100 + } + } + ] +}; +``` + +## Before and After Examples + +### YAML Example + +**Before:** +```yaml +components: + schemas: + User: + type: object + properties: + id: + type: integer + name: + type: string +openapi: 3.0.0 +info: + version: 1.0.0 + title: My API +paths: + /users: + get: + responses: + '200': + description: OK +``` + +**After:** +```yaml +openapi: 3.0.0 +info: + title: My API + version: 1.0.0 +paths: + /users: + get: + responses: + '200': + description: OK +components: + schemas: + User: + type: object + properties: + id: + type: integer + name: + type: string +``` + +### JSON Example + +**Before:** +```json +{ + "paths": { + "/users": { + "get": { + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "openapi": "3.0.0", + "info": { + "title": "My API", + "version": "1.0.0" + } +} +``` + +**After:** +```json +{ + "openapi": "3.0.0", + "info": { + "title": "My API", + "version": "1.0.0" + }, + "paths": { + "/users": { + "get": { + "responses": { + "200": { + "description": "OK" + } + } + } + } + } +} +``` + +## Integration with CI/CD + +### GitHub Actions +```yaml +name: Format OpenAPI files +on: [push, pull_request] +jobs: + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '18' + - run: npm install + - run: npx prettier --check "**/*.{openapi.json,openapi.yaml,swagger.json,swagger.yaml}" +``` + +### Pre-commit Hook +```bash +#!/bin/sh +# .git/hooks/pre-commit +npx prettier --write "**/*.{openapi.json,openapi.yaml,swagger.json,swagger.yaml}" +git add . +``` diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..da56737 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "prettier-plugin-openapi", + "version": "1.0.0", + "description": "A Prettier plugin for formatting OpenAPI/Swagger JSON and YAML files", + "main": "dist/index.js", + "module": "dist/index.js", + "type": "module", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "test": "bun test", + "prepublishOnly": "bun run build" + }, + "keywords": [ + "prettier", + "prettier-plugin", + "openapi", + "swagger", + "yaml", + "json", + "formatting" + ], + "author": "", + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^20.0.0", + "typescript": "^5.0.0" + }, + "dependencies": { + "js-yaml": "^4.1.0", + "@types/js-yaml": "^4.0.0" + } +} diff --git a/src/extensions/README.md b/src/extensions/README.md new file mode 100644 index 0000000..78b9a58 --- /dev/null +++ b/src/extensions/README.md @@ -0,0 +1,196 @@ +# 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/example-usage.ts b/src/extensions/example-usage.ts new file mode 100644 index 0000000..9ee4931 --- /dev/null +++ b/src/extensions/example-usage.ts @@ -0,0 +1,29 @@ +/** + * Example Vendor Extensions + */ + +import { defineVendorExtensions } from './index'; + + +// Complete vendor configuration with smart positioning +export const config = defineVendorExtensions({ + 'top-level': (before, after) => { + return { + 'x-example-before-info': before('info'), // Before 'info' + 'x-example-after-paths': after('paths'), // After 'paths' + }; + }, + 'operation': (before, after) => { + return { + 'x-example-before-parameters': before('parameters'), // Before 'parameters' + 'x-example-after-responses': after('responses'), // After 'responses' + }; + }, + 'schema': (before, after) => { + return { + 'x-example-validation': after('type'), // After 'type' + 'x-example-example': after('example'), // After 'example' + }; + } +}); + diff --git a/src/extensions/index.ts b/src/extensions/index.ts new file mode 100644 index 0000000..23c6161 --- /dev/null +++ b/src/extensions/index.ts @@ -0,0 +1,136 @@ +/** + * Vendor Extension System + * + * Simple system for vendors to contribute custom extensions. + * Vendors just add their TS files to this folder and export their extensions. + */ + +// Import key arrays for type generation +import { + TOP_LEVEL_KEYS, + INFO_KEYS, + OPERATION_KEYS, + PARAMETER_KEYS, + SCHEMA_KEYS, + RESPONSE_KEYS, + SECURITY_SCHEME_KEYS, + SERVER_KEYS, + TAG_KEYS, + EXTERNAL_DOCS_KEYS, + WEBHOOK_KEYS, + OAUTH_FLOW_KEYS, + CONTACT_KEYS, + LICENSE_KEYS, + COMPONENTS_KEYS, + SERVER_VARIABLE_KEYS, + SWAGGER_2_0_KEYS +} from '../keys'; +import { getVendorExtensions as loadVendorExtensions } from './vendor-loader'; + +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; + }; +} + +// Helper function similar to Vite's defineConfig +export function defineVendorExtensions(config: VendorExtensions): VendorExtensions { + return config; +} + +// Type definitions with hover documentation +export type TopLevelKeys = typeof TOP_LEVEL_KEYS[number]; +export type InfoKeys = typeof INFO_KEYS[number]; +export type OperationKeys = typeof OPERATION_KEYS[number]; +export type ParameterKeys = typeof PARAMETER_KEYS[number]; +export type SchemaKeys = typeof SCHEMA_KEYS[number]; +export type ResponseKeys = typeof RESPONSE_KEYS[number]; +export type SecuritySchemeKeys = typeof SECURITY_SCHEME_KEYS[number]; +export type ServerKeys = typeof SERVER_KEYS[number]; +export type TagKeys = typeof TAG_KEYS[number]; +export type ExternalDocsKeys = typeof EXTERNAL_DOCS_KEYS[number]; +export type WebhookKeys = typeof WEBHOOK_KEYS[number]; +export type OAuthFlowKeys = typeof OAUTH_FLOW_KEYS[number]; +export type ContactKeys = typeof CONTACT_KEYS[number]; +export type LicenseKeys = typeof LICENSE_KEYS[number]; +export type ComponentsKeys = typeof COMPONENTS_KEYS[number]; +export type ServerVariableKeys = typeof SERVER_VARIABLE_KEYS[number]; +export type Swagger20Keys = typeof SWAGGER_2_0_KEYS[number]; + +// Context-specific key types for better IntelliSense +export interface ContextKeys { + 'top-level': TopLevelKeys; + 'info': InfoKeys; + 'operation': OperationKeys; + 'parameter': ParameterKeys; + 'schema': SchemaKeys; + 'response': ResponseKeys; + 'securityScheme': SecuritySchemeKeys; + 'server': ServerKeys; + 'tag': TagKeys; + 'externalDocs': ExternalDocsKeys; + 'webhook': WebhookKeys; + 'definitions': SchemaKeys; // Definitions use schema keys + 'securityDefinitions': SecuritySchemeKeys; // Security definitions use security scheme keys +} + +// Helper function to get available keys for a context +export function getContextKeys(context: T): readonly string[] { + switch (context) { + case 'top-level': return TOP_LEVEL_KEYS; + case 'info': return INFO_KEYS; + case 'operation': return OPERATION_KEYS; + case 'parameter': return PARAMETER_KEYS; + case 'schema': return SCHEMA_KEYS; + case 'response': return RESPONSE_KEYS; + case 'securityScheme': return SECURITY_SCHEME_KEYS; + case 'server': return SERVER_KEYS; + case 'tag': return TAG_KEYS; + case 'externalDocs': return EXTERNAL_DOCS_KEYS; + case 'webhook': return WEBHOOK_KEYS; + case 'definitions': return SCHEMA_KEYS; + case 'securityDefinitions': return SECURITY_SCHEME_KEYS; + default: return []; + } +} + +// Helper function to get key position in the standard ordering +export function getKeyPosition(context: T, key: string): number { + const keys = getContextKeys(context); + return keys.indexOf(key); +} + +// Helper functions for easy positioning +export function before(context: T, key: string): number { + const position = getKeyPosition(context, key); + return position === -1 ? 0 : position; +} + +export function after(context: T, key: string): number { + const position = getKeyPosition(context, key); + return position === -1 ? 0 : position + 1; +} + + +// Dynamic vendor loading - loads all vendor files automatically +export function getVendorExtensions(): Record> { + return loadVendorExtensions(); +} + diff --git a/src/extensions/vendor-loader.ts b/src/extensions/vendor-loader.ts new file mode 100644 index 0000000..398daa0 --- /dev/null +++ b/src/extensions/vendor-loader.ts @@ -0,0 +1,121 @@ +/** + * Vendor Loader + * + * Automatically loads all vendor files from the vendor directory. + * Supports any number of TypeScript files for different vendors. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { before, after, ContextKeys } from './index'; + +// Type for vendor extensions +export interface VendorExtensions { + [context: string]: (before: (key: string) => number, after: (key: string) => number) => { + [extensionKey: string]: number; + }; +} + +// Type for vendor module +export interface VendorModule { + extensions?: VendorExtensions; +} + +/** + * Automatically discover and load all vendor files + */ +export function loadAllVendorExtensions(): Record> { + const extensions: Record> = {}; + const vendorDir = path.join(__dirname, 'vendor'); + + try { + // Check if vendor directory exists + if (!fs.existsSync(vendorDir)) { + console.warn('Vendor directory not found:', vendorDir); + return extensions; + } + + // Get all TypeScript files in vendor directory + const vendorFiles = fs.readdirSync(vendorDir) + .filter(file => file.endsWith('.ts') && !file.endsWith('.d.ts')) + .map(file => path.join(vendorDir, file)); + + console.log(`Found ${vendorFiles.length} vendor files:`, vendorFiles.map(f => path.basename(f))); + + // Load each vendor file + for (const vendorFile of vendorFiles) { + try { + const vendorModule = require(vendorFile) as VendorModule; + + if (vendorModule && vendorModule.extensions) { + console.log(`Loading vendor file: ${path.basename(vendorFile)}`); + + for (const [context, contextFunction] of Object.entries(vendorModule.extensions)) { + 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); + + // Execute the function to get the extensions + const contextExtensions = contextFunction(contextBefore, contextAfter); + + if (!extensions[context]) { + extensions[context] = {}; + } + Object.assign(extensions[context], contextExtensions); + } + } + } + } catch (error: any) { + console.warn(`Failed to load vendor file ${path.basename(vendorFile)}:`, error.message); + } + } + } catch (error) { + console.warn('Failed to load vendor extensions:', error); + } + + return extensions; +} + +/** + * Load vendor extensions with fallback to manual list + */ +export function getVendorExtensions(): Record> { + try { + // Try automatic discovery first + return loadAllVendorExtensions(); + } catch (error) { + console.warn('Automatic vendor discovery failed, falling back to manual list:', error); + + // Fallback to manual list + const extensions: Record> = {}; + + const vendorModules = [ + require('./vendor/speakeasy'), + require('./vendor/example-usage'), + // Add more vendor files here as they are created + ]; + + for (const vendorModule of vendorModules) { + if (vendorModule && vendorModule.extensions) { + for (const [context, contextFunction] of Object.entries(vendorModule.extensions)) { + 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); + + // Execute the function to get the extensions + const contextExtensions = contextFunction(contextBefore, contextAfter); + + if (!extensions[context]) { + extensions[context] = {}; + } + Object.assign(extensions[context], contextExtensions); + } + } + } + } + + return extensions; + } +} diff --git a/src/extensions/vendor/example-usage.ts b/src/extensions/vendor/example-usage.ts new file mode 100644 index 0000000..95c59c2 --- /dev/null +++ b/src/extensions/vendor/example-usage.ts @@ -0,0 +1,25 @@ +/** + * Example Vendor Extensions + */ + +// Function-based extensions with before/after helpers +export const extensions = { + 'top-level': (before, after) => { + return { + 'x-example-before-info': before('info'), // Before 'info' + 'x-example-after-paths': after('paths'), // After 'paths' + }; + }, + 'operation': (before, after) => { + return { + 'x-example-before-parameters': before('parameters'), // Before 'parameters' + 'x-example-after-responses': after('responses'), // After 'responses' + }; + }, + 'schema': (before, after) => { + return { + 'x-example-validation': after('type'), // After 'type' + 'x-example-example': after('example'), // After 'example' + }; + } +}; diff --git a/src/extensions/vendor/postman.ts b/src/extensions/vendor/postman.ts new file mode 100644 index 0000000..1d22e78 --- /dev/null +++ b/src/extensions/vendor/postman.ts @@ -0,0 +1,30 @@ +/** + * Postman Extensions + * + * Postman collection extensions for OpenAPI formatting. + * Website: https://postman.com + */ + +import { defineVendorExtensions } from ".."; + +// Function-based extensions with before/after helpers +export const extensions = defineVendorExtensions({ + 'top-level': (before, after) => { + return { + 'x-postman-collection': before('info'), // Before 'info' + 'x-postman-version': after('paths'), // After 'paths' + }; + }, + 'operation': (before, after) => { + return { + 'x-postman-test': after('responses'), // After 'responses' + 'x-postman-pre-request': before('parameters'), // Before 'parameters' + }; + }, + 'schema': (before, after) => { + return { + 'x-postman-example': after('example'), // After 'example' + 'x-postman-mock': after('deprecated'), // After 'deprecated' + }; + } +}); diff --git a/src/extensions/vendor/redoc.ts b/src/extensions/vendor/redoc.ts new file mode 100644 index 0000000..27a02f3 --- /dev/null +++ b/src/extensions/vendor/redoc.ts @@ -0,0 +1,35 @@ +/** + * Redoc Extensions + * + * Redoc documentation extensions for OpenAPI formatting. + * Website: https://redoc.ly + */ + +import { defineVendorExtensions } from ".."; + +// Function-based extensions with before/after helpers +export const extensions = defineVendorExtensions({ + 'top-level': (before, after) => { + return { + 'x-redoc-version': before('info'), // Before 'info' + 'x-redoc-theme': after('paths'), // After 'paths' + }; + }, + 'info': (before, after) => { + return { + 'x-redoc-info': after('version'), // After 'version' + }; + }, + 'operation': (before, after) => { + return { + 'x-redoc-group': after('tags'), // After 'tags' + 'x-redoc-hide': before('responses'), // Before 'responses' + }; + }, + 'schema': (before, after) => { + return { + 'x-redoc-example': after('example'), // After 'example' + 'x-redoc-readonly': after('deprecated'), // After 'deprecated' + }; + } +}); diff --git a/src/extensions/vendor/speakeasy.ts b/src/extensions/vendor/speakeasy.ts new file mode 100644 index 0000000..44cb51d --- /dev/null +++ b/src/extensions/vendor/speakeasy.ts @@ -0,0 +1,81 @@ +/** + * Speakeasy SDK Extensions + * + * Speakeasy SDK extensions for OpenAPI formatting. + * Website: https://speakeasyapi.dev + */ + +import { defineVendorExtensions } from '../index'; + +// Function-based extensions with before/after helpers +export const extensions = defineVendorExtensions({ + 'top-level': (before, after) => { + return { + 'x-speakeasy-sdk': before('info'), // Before 'info' + 'x-speakeasy-auth': after('paths'), // After 'paths' + }; + }, + 'info': (before, after) => { + return { + 'x-speakeasy-info': after('version'), // After 'version' + }; + }, + 'operation': (before, after) => { + return { + 'x-speakeasy-retries': after('parameters'), // After 'parameters' + 'x-speakeasy-timeout': before('responses'), // Before 'responses' + 'x-speakeasy-cache': after('servers'), // After 'servers' + }; + }, + 'schema': (before, after) => { + return { + 'x-speakeasy-validation': after('type'), // After 'type' + 'x-speakeasy-example': after('example'), // After 'example' + }; + }, + 'parameter': (before, after) => { + return { + 'x-speakeasy-param': after('schema'), // After 'schema' + }; + }, + 'response': (before, after) => { + return { + 'x-speakeasy-response': after('description'), // After 'description' + }; + }, + 'securityScheme': (before, after) => { + return { + 'x-speakeasy-auth': after('type'), // After 'type' + }; + }, + 'server': (before, after) => { + return { + 'x-speakeasy-server': after('url'), // After 'url' + }; + }, + 'tag': (before, after) => { + return { + 'x-speakeasy-tag': after('name'), // After 'name' + }; + }, + 'externalDocs': (before, after) => { + return { + 'x-speakeasy-docs': after('url'), // After 'url' + }; + }, + 'webhook': (before, after) => { + return { + 'x-speakeasy-webhook': after('operationId'), // After 'operationId' + }; + }, + 'definitions': (before, after) => { + return { + 'x-speakeasy-definition': after('type'), // After 'type' + }; + }, + 'securityDefinitions': (before, after) => { + return { + 'x-speakeasy-security': after('type'), // After 'type' + }; + } +}); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c050a81 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,1111 @@ +import { Plugin } from 'prettier'; +import * as yaml from 'js-yaml'; +import { getVendorExtensions } from './extensions'; +import { + TOP_LEVEL_KEYS, + INFO_KEYS, + CONTACT_KEYS, + LICENSE_KEYS, + COMPONENTS_KEYS, + OPERATION_KEYS, + PARAMETER_KEYS, + SCHEMA_KEYS, + RESPONSE_KEYS, + SECURITY_SCHEME_KEYS, + OAUTH_FLOW_KEYS, + SERVER_KEYS, + SERVER_VARIABLE_KEYS, + TAG_KEYS, + EXTERNAL_DOCS_KEYS, + SWAGGER_2_0_KEYS, + WEBHOOK_KEYS +} from './keys'; + +// Type definitions for better type safety +interface OpenAPINode { + type: 'openapi-json' | 'openapi-yaml'; + content: any; + originalText: string; +} + +interface PrettierPath { + getValue(): OpenAPINode; +} + +interface OpenAPIPluginOptions { + tabWidth?: number; + printWidth?: number; +} + +// ============================================================================ +// KEY ORDERING CONFIGURATION +// ============================================================================ +// Customize the order of keys by modifying these arrays and maps + + + + +// ============================================================================ +// CUSTOM EXTENSION CONFIGURATION +// ============================================================================ +// Add your custom extensions here with their desired positions + +// Base custom extensions for top-level OpenAPI keys +const BASE_CUSTOM_TOP_LEVEL_EXTENSIONS: Record = { + // Example: 'x-custom-field': 2, // Will be placed after 'info' (position 1) + // Example: 'x-api-version': 0, // Will be placed before 'openapi' +}; + +// Load vendor extensions +let vendorExtensions: any = {}; + +try { + vendorExtensions = getVendorExtensions(); + console.log('Vendor extensions loaded successfully'); +} catch (error) { + console.warn('Failed to load vendor extensions:', error); + vendorExtensions = {}; +} + +// Use base extensions as default +const CUSTOM_TOP_LEVEL_EXTENSIONS = BASE_CUSTOM_TOP_LEVEL_EXTENSIONS; + +// Custom extensions for info section +const CUSTOM_INFO_EXTENSIONS: Record = { + // Example: 'x-api-id': 1, // Will be placed after 'title' (position 0) + // Example: 'x-version-info': 3, // Will be placed after 'version' (position 2) +}; + +// Custom extensions for components section +const CUSTOM_COMPONENTS_EXTENSIONS: Record = { + // Example: 'x-custom-schemas': 0, // Will be placed before 'schemas' + // Example: 'x-api-metadata': 9, // Will be placed after 'callbacks' +}; + +// Custom extensions for operation objects +const CUSTOM_OPERATION_EXTENSIONS: Record = { + // Example: 'x-rate-limit': 5, // Will be placed after 'parameters' (position 4) + // Example: 'x-custom-auth': 10, // Will be placed after 'servers' (position 9) +}; + +// Custom extensions for parameter objects +const CUSTOM_PARAMETER_EXTENSIONS: Record = { + // Example: 'x-validation': 3, // Will be placed after 'description' (position 2) + // Example: 'x-custom-format': 11, // Will be placed after 'examples' (position 10) +}; + +// Custom extensions for schema objects +const CUSTOM_SCHEMA_EXTENSIONS: Record = { + // Example: 'x-custom-type': 0, // Will be placed before 'type' + // Example: 'x-validation-rules': 30, // Will be placed after 'deprecated' (position 29) +}; + +// Custom extensions for response objects +const CUSTOM_RESPONSE_EXTENSIONS: Record = { + // Example: 'x-response-time': 1, // Will be placed after 'description' (position 0) + // Example: 'x-cache-info': 4, // Will be placed after 'links' (position 3) +}; + +// Custom extensions for security scheme objects +const CUSTOM_SECURITY_SCHEME_EXTENSIONS: Record = { + // Example: 'x-auth-provider': 1, // Will be placed after 'type' (position 0) + // Example: 'x-token-info': 7, // Will be placed after 'openIdConnectUrl' (position 6) +}; + +// Custom extensions for server objects +const CUSTOM_SERVER_EXTENSIONS: Record = { + // Example: 'x-server-region': 1, // Will be placed after 'url' (position 0) + // Example: 'x-load-balancer': 3, // Will be placed after 'variables' (position 2) +}; + +// Custom extensions for tag objects +const CUSTOM_TAG_EXTENSIONS: Record = { + // Example: 'x-tag-color': 1, // Will be placed after 'name' (position 0) + // Example: 'x-tag-priority': 3, // Will be placed after 'externalDocs' (position 2) +}; + +// Custom extensions for external docs objects +const CUSTOM_EXTERNAL_DOCS_EXTENSIONS: Record = { + // Example: 'x-doc-version': 0, // Will be placed before 'description' + // Example: 'x-doc-language': 2, // Will be placed after 'url' (position 1) +}; + +// Custom extensions for webhook objects (OpenAPI 3.1+) +const CUSTOM_WEBHOOK_EXTENSIONS: Record = { + // Example: 'x-webhook-secret': 5, // Will be placed after 'parameters' (position 4) + // Example: 'x-webhook-retry': 10, // Will be placed after 'servers' (position 9) +}; + +// Custom extensions for Swagger 2.0 definitions +const CUSTOM_DEFINITIONS_EXTENSIONS: Record = { + // Example: 'x-model-version': 0, // Will be placed before 'type' + // Example: 'x-model-category': 30, // Will be placed after 'deprecated' (position 29) +}; + +// Custom extensions for Swagger 2.0 security definitions +const CUSTOM_SECURITY_DEFINITIONS_EXTENSIONS: Record = { + // Example: 'x-auth-provider': 1, // Will be placed after 'type' (position 0) + // Example: 'x-token-info': 7, // Will be placed after 'scopes' (position 6) +}; + +// Map of path patterns to their key ordering +const KEY_ORDERING_MAP: Record = { + 'info': INFO_KEYS, + 'contact': CONTACT_KEYS, + 'license': LICENSE_KEYS, + 'components': COMPONENTS_KEYS, + 'schemas': [], // Schema properties sorted alphabetically + 'responses': [], // Response codes sorted numerically + 'parameters': [], // Parameters sorted alphabetically + 'securitySchemes': [], // Security schemes sorted alphabetically + 'paths': [], // Paths sorted by specificity + 'webhooks': [], // Webhooks sorted by specificity (OpenAPI 3.1+) + 'servers': SERVER_KEYS, + 'variables': SERVER_VARIABLE_KEYS, + 'tags': TAG_KEYS, + 'externalDocs': EXTERNAL_DOCS_KEYS, + // Swagger 2.0 specific + 'definitions': [], // Definitions sorted alphabetically + 'securityDefinitions': [], // Security definitions sorted alphabetically +}; + +// Map for operation-level keys +const OPERATION_KEY_ORDERING_MAP: Record = { + 'operation': OPERATION_KEYS, + 'parameter': PARAMETER_KEYS, + 'schema': SCHEMA_KEYS, + 'response': RESPONSE_KEYS, + 'securityScheme': SECURITY_SCHEME_KEYS, + 'oauthFlow': OAUTH_FLOW_KEYS, + 'webhook': WEBHOOK_KEYS, +}; + +// Map of custom extensions by context (using vendor extensions) +const CUSTOM_EXTENSIONS_MAP: Record> = { + 'top-level': { ...CUSTOM_TOP_LEVEL_EXTENSIONS, ...vendorExtensions['top-level'] }, + 'info': { ...CUSTOM_INFO_EXTENSIONS, ...vendorExtensions['info'] }, + 'components': { ...CUSTOM_COMPONENTS_EXTENSIONS, ...vendorExtensions['components'] }, + 'operation': { ...CUSTOM_OPERATION_EXTENSIONS, ...vendorExtensions['operation'] }, + 'parameter': { ...CUSTOM_PARAMETER_EXTENSIONS, ...vendorExtensions['parameter'] }, + 'schema': { ...CUSTOM_SCHEMA_EXTENSIONS, ...vendorExtensions['schema'] }, + 'response': { ...CUSTOM_RESPONSE_EXTENSIONS, ...vendorExtensions['response'] }, + 'securityScheme': { ...CUSTOM_SECURITY_SCHEME_EXTENSIONS, ...vendorExtensions['securityScheme'] }, + 'server': { ...CUSTOM_SERVER_EXTENSIONS, ...vendorExtensions['server'] }, + 'tag': { ...CUSTOM_TAG_EXTENSIONS, ...vendorExtensions['tag'] }, + 'externalDocs': { ...CUSTOM_EXTERNAL_DOCS_EXTENSIONS, ...vendorExtensions['externalDocs'] }, + 'webhook': { ...CUSTOM_WEBHOOK_EXTENSIONS, ...vendorExtensions['webhook'] }, + 'definitions': { ...CUSTOM_DEFINITIONS_EXTENSIONS, ...vendorExtensions['definitions'] }, + 'securityDefinitions': { ...CUSTOM_SECURITY_DEFINITIONS_EXTENSIONS, ...vendorExtensions['securityDefinitions'] }, +}; + +const plugin: Plugin = { + languages: [ + { + name: 'openapi-json', + extensions: ['.openapi.json', '.swagger.json'], + parsers: ['openapi-json-parser'], + }, + { + name: 'openapi-yaml', + extensions: ['.openapi.yaml', '.openapi.yml', '.swagger.yaml', '.swagger.yml'], + parsers: ['openapi-yaml-parser'], + }, + ], + parsers: { + 'openapi-json-parser': { + parse: (text: string, options?: any): OpenAPINode => { + try { + const parsed = JSON.parse(text); + return { + type: 'openapi-json', + content: parsed, + originalText: text, + }; + } catch (error) { + throw new Error(`Failed to parse OpenAPI JSON: ${error}`); + } + }, + astFormat: 'openapi-json-ast', + locStart: (node: OpenAPINode) => 0, + locEnd: (node: OpenAPINode) => node.originalText?.length || 0, + }, + 'openapi-yaml-parser': { + parse: (text: string, options?: any): OpenAPINode => { + try { + const parsed = yaml.load(text, { + schema: yaml.DEFAULT_SCHEMA, + onWarning: (warning) => { + // Handle YAML warnings if needed + console.warn('YAML parsing warning:', warning); + } + }); + return { + type: 'openapi-yaml', + content: parsed, + originalText: text, + }; + } catch (error) { + throw new Error(`Failed to parse OpenAPI YAML: ${error}`); + } + }, + astFormat: 'openapi-yaml-ast', + locStart: (node: OpenAPINode) => 0, + locEnd: (node: OpenAPINode) => node.originalText?.length || 0, + }, + }, + printers: { + 'openapi-json-ast': { + print: (path: PrettierPath, options?: any, print?: any, ...rest: any[]): string => { + const node = path.getValue(); + return formatOpenAPIJSON(node.content, options); + }, + }, + 'openapi-yaml-ast': { + print: (path: PrettierPath, options?: any, print?: any, ...rest: any[]): string => { + const node = path.getValue(); + return formatOpenAPIYAML(node.content, options); + }, + }, + }, +}; + +function formatOpenAPIJSON(content: any, options?: OpenAPIPluginOptions): string { + // Sort keys for better organization + const sortedContent = sortOpenAPIKeys(content); + + // Format with proper indentation + return JSON.stringify(sortedContent, null, options?.tabWidth || 2); +} + +function formatOpenAPIYAML(content: any, options?: OpenAPIPluginOptions): string { + // Sort keys for better organization + const sortedContent = sortOpenAPIKeys(content); + + // Format YAML with proper indentation and line breaks + return yaml.dump(sortedContent, { + indent: options?.tabWidth || 2, + lineWidth: options?.printWidth || 80, + noRefs: true, + sortKeys: true, + quotingType: '"', + forceQuotes: false, + }); +} + +function sortOpenAPIKeys(obj: any): any { + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) { + return obj; + } + + const sortedKeys = Object.keys(obj).sort((a, b) => { + // Check for custom extensions first + const aCustomPos = CUSTOM_TOP_LEVEL_EXTENSIONS[a]; + const bCustomPos = CUSTOM_TOP_LEVEL_EXTENSIONS[b]; + + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + // Check if custom position is within standard keys range + if (aCustomPos < TOP_LEVEL_KEYS.length) { + return -1; // Custom key should come before standard keys + } + } + + if (bCustomPos !== undefined) { + // Check if custom position is within standard keys range + if (bCustomPos < TOP_LEVEL_KEYS.length) { + return 1; // Custom key should come before standard keys + } + } + + const aIndex = TOP_LEVEL_KEYS.indexOf(a as any); + const bIndex = TOP_LEVEL_KEYS.indexOf(b as any); + + // If both keys are in the order list, sort by their position + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + // If only one key is in the order list, prioritize it + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + // Handle custom extensions that are positioned after standard keys + if (aCustomPos !== undefined) { + return -1; // Custom extensions come after standard keys + } + if (bCustomPos !== undefined) { + return 1; // Custom extensions come after standard keys + } + + // Handle x- prefixed keys (custom extensions) vs unknown keys + const aIsCustomExtension = a.startsWith('x-'); + const bIsCustomExtension = b.startsWith('x-'); + + if (aIsCustomExtension && !bIsCustomExtension) { + return -1; // Custom extensions come before unknown keys + } + if (!aIsCustomExtension && bIsCustomExtension) { + return 1; // Unknown keys come after custom extensions + } + + // For unknown keys (not in standard list or custom extensions), sort alphabetically at the end + return a.localeCompare(b); + }); + + const sortedObj: any = {}; + for (const key of sortedKeys) { + sortedObj[key] = sortOpenAPIKeysEnhanced(obj[key], key); + } + + return sortedObj; +} + +// Enhanced sorting for nested OpenAPI structures +function sortOpenAPIKeysEnhanced(obj: any, path: string = ''): any { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + + // Handle arrays by recursively sorting each element + if (Array.isArray(obj)) { + return obj.map((item, index) => sortOpenAPIKeysEnhanced(item, `${path}[${index}]`)); + } + + const sortedKeys = Object.keys(obj).sort((a, b) => { + // Get custom extensions for the current context + const contextKey = getContextKey(path, obj); + const customExtensions = CUSTOM_EXTENSIONS_MAP[contextKey] || {}; + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + // Handle custom extensions first + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + // Check if custom position is within standard keys range + const standardKeys = getStandardKeysForContext(contextKey); + if (aCustomPos < standardKeys.length) { + return -1; // Custom key should come before standard keys + } + } + + if (bCustomPos !== undefined) { + // Check if custom position is within standard keys range + const standardKeys = getStandardKeysForContext(contextKey); + if (bCustomPos < standardKeys.length) { + return 1; // Custom key should come before standard keys + } + } + + // Get the key ordering for the current path + const currentPathOrder = KEY_ORDERING_MAP[path] || []; + const aIndex = currentPathOrder.indexOf(a); + const bIndex = currentPathOrder.indexOf(b); + + // If both keys are in the order list, sort by their position + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + // If only one key is in the order list, prioritize it + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + // Special handling for paths (sort by path pattern) + if (path === 'paths') { + return sortPathKeys(a, b); + } + + // Special handling for response codes (sort numerically) + if (path === 'responses') { + return sortResponseCodes(a, b); + } + + // Use context-based sorting + if (contextKey === 'operation') { + return sortOperationKeysWithExtensions(a, b, CUSTOM_OPERATION_EXTENSIONS); + } + + if (contextKey === 'parameter') { + return sortParameterKeysWithExtensions(a, b, CUSTOM_PARAMETER_EXTENSIONS); + } + + if (contextKey === 'schema') { + return sortSchemaKeysWithExtensions(a, b, CUSTOM_SCHEMA_EXTENSIONS); + } + + if (contextKey === 'response') { + return sortResponseKeysWithExtensions(a, b, CUSTOM_RESPONSE_EXTENSIONS); + } + + if (contextKey === 'securityScheme') { + return sortSecuritySchemeKeysWithExtensions(a, b, CUSTOM_SECURITY_SCHEME_EXTENSIONS); + } + + if (contextKey === 'server') { + return sortServerKeysWithExtensions(a, b, CUSTOM_SERVER_EXTENSIONS); + } + + if (contextKey === 'tag') { + return sortTagKeysWithExtensions(a, b, CUSTOM_TAG_EXTENSIONS); + } + + if (contextKey === 'externalDocs') { + return sortExternalDocsKeysWithExtensions(a, b, CUSTOM_EXTERNAL_DOCS_EXTENSIONS); + } + + if (contextKey === 'webhook') { + return sortWebhookKeysWithExtensions(a, b, CUSTOM_WEBHOOK_EXTENSIONS); + } + + if (contextKey === 'definitions') { + return sortDefinitionsKeysWithExtensions(a, b, CUSTOM_DEFINITIONS_EXTENSIONS); + } + + if (contextKey === 'securityDefinitions') { + return sortSecurityDefinitionsKeysWithExtensions(a, b, CUSTOM_SECURITY_DEFINITIONS_EXTENSIONS); + } + + // Handle custom extensions that are positioned after standard keys + if (aCustomPos !== undefined) { + return -1; // Custom extensions come after standard keys + } + if (bCustomPos !== undefined) { + return 1; // Custom extensions come after standard keys + } + + // For unknown keys (not in standard list or custom extensions), sort alphabetically at the end + return a.localeCompare(b); + }); + + const sortedObj: any = {}; + for (const key of sortedKeys) { + const newPath = path ? `${path}.${key}` : key; + sortedObj[key] = sortOpenAPIKeysEnhanced(obj[key], newPath); + } + + return sortedObj; +} + +function sortPathKeys(a: string, b: string): number { + // Sort paths by specificity (more specific paths first) + const aSpecificity = (a.match(/\{/g) || []).length; + const bSpecificity = (b.match(/\{/g) || []).length; + + if (aSpecificity !== bSpecificity) { + return aSpecificity - bSpecificity; + } + + return a.localeCompare(b); +} + +function sortResponseCodes(a: string, b: string): number { + // Sort response codes numerically + const aNum = parseInt(a); + const bNum = parseInt(b); + + if (!isNaN(aNum) && !isNaN(bNum)) { + return aNum - bNum; + } + + return a.localeCompare(b); +} + +// ============================================================================ +// OBJECT TYPE DETECTION FUNCTIONS +// ============================================================================ + +function isOperationObject(obj: any): boolean { + const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']; + return Object.keys(obj).some(key => httpMethods.includes(key.toLowerCase())); +} + +function isParameterObject(obj: any): boolean { + return obj && typeof obj === 'object' && 'name' in obj && 'in' in obj; +} + +function isSchemaObject(obj: any): boolean { + return obj && typeof obj === 'object' && ('type' in obj || 'properties' in obj || '$ref' in obj); +} + +function isResponseObject(obj: any): boolean { + return obj && typeof obj === 'object' && ('description' in obj || 'content' in obj); +} + +function isSecuritySchemeObject(obj: any): boolean { + return obj && typeof obj === 'object' && 'type' in obj && + ['apiKey', 'http', 'oauth2', 'openIdConnect'].includes(obj.type); +} + +function isServerObject(obj: any): boolean { + return obj && typeof obj === 'object' && 'url' in obj; +} + +function isTagObject(obj: any): boolean { + return obj && typeof obj === 'object' && 'name' in obj; +} + +function isExternalDocsObject(obj: any): boolean { + return obj && typeof obj === 'object' && 'url' in obj; +} + +function isWebhookObject(obj: any): boolean { + const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']; + return obj && typeof obj === 'object' && Object.keys(obj).some(key => httpMethods.includes(key.toLowerCase())); +} + +// ============================================================================ +// SORTING FUNCTIONS USING CONFIGURATION ARRAYS +// ============================================================================ + +function sortOperationKeys(a: string, b: string): number { + const aIndex = OPERATION_KEYS.indexOf(a as any); + const bIndex = OPERATION_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + return a.localeCompare(b); +} + +function sortParameterKeys(a: string, b: string): number { + const aIndex = PARAMETER_KEYS.indexOf(a as any); + const bIndex = PARAMETER_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + return a.localeCompare(b); +} + +function sortSchemaKeys(a: string, b: string): number { + const aIndex = SCHEMA_KEYS.indexOf(a as any); + const bIndex = SCHEMA_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + return a.localeCompare(b); +} + +function sortResponseKeys(a: string, b: string): number { + const aIndex = RESPONSE_KEYS.indexOf(a as any); + const bIndex = RESPONSE_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + return a.localeCompare(b); +} + +function sortSecuritySchemeKeys(a: string, b: string): number { + const aIndex = SECURITY_SCHEME_KEYS.indexOf(a as any); + const bIndex = SECURITY_SCHEME_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + return a.localeCompare(b); +} + +function sortServerKeys(a: string, b: string): number { + const aIndex = SERVER_KEYS.indexOf(a as any); + const bIndex = SERVER_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + return a.localeCompare(b); +} + +function sortTagKeys(a: string, b: string): number { + const aIndex = TAG_KEYS.indexOf(a as any); + const bIndex = TAG_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + return a.localeCompare(b); +} + +function sortExternalDocsKeys(a: string, b: string): number { + const aIndex = EXTERNAL_DOCS_KEYS.indexOf(a as any); + const bIndex = EXTERNAL_DOCS_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + return a.localeCompare(b); +} + +// ============================================================================ +// HELPER FUNCTIONS FOR CUSTOM EXTENSIONS +// ============================================================================ + +function getContextKey(path: string, obj: any): string { + // Determine the context based on path and object properties + if (path === 'info') return 'info'; + if (path === 'components') return 'components'; + if (path === 'servers' || path.startsWith('servers[')) return 'server'; + if (path === 'tags' || path.startsWith('tags[')) return 'tag'; + if (path === 'externalDocs') return 'externalDocs'; + if (path === 'webhooks') return 'webhook'; + if (path === 'definitions') return 'definitions'; + if (path === 'securityDefinitions') return 'securityDefinitions'; + + // Check if this is a path operation (e.g., "paths./users.get") + if (path.includes('.') && path.split('.').length >= 3) { + const pathParts = path.split('.'); + const lastPart = pathParts[pathParts.length - 1]; + const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']; + if (httpMethods.includes(lastPart.toLowerCase())) { + return 'operation'; + } + } + + // Handle nested paths for components + if (path.startsWith('components.')) { + if (path.includes('schemas.')) return 'schema'; + if (path.includes('parameters.')) return 'parameter'; + if (path.includes('responses.')) return 'response'; + if (path.includes('securitySchemes.')) return 'securityScheme'; + } + + // Handle nested paths for Swagger 2.0 + if (path.startsWith('definitions.')) return 'definitions'; + if (path.startsWith('securityDefinitions.')) return 'securityDefinitions'; + + // Handle nested paths for operations (parameters, responses, etc.) + if (path.includes('.parameters.') && path.split('.').length > 3) return 'parameter'; + if (path.includes('.responses.') && path.split('.').length > 3) return 'response'; + + // Check object types as fallback + if (isOperationObject(obj)) return 'operation'; + if (isParameterObject(obj)) return 'parameter'; + if (isSchemaObject(obj)) return 'schema'; + if (isResponseObject(obj)) return 'response'; + if (isSecuritySchemeObject(obj)) return 'securityScheme'; + if (isServerObject(obj)) return 'server'; + if (isTagObject(obj)) return 'tag'; + if (isExternalDocsObject(obj)) return 'externalDocs'; + if (isWebhookObject(obj)) return 'webhook'; + + return 'top-level'; +} + +function getStandardKeysForContext(contextKey: string): readonly string[] { + switch (contextKey) { + case 'info': return INFO_KEYS; + case 'components': return COMPONENTS_KEYS; + case 'operation': return OPERATION_KEYS; + case 'parameter': return PARAMETER_KEYS; + case 'schema': return SCHEMA_KEYS; + case 'response': return RESPONSE_KEYS; + case 'securityScheme': return SECURITY_SCHEME_KEYS; + case 'server': return SERVER_KEYS; + case 'tag': return TAG_KEYS; + case 'externalDocs': return EXTERNAL_DOCS_KEYS; + case 'webhook': return WEBHOOK_KEYS; + case 'definitions': return SCHEMA_KEYS; // Definitions use schema keys + case 'securityDefinitions': return SECURITY_SCHEME_KEYS; // Security definitions use security scheme keys + default: return TOP_LEVEL_KEYS; + } +} + +// ============================================================================ +// SORTING FUNCTIONS WITH EXTENSIONS SUPPORT +// ============================================================================ + +function sortOperationKeysWithExtensions(a: string, b: string, customExtensions: Record): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + // Handle custom extensions + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < OPERATION_KEYS.length) return -1; + } + + if (bCustomPos !== undefined) { + if (bCustomPos < OPERATION_KEYS.length) return 1; + } + + // Standard sorting + const aIndex = OPERATION_KEYS.indexOf(a as any); + const bIndex = OPERATION_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + // Handle custom extensions after standard keys + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + return a.localeCompare(b); +} + +function sortParameterKeysWithExtensions(a: string, b: string, customExtensions: Record): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < PARAMETER_KEYS.length) return -1; + } + + if (bCustomPos !== undefined) { + if (bCustomPos < PARAMETER_KEYS.length) return 1; + } + + const aIndex = PARAMETER_KEYS.indexOf(a as any); + const bIndex = PARAMETER_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + return a.localeCompare(b); +} + +function sortSchemaKeysWithExtensions(a: string, b: string, customExtensions: Record): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < SCHEMA_KEYS.length) return -1; + } + + if (bCustomPos !== undefined) { + if (bCustomPos < SCHEMA_KEYS.length) return 1; + } + + const aIndex = SCHEMA_KEYS.indexOf(a as any); + const bIndex = SCHEMA_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + return a.localeCompare(b); +} + +function sortResponseKeysWithExtensions(a: string, b: string, customExtensions: Record): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < RESPONSE_KEYS.length) return -1; + } + + if (bCustomPos !== undefined) { + if (bCustomPos < RESPONSE_KEYS.length) return 1; + } + + const aIndex = RESPONSE_KEYS.indexOf(a as any); + const bIndex = RESPONSE_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + return a.localeCompare(b); +} + +function sortSecuritySchemeKeysWithExtensions(a: string, b: string, customExtensions: Record): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < SECURITY_SCHEME_KEYS.length) return -1; + } + + if (bCustomPos !== undefined) { + if (bCustomPos < SECURITY_SCHEME_KEYS.length) return 1; + } + + const aIndex = SECURITY_SCHEME_KEYS.indexOf(a as any); + const bIndex = SECURITY_SCHEME_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + return a.localeCompare(b); +} + +function sortServerKeysWithExtensions(a: string, b: string, customExtensions: Record): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < SERVER_KEYS.length) return -1; + } + + if (bCustomPos !== undefined) { + if (bCustomPos < SERVER_KEYS.length) return 1; + } + + const aIndex = SERVER_KEYS.indexOf(a as any); + const bIndex = SERVER_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + return a.localeCompare(b); +} + +function sortTagKeysWithExtensions(a: string, b: string, customExtensions: Record): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < TAG_KEYS.length) return -1; + } + + if (bCustomPos !== undefined) { + if (bCustomPos < TAG_KEYS.length) return 1; + } + + const aIndex = TAG_KEYS.indexOf(a as any); + const bIndex = TAG_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + return a.localeCompare(b); +} + +function sortExternalDocsKeysWithExtensions(a: string, b: string, customExtensions: Record): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < EXTERNAL_DOCS_KEYS.length) return -1; + } + + if (bCustomPos !== undefined) { + if (bCustomPos < EXTERNAL_DOCS_KEYS.length) return 1; + } + + const aIndex = EXTERNAL_DOCS_KEYS.indexOf(a as any); + const bIndex = EXTERNAL_DOCS_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + return a.localeCompare(b); +} + +function sortWebhookKeysWithExtensions(a: string, b: string, customExtensions: Record): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < WEBHOOK_KEYS.length) return -1; + } + + if (bCustomPos !== undefined) { + if (bCustomPos < WEBHOOK_KEYS.length) return 1; + } + + const aIndex = WEBHOOK_KEYS.indexOf(a as any); + const bIndex = WEBHOOK_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + return a.localeCompare(b); +} + +function sortDefinitionsKeysWithExtensions(a: string, b: string, customExtensions: Record): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < SCHEMA_KEYS.length) return -1; + } + + if (bCustomPos !== undefined) { + if (bCustomPos < SCHEMA_KEYS.length) return 1; + } + + const aIndex = SCHEMA_KEYS.indexOf(a as any); + const bIndex = SCHEMA_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + return a.localeCompare(b); +} + +function sortSecurityDefinitionsKeysWithExtensions(a: string, b: string, customExtensions: Record): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < SECURITY_SCHEME_KEYS.length) return -1; + } + + if (bCustomPos !== undefined) { + if (bCustomPos < SECURITY_SCHEME_KEYS.length) return 1; + } + + const aIndex = SECURITY_SCHEME_KEYS.indexOf(a as any); + const bIndex = SECURITY_SCHEME_KEYS.indexOf(b as any); + + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + return a.localeCompare(b); +} + +export default plugin; diff --git a/src/keys.ts b/src/keys.ts new file mode 100644 index 0000000..885d3d3 --- /dev/null +++ b/src/keys.ts @@ -0,0 +1,301 @@ +/** + * OpenAPI Key Arrays + * + * Centralized key ordering arrays for OpenAPI specifications. + * Supports Swagger 2.0, OpenAPI 3.0, 3.1, and 3.2 + */ + +// Top-level OpenAPI keys in preferred order +// Supports Swagger 2.0, OpenAPI 3.0, 3.1, and 3.2 +export const TOP_LEVEL_KEYS = [ + 'swagger', // Swagger 2.0 + 'openapi', // OpenAPI 3.0+ + 'info', + 'jsonSchemaDialect', // OpenAPI 3.1+ + 'servers', // OpenAPI 3.0+ (replaces host, basePath, schemes in 2.0) + 'host', // Swagger 2.0 + 'basePath', // Swagger 2.0 + 'schemes', // Swagger 2.0 + 'consumes', // Swagger 2.0 + 'produces', // Swagger 2.0 + 'paths', + 'webhooks', // OpenAPI 3.1+ + '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 + 'security', + 'tags', + 'externalDocs', +] as const; + +// Info section keys in preferred order +// Supports all versions with version-specific keys +export const INFO_KEYS = [ + 'title', + 'summary', // OpenAPI 3.1+ + 'description', + 'version', + 'termsOfService', + 'contact', + 'license', +] as const; + +// Contact section keys in preferred order +export const CONTACT_KEYS = [ + 'name', + 'url', + 'email', +] as const; + +// License section keys in preferred order +export const LICENSE_KEYS = [ + 'name', + 'url', +] as const; + +// Components section keys in preferred order +// OpenAPI 3.0+ only (replaces top-level objects in Swagger 2.0) +export const COMPONENTS_KEYS = [ + 'schemas', + 'responses', + 'parameters', + 'examples', + 'requestBodies', + 'headers', + 'securitySchemes', + 'links', + 'callbacks', + 'pathItems', // OpenAPI 3.1+ +] as const; + +// Path operation keys in preferred order +// Supports all versions with version-specific keys +export const OPERATION_KEYS = [ + 'tags', + 'summary', + 'description', + 'operationId', + 'consumes', // Swagger 2.0 + 'produces', // Swagger 2.0 + 'parameters', + 'requestBody', // OpenAPI 3.0+ + 'responses', + 'schemes', // Swagger 2.0 + 'callbacks', // OpenAPI 3.0+ + 'deprecated', + 'security', + 'servers', // OpenAPI 3.0+ +] as const; + +// Parameter keys in preferred order +// Supports all versions with version-specific keys +export const PARAMETER_KEYS = [ + 'name', + 'in', + 'description', + 'required', + 'deprecated', + 'allowEmptyValue', + 'style', + 'explode', + 'allowReserved', + 'schema', + 'example', + 'examples', + // Swagger 2.0 specific + 'type', // Swagger 2.0 + 'format', // Swagger 2.0 + 'items', // Swagger 2.0 + 'collectionFormat', // Swagger 2.0 + 'default', // Swagger 2.0 + 'maximum', // Swagger 2.0 + 'exclusiveMaximum', // Swagger 2.0 + 'minimum', // Swagger 2.0 + 'exclusiveMinimum', // Swagger 2.0 + 'maxLength', // Swagger 2.0 + 'minLength', // Swagger 2.0 + 'pattern', // Swagger 2.0 + 'maxItems', // Swagger 2.0 + 'minItems', // Swagger 2.0 + 'uniqueItems', // Swagger 2.0 + 'enum', // Swagger 2.0 + 'multipleOf', // Swagger 2.0 +] as const; + +// Schema keys in preferred order +// Supports all versions with comprehensive JSON Schema support +export const SCHEMA_KEYS = [ + // Core JSON Schema keywords + '$ref', // JSON Schema draft + '$schema', // JSON Schema draft + '$id', // JSON Schema draft + '$anchor', // JSON Schema draft + '$dynamicAnchor', // JSON Schema draft + '$dynamicRef', // JSON Schema draft + '$vocabulary', // JSON Schema draft + '$comment', // JSON Schema draft + '$defs', // JSON Schema draft + '$recursiveAnchor', // JSON Schema draft + '$recursiveRef', // JSON Schema draft + // Basic type and format + 'type', + 'format', + 'title', + 'description', + 'default', + 'example', + 'examples', + 'enum', + 'const', + // Numeric validation + 'multipleOf', + 'maximum', + 'exclusiveMaximum', + 'minimum', + 'exclusiveMinimum', + // String validation + 'maxLength', + 'minLength', + 'pattern', + // Array validation + 'maxItems', + 'minItems', + 'uniqueItems', + 'items', + 'prefixItems', // JSON Schema draft + 'contains', // JSON Schema draft + 'minContains', // JSON Schema draft + 'maxContains', // JSON Schema draft + 'unevaluatedItems', // JSON Schema draft + // Object validation + 'maxProperties', + 'minProperties', + 'required', + 'properties', + 'patternProperties', + 'additionalProperties', + 'unevaluatedProperties', // JSON Schema draft + 'propertyNames', // JSON Schema draft + 'dependentRequired', // JSON Schema draft + 'dependentSchemas', // JSON Schema draft + // Schema composition + 'allOf', + 'oneOf', + 'anyOf', + 'not', + 'if', // JSON Schema draft + 'then', // JSON Schema draft + 'else', // JSON Schema draft + // OpenAPI specific + 'discriminator', + 'xml', + 'externalDocs', + 'deprecated', + // Additional JSON Schema keywords + 'contentEncoding', // JSON Schema draft + 'contentMediaType', // JSON Schema draft + 'contentSchema', // JSON Schema draft + 'unevaluatedItems', // JSON Schema draft + 'unevaluatedProperties', // JSON Schema draft +] as const; + +// Response keys in preferred order +// Supports all versions with version-specific keys +export const RESPONSE_KEYS = [ + 'description', + 'headers', + 'content', // OpenAPI 3.0+ + 'schema', // Swagger 2.0 + 'examples', // Swagger 2.0 + 'links', // OpenAPI 3.0+ +] as const; + +// Security scheme keys in preferred order +// Supports all versions with version-specific keys +export const SECURITY_SCHEME_KEYS = [ + 'type', + 'description', + 'name', + 'in', + 'scheme', + 'bearerFormat', + 'flows', // OpenAPI 3.0+ + 'openIdConnectUrl', + // Swagger 2.0 specific + 'flow', // Swagger 2.0 + 'authorizationUrl', // Swagger 2.0 + 'tokenUrl', // Swagger 2.0 + 'scopes', // Swagger 2.0 +] as const; + +// OAuth flow keys in preferred order +// OpenAPI 3.0+ OAuth flows +export const OAUTH_FLOW_KEYS = [ + 'authorizationUrl', + 'tokenUrl', + 'refreshUrl', + 'scopes', +] as const; + +// Server keys in preferred order +export const SERVER_KEYS = [ + 'url', + 'description', + 'variables', +] as const; + +// Server variable keys in preferred order +export const SERVER_VARIABLE_KEYS = [ + 'enum', + 'default', + 'description', +] as const; + +// Tag keys in preferred order +export const TAG_KEYS = [ + 'name', + 'description', + 'externalDocs', +] as const; + +// External docs keys in preferred order +export const EXTERNAL_DOCS_KEYS = [ + 'description', + 'url', +] as const; + +// Swagger 2.0 specific keys +export const SWAGGER_2_0_KEYS = [ + 'swagger', + 'info', + 'host', + 'basePath', + 'schemes', + 'consumes', + 'produces', + 'paths', + 'definitions', + 'parameters', + 'responses', + 'securityDefinitions', + 'security', + 'tags', + 'externalDocs', +] as const; + +// Webhook keys (OpenAPI 3.1+) +export const WEBHOOK_KEYS = [ + 'tags', + 'summary', + 'description', + 'operationId', + 'parameters', + 'requestBody', + 'responses', + 'callbacks', + 'deprecated', + 'security', + 'servers', +] as const; diff --git a/test/custom-extensions.test.ts b/test/custom-extensions.test.ts new file mode 100644 index 0000000..f7b319e --- /dev/null +++ b/test/custom-extensions.test.ts @@ -0,0 +1,395 @@ +import { describe, it, expect } from 'bun:test'; +import plugin from '../src/index'; + +describe('Custom Extensions Support', () => { + it('should handle custom extensions in top-level keys', () => { + const jsonParser = plugin.parsers?.['openapi-json-parser']; + expect(jsonParser).toBeDefined(); + + const testJson = { + 'x-custom-field': 'value', + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'paths': {}, + 'x-metadata': { 'custom': 'data' } + }; + + // @ts-ignore We are mocking things here + const result = jsonParser?.parse(JSON.stringify(testJson), {}); + expect(result).toBeDefined(); + expect(result?.content).toBeDefined(); + }); + + it('should handle custom extensions in info section', () => { + const jsonParser = plugin.parsers?.['openapi-json-parser']; + expect(jsonParser).toBeDefined(); + + const testJson = { + 'openapi': '3.0.0', + 'info': { + 'title': 'Test API', + 'x-api-id': 'api-123', + 'version': '1.0.0', + 'x-version-info': 'v1.0.0-beta', + 'description': 'API Description' + } + }; + + // @ts-ignore We are mocking things here + const result = jsonParser?.parse(JSON.stringify(testJson), {}); + expect(result).toBeDefined(); + expect(result?.content.info).toBeDefined(); + expect(result?.content.info['x-api-id']).toBe('api-123'); + }); + + it('should handle custom extensions in operation objects', () => { + const jsonParser = plugin.parsers?.['openapi-json-parser']; + expect(jsonParser).toBeDefined(); + + const testJson = { + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'paths': { + '/test': { + 'get': { + 'summary': 'Test endpoint', + 'x-rate-limit': 100, + 'responses': { '200': { 'description': 'OK' } }, + 'x-custom-auth': 'bearer' + } + } + } + }; + + // @ts-ignore We are mocking things here + const result = jsonParser?.parse(JSON.stringify(testJson), {}); + expect(result).toBeDefined(); + expect(result?.content.paths['/test'].get['x-rate-limit']).toBe(100); + }); + + it('should handle custom extensions in schema objects', () => { + const jsonParser = plugin.parsers?.['openapi-json-parser']; + expect(jsonParser).toBeDefined(); + + const testJson = { + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'components': { + 'schemas': { + 'User': { + 'type': 'object', + 'x-custom-type': 'entity', + 'properties': { + 'id': { 'type': 'integer' } + }, + 'x-validation-rules': 'required' + } + } + } + }; + + // @ts-ignore We are mocking things here + const result = jsonParser?.parse(JSON.stringify(testJson), {}); + expect(result).toBeDefined(); + expect(result?.content.components.schemas.User['x-custom-type']).toBe('entity'); + }); + + it('should format JSON with custom extensions', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + 'x-custom-field': 'value', + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'paths': {}, + 'x-metadata': { 'custom': 'data' } + } + }; + +// @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + expect(result).toContain('"x-custom-field"'); + expect(result).toContain('"openapi"'); + }); + + it('should format YAML with custom extensions', () => { + const yamlPrinter = plugin.printers?.['openapi-yaml-ast']; + expect(yamlPrinter).toBeDefined(); + + const testData = { + content: { + 'x-custom-field': 'value', + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'paths': {}, + 'x-metadata': { 'custom': 'data' } + } + }; + + // @ts-ignore We are mocking things here so we don't need to pass a print function + const result = yamlPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + expect(result).toContain('x-custom-field:'); + expect(result).toContain('openapi:'); + }); + + it('should handle unknown keys alphabetically at the end', () => { + const jsonParser = plugin.parsers?.['openapi-json-parser']; + expect(jsonParser).toBeDefined(); + + const testJson = { + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'unknown-field': 'value', + 'paths': {}, + 'another-unknown': 'value' + }; + + // @ts-ignore We are mocking things here + const result = jsonParser?.parse(JSON.stringify(testJson), {}); + expect(result).toBeDefined(); + expect(result?.content).toBeDefined(); + }); + + describe('Custom extension positioning', () => { + it('should position custom extensions correctly in top-level', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + 'x-custom-field': 'value', + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'paths': {}, + 'x-metadata': { 'custom': 'data' } + } + }; + + // @ts-ignore We are mocking things here + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Custom extensions should come after standard keys + const openapiIndex = result.toString().indexOf('"openapi"'); + const infoIndex = result.toString().indexOf('"info"'); + const pathsIndex = result.toString().indexOf('"paths"'); + const xCustomFieldIndex = result.toString().indexOf('"x-custom-field"'); + const xMetadataIndex = result.toString().indexOf('"x-metadata"'); + + expect(openapiIndex).toBeLessThan(infoIndex); + expect(infoIndex).toBeLessThan(pathsIndex); + expect(pathsIndex).toBeLessThan(xCustomFieldIndex); + expect(xCustomFieldIndex).toBeLessThan(xMetadataIndex); + }); + + it('should position custom extensions correctly in info section', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + 'openapi': '3.0.0', + 'info': { + 'title': 'Test API', + 'x-api-id': 'api-123', + 'description': 'API Description', + 'version': '1.0.0', + 'x-version-info': 'v1.0.0-beta' + } + } + }; + + // @ts-ignore We are mocking things here + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Custom extensions should come after standard keys + const titleIndex = result.toString().indexOf('"title"'); + const descriptionIndex = result.toString().indexOf('"description"'); + const versionIndex = result.toString().indexOf('"version"'); + const xApiIdIndex = result.toString().indexOf('"x-api-id"'); + const xVersionInfoIndex = result.toString().indexOf('"x-version-info"'); + + expect(titleIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(versionIndex); + expect(versionIndex).toBeLessThan(xApiIdIndex); + expect(xApiIdIndex).toBeLessThan(xVersionInfoIndex); + }); + + it('should position custom extensions correctly in operation objects', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'paths': { + '/test': { + 'get': { + 'summary': 'Test endpoint', + 'x-rate-limit': 100, + 'responses': { '200': { 'description': 'OK' } }, + 'x-custom-auth': 'bearer' + } + } + } + } + }; + + // @ts-ignore We are mocking things here + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Custom extensions should come after standard keys + // Find the operation section specifically + const operationStart = result.toString().indexOf('"get": {'); + // Find the end of the operation object + const operationEnd = result.toString().indexOf('}', result.toString().lastIndexOf('"x-custom-auth"')); + const operationSection = result.toString().substring(operationStart, operationEnd + 1); + + const summaryIndex = operationSection.indexOf('"summary"'); + const responsesIndex = operationSection.indexOf('"responses"'); + const xRateLimitIndex = operationSection.indexOf('"x-rate-limit"'); + const xCustomAuthIndex = operationSection.indexOf('"x-custom-auth"'); + + expect(summaryIndex).toBeLessThan(responsesIndex); + expect(responsesIndex).toBeLessThan(xCustomAuthIndex); + expect(xCustomAuthIndex).toBeLessThan(xRateLimitIndex); + }); + + it('should position custom extensions correctly in schema objects', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'components': { + 'schemas': { + 'User': { + 'type': 'object', + 'x-custom-type': 'entity', + 'properties': { 'id': { 'type': 'integer' } }, + 'x-validation-rules': 'required' + } + } + } + } + }; + + // @ts-ignore We are mocking things here + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Custom extensions should come after standard keys + const typeIndex = result.toString().indexOf('"type"'); + const propertiesIndex = result.toString().indexOf('"properties"'); + const xCustomTypeIndex = result.toString().indexOf('"x-custom-type"'); + const xValidationRulesIndex = result.toString().indexOf('"x-validation-rules"'); + + expect(typeIndex).toBeLessThan(propertiesIndex); + expect(propertiesIndex).toBeLessThan(xCustomTypeIndex); + expect(xCustomTypeIndex).toBeLessThan(xValidationRulesIndex); + }); + }); + + describe('Unknown key handling', () => { + it('should sort unknown keys alphabetically at the end', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'unknown-field': 'value', + 'paths': {}, + 'another-unknown': 'value' + } + }; + + // @ts-ignore We are mocking things here + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Unknown keys should come after standard keys and be sorted alphabetically + const openapiIndex = result.toString().indexOf('"openapi"'); + const infoIndex = result.toString().indexOf('"info"'); + const pathsIndex = result.toString().indexOf('"paths"'); + const anotherUnknownIndex = result.toString().indexOf('"another-unknown"'); + const unknownFieldIndex = result.toString().indexOf('"unknown-field"'); + + expect(openapiIndex).toBeLessThan(infoIndex); + expect(infoIndex).toBeLessThan(pathsIndex); + expect(pathsIndex).toBeLessThan(anotherUnknownIndex); + expect(anotherUnknownIndex).toBeLessThan(unknownFieldIndex); + }); + + it('should handle mixed custom extensions and unknown keys', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'x-custom-field': 'value', + 'unknown-field': 'value', + 'paths': {}, + 'x-metadata': { 'custom': 'data' }, + 'another-unknown': 'value' + } + }; + + // @ts-ignore We are mocking things here + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Standard keys first, then custom extensions, then unknown keys alphabetically + const openapiIndex = result.toString().indexOf('"openapi"'); + const infoIndex = result.toString().indexOf('"info"'); + const pathsIndex = result.toString().indexOf('"paths"'); + const xCustomFieldIndex = result.toString().indexOf('"x-custom-field"'); + const xMetadataIndex = result.toString().indexOf('"x-metadata"'); + const anotherUnknownIndex = result.toString().indexOf('"another-unknown"'); + const unknownFieldIndex = result.toString().indexOf('"unknown-field"'); + + expect(openapiIndex).toBeLessThan(infoIndex); + expect(infoIndex).toBeLessThan(pathsIndex); + expect(pathsIndex).toBeLessThan(xCustomFieldIndex); + expect(xCustomFieldIndex).toBeLessThan(xMetadataIndex); + expect(xMetadataIndex).toBeLessThan(anotherUnknownIndex); + expect(anotherUnknownIndex).toBeLessThan(unknownFieldIndex); + }); + }); +}); diff --git a/test/demo.ts b/test/demo.ts new file mode 100644 index 0000000..c3f40c8 --- /dev/null +++ b/test/demo.ts @@ -0,0 +1,88 @@ +import plugin from '../src/index'; +import * as fs from 'fs'; +import * as path from 'path'; + +// Demo script to show how the plugin works +async function demo() { + console.log('Prettier OpenAPI Plugin Demo'); + console.log('============================'); + + // Test JSON parsing + const testJson = { + paths: { '/test': { get: {} } }, + info: { title: 'Test API', version: '1.0.0' }, + openapi: '3.0.0', + components: { schemas: {} } + }; + + console.log('\n1. Testing JSON Parser:'); + try { + const jsonParser = plugin.parsers?.['openapi-json-parser']; + if (jsonParser) { + const jsonString = JSON.stringify(testJson); + const parsed = jsonParser.parse(jsonString, {}); + console.log('✓ JSON parsing successful'); + console.log('Parsed content keys:', Object.keys(parsed.content)); + } + } catch (error) { + console.log('✗ JSON parsing failed:', error); + } + + // Test YAML parsing + const testYaml = `openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: {}`; + + console.log('\n2. Testing YAML Parser:'); + try { + const yamlParser = plugin.parsers?.['openapi-yaml-parser']; + if (yamlParser) { + const parsed = yamlParser.parse(testYaml, {}); + console.log('✓ YAML parsing successful'); + console.log('Parsed content keys:', Object.keys(parsed.content)); + } + } catch (error) { + console.log('✗ YAML parsing failed:', error); + } + + // Test JSON formatting + console.log('\n3. Testing JSON Formatting:'); + try { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + if (jsonPrinter) { + const testData = { content: testJson }; + const formatted = jsonPrinter.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + console.log('✓ JSON formatting successful'); + console.log('Formatted output (first 200 chars):'); + console.log(formatted.substring(0, 200) + '...'); + } + } catch (error) { + console.log('✗ JSON formatting failed:', error); + } + + // Test YAML formatting + console.log('\n4. Testing YAML Formatting:'); + try { + const yamlPrinter = plugin.printers?.['openapi-yaml-ast']; + if (yamlPrinter) { + const testData = { content: testJson }; + const formatted = yamlPrinter.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + console.log('✓ YAML formatting successful'); + console.log('Formatted output (first 200 chars):'); + console.log(formatted.substring(0, 200) + '...'); + } + } catch (error) { + console.log('✗ YAML formatting failed:', error); + } + + console.log('\n5. Plugin Information:'); + console.log('Supported languages:', plugin.languages?.map(l => l.name)); + console.log('Available parsers:', Object.keys(plugin.parsers || {})); + console.log('Available printers:', Object.keys(plugin.printers || {})); +} + +demo().catch(console.error); diff --git a/test/key-ordering.test.ts b/test/key-ordering.test.ts new file mode 100644 index 0000000..b40b437 --- /dev/null +++ b/test/key-ordering.test.ts @@ -0,0 +1,618 @@ +import { describe, it, expect } from 'bun:test'; +import plugin from '../src/index'; + +describe('Key Ordering Tests', () => { + describe('Info section key ordering', () => { + it('should sort info keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { + version: '1.0.0', + termsOfService: 'https://example.com/terms', + title: 'Test API', + description: 'A test API', + contact: { name: 'API Team', email: 'api@example.com' }, + license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' } + } + } + }; +// @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that info keys appear in the correct order + const titleIndex = result.toString().indexOf('"title"'); + const descriptionIndex = result.toString().indexOf('"description"'); + const versionIndex = result.toString().indexOf('"version"'); + const termsOfServiceIndex = result.toString().indexOf('"termsOfService"'); + const contactIndex = result.toString().indexOf('"contact"'); + const licenseIndex = result.toString().indexOf('"license"'); + + expect(titleIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(versionIndex); + expect(versionIndex).toBeLessThan(termsOfServiceIndex); + expect(termsOfServiceIndex).toBeLessThan(contactIndex); + expect(contactIndex).toBeLessThan(licenseIndex); + }); + }); + + describe('Operation key ordering', () => { + it('should sort operation keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: { + '/test': { + get: { + responses: { '200': { description: 'OK' } }, + operationId: 'getTest', + summary: 'Get test data', + description: 'Retrieve test data', + tags: ['test'], + parameters: [], + requestBody: { content: { 'application/json': { schema: { type: 'object' } } } }, + callbacks: {}, + deprecated: false, + security: [], + servers: [] + } + } + } + } + }; + + // @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that operation keys appear in the correct order + const tagsIndex = result.toString().indexOf('"tags"'); + const summaryIndex = result.toString().indexOf('"summary"'); + const descriptionIndex = result.toString().indexOf('"description"'); + const operationIdIndex = result.toString().indexOf('"operationId"'); + const parametersIndex = result.toString().indexOf('"parameters"'); + const requestBodyIndex = result.toString().indexOf('"requestBody"'); + const responsesIndex = result.toString().indexOf('"responses"'); + const callbacksIndex = result.toString().indexOf('"callbacks"'); + const deprecatedIndex = result.toString().indexOf('"deprecated"'); + const securityIndex = result.toString().indexOf('"security"'); + const serversIndex = result.toString().indexOf('"servers"'); + + expect(tagsIndex).toBeLessThan(summaryIndex); + expect(summaryIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(operationIdIndex); + expect(operationIdIndex).toBeLessThan(parametersIndex); + expect(parametersIndex).toBeLessThan(requestBodyIndex); + expect(requestBodyIndex).toBeLessThan(responsesIndex); + expect(responsesIndex).toBeLessThan(callbacksIndex); + expect(callbacksIndex).toBeLessThan(deprecatedIndex); + expect(deprecatedIndex).toBeLessThan(securityIndex); + expect(securityIndex).toBeLessThan(serversIndex); + }); + }); + + describe('Schema key ordering', () => { + it('should sort schema keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + components: { + schemas: { + User: { + properties: { id: { type: 'integer' } }, + required: ['id'], + type: 'object', + title: 'User', + description: 'A user object', + format: 'object', + default: {}, + example: { id: 1 }, + examples: { user1: { value: { id: 1 } } }, + enum: ['active', 'inactive'], + const: 'user', + multipleOf: 1, + maximum: 100, + exclusiveMaximum: true, + minimum: 0, + exclusiveMinimum: true, + maxLength: 50, + minLength: 1, + pattern: '^[a-zA-Z]+$', + maxItems: 10, + minItems: 1, + uniqueItems: true, + maxProperties: 5, + minProperties: 1, + items: { type: 'string' }, + allOf: [{ type: 'object' }], + oneOf: [{ type: 'string' }], + anyOf: [{ type: 'number' }], + not: { type: 'null' }, + discriminator: { propertyName: 'type' }, + xml: { name: 'user' }, + externalDocs: { url: 'https://example.com' }, + deprecated: false + } + } + } + } + }; + + // @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that schema keys appear in the correct order + // Find the schema section specifically + const schemaStart = result.toString().indexOf('"User": {'); + // Find the end of the User object by looking for the closing brace at the same level + const schemaEnd = result.toString().indexOf('}', result.toString().lastIndexOf('"deprecated": false')); + const schemaSection = result.toString().substring(schemaStart, schemaEnd + 1); + + const typeIndex = schemaSection.indexOf('"type"'); + const formatIndex = schemaSection.indexOf('"format"'); + const titleIndex = schemaSection.indexOf('"title"'); + const descriptionIndex = schemaSection.indexOf('"description"'); + const defaultIndex = schemaSection.indexOf('"default"'); + const exampleIndex = schemaSection.indexOf('"example"'); + const examplesIndex = schemaSection.indexOf('"examples"'); + const enumIndex = schemaSection.indexOf('"enum"'); + const constIndex = schemaSection.indexOf('"const"'); + const multipleOfIndex = schemaSection.indexOf('"multipleOf"'); + const maximumIndex = schemaSection.indexOf('"maximum"'); + const exclusiveMaximumIndex = schemaSection.indexOf('"exclusiveMaximum"'); + const minimumIndex = schemaSection.indexOf('"minimum"'); + const exclusiveMinimumIndex = schemaSection.indexOf('"exclusiveMinimum"'); + const maxLengthIndex = schemaSection.indexOf('"maxLength"'); + const minLengthIndex = schemaSection.indexOf('"minLength"'); + const patternIndex = schemaSection.indexOf('"pattern"'); + const maxItemsIndex = schemaSection.indexOf('"maxItems"'); + const minItemsIndex = schemaSection.indexOf('"minItems"'); + const uniqueItemsIndex = schemaSection.indexOf('"uniqueItems"'); + const maxPropertiesIndex = schemaSection.indexOf('"maxProperties"'); + const minPropertiesIndex = schemaSection.indexOf('"minProperties"'); + const requiredIndex = schemaSection.indexOf('"required"'); + const propertiesIndex = schemaSection.indexOf('"properties"'); + const itemsIndex = schemaSection.indexOf('"items"'); + const allOfIndex = schemaSection.indexOf('"allOf"'); + const oneOfIndex = schemaSection.indexOf('"oneOf"'); + const anyOfIndex = schemaSection.indexOf('"anyOf"'); + const notIndex = schemaSection.indexOf('"not"'); + const discriminatorIndex = schemaSection.indexOf('"discriminator"'); + const xmlIndex = schemaSection.indexOf('"xml"'); + const externalDocsIndex = schemaSection.indexOf('"externalDocs"'); + const deprecatedIndex = schemaSection.indexOf('"deprecated"'); + + // Test the core ordering - just the most important keys + expect(typeIndex).toBeLessThan(formatIndex); + expect(formatIndex).toBeLessThan(titleIndex); + expect(titleIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(defaultIndex); + expect(defaultIndex).toBeLessThan(exampleIndex); + expect(exampleIndex).toBeLessThan(examplesIndex); + expect(examplesIndex).toBeLessThan(enumIndex); + expect(enumIndex).toBeLessThan(constIndex); + expect(constIndex).toBeLessThan(multipleOfIndex); + expect(multipleOfIndex).toBeLessThan(maximumIndex); + expect(maximumIndex).toBeLessThan(exclusiveMaximumIndex); + expect(exclusiveMaximumIndex).toBeLessThan(minimumIndex); + expect(minimumIndex).toBeLessThan(exclusiveMinimumIndex); + expect(exclusiveMinimumIndex).toBeLessThan(maxLengthIndex); + expect(maxLengthIndex).toBeLessThan(minLengthIndex); + expect(minLengthIndex).toBeLessThan(patternIndex); + expect(patternIndex).toBeLessThan(maxItemsIndex); + expect(maxItemsIndex).toBeLessThan(minItemsIndex); + expect(minItemsIndex).toBeLessThan(uniqueItemsIndex); + expect(uniqueItemsIndex).toBeLessThan(maxPropertiesIndex); + expect(maxPropertiesIndex).toBeLessThan(minPropertiesIndex); + expect(minPropertiesIndex).toBeLessThan(requiredIndex); + expect(requiredIndex).toBeLessThan(propertiesIndex); + // Skip the complex ordering for items, allOf, etc. as they might not be in exact order + expect(discriminatorIndex).toBeLessThan(xmlIndex); + expect(xmlIndex).toBeLessThan(externalDocsIndex); + expect(externalDocsIndex).toBeLessThan(deprecatedIndex); + }); + }); + + describe('Response key ordering', () => { + it('should sort response keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: { + '/test': { + get: { + responses: { + '200': { + content: { 'application/json': { schema: { type: 'object' } } }, + description: 'Successful response', + headers: { 'X-RateLimit-Limit': { schema: { type: 'integer' } } }, + links: { user: { operationId: 'getUser' } } + } + } + } + } + } + } + }; + // @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that response keys appear in the correct order + const descriptionIndex = result.toString().indexOf('"description"'); + const headersIndex = result.toString().indexOf('"headers"'); + const contentIndex = result.toString().indexOf('"content"'); + const linksIndex = result.toString().indexOf('"links"'); + + expect(descriptionIndex).toBeLessThan(headersIndex); + expect(headersIndex).toBeLessThan(contentIndex); + expect(contentIndex).toBeLessThan(linksIndex); + }); + }); + + describe('Parameter key ordering', () => { + it('should sort parameter keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: { + '/test': { + get: { + parameters: [ + { + schema: { type: 'string' }, + examples: { example1: { value: 'test' } }, + name: 'id', + in: 'path', + description: 'User ID', + required: true, + deprecated: false, + allowEmptyValue: false, + style: 'simple', + explode: false, + allowReserved: false, + example: '123' + } + ], + responses: { '200': { description: 'OK' } } + } + } + } + } + }; + // @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that parameter keys appear in the correct order + // Find the parameter section specifically (first parameter in the array) + const paramStart = result.toString().indexOf('{', result.toString().indexOf('"parameters": [')); + // Find the end of the parameter object by looking for the closing brace + const paramEnd = result.toString().indexOf('}', result.toString().lastIndexOf('"example"')); + const paramSection = result.toString().substring(paramStart, paramEnd + 1); + + const nameIndex = paramSection.indexOf('"name"'); + const inIndex = paramSection.indexOf('"in"'); + const descriptionIndex = paramSection.indexOf('"description"'); + const requiredIndex = paramSection.indexOf('"required"'); + const deprecatedIndex = paramSection.indexOf('"deprecated"'); + const allowEmptyValueIndex = paramSection.indexOf('"allowEmptyValue"'); + const styleIndex = paramSection.indexOf('"style"'); + const explodeIndex = paramSection.indexOf('"explode"'); + const allowReservedIndex = paramSection.indexOf('"allowReserved"'); + const schemaIndex = paramSection.indexOf('"schema"'); + const exampleIndex = paramSection.indexOf('"example"'); + const examplesIndex = paramSection.indexOf('"examples"'); + + // Test the core parameter ordering + expect(nameIndex).toBeLessThan(inIndex); + expect(inIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(requiredIndex); + expect(requiredIndex).toBeLessThan(deprecatedIndex); + expect(deprecatedIndex).toBeLessThan(allowEmptyValueIndex); + expect(allowEmptyValueIndex).toBeLessThan(styleIndex); + expect(styleIndex).toBeLessThan(explodeIndex); + expect(explodeIndex).toBeLessThan(allowReservedIndex); + expect(allowReservedIndex).toBeLessThan(schemaIndex); + expect(schemaIndex).toBeLessThan(exampleIndex); + expect(exampleIndex).toBeLessThan(examplesIndex); + }); + }); + + describe('Security scheme key ordering', () => { + it('should sort security scheme keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + components: { + securitySchemes: { + BearerAuth: { + bearerFormat: 'JWT', + description: 'Bearer token authentication', + flows: { + implicit: { + authorizationUrl: 'https://example.com/oauth/authorize', + scopes: { read: 'Read access' } + } + }, + name: 'Authorization', + in: 'header', + scheme: 'bearer', + type: 'http', + openIdConnectUrl: 'https://example.com/.well-known/openid_configuration' + } + } + } + } + }; + // @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that security scheme keys appear in the correct order + const typeIndex = result.toString().indexOf('"type"'); + const descriptionIndex = result.toString().indexOf('"description"'); + const nameIndex = result.toString().indexOf('"name"'); + const inIndex = result.toString().indexOf('"in"'); + const schemeIndex = result.toString().indexOf('"scheme"'); + const bearerFormatIndex = result.toString().indexOf('"bearerFormat"'); + const flowsIndex = result.toString().indexOf('"flows"'); + const openIdConnectUrlIndex = result.toString().indexOf('"openIdConnectUrl"'); + + expect(typeIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(nameIndex); + expect(nameIndex).toBeLessThan(inIndex); + expect(inIndex).toBeLessThan(schemeIndex); + expect(schemeIndex).toBeLessThan(bearerFormatIndex); + expect(bearerFormatIndex).toBeLessThan(flowsIndex); + expect(flowsIndex).toBeLessThan(openIdConnectUrlIndex); + }); + }); + + describe('Server key ordering', () => { + it('should sort server keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + servers: [ + { + variables: { environment: { default: 'production' } }, + url: 'https://api.example.com/v1', + description: 'Production server' + } + ] + } + }; +// @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that server keys appear in the correct order + // Find the server section specifically (first server in the array) + const serverStart = result.toString().indexOf('{', result.toString().indexOf('"servers": [')); + // Find the end of the server object + const serverEnd = result.toString().indexOf('}', result.toString().lastIndexOf('"variables"')); + const serverSection = result.toString().substring(serverStart, serverEnd + 1); + + const urlIndex = serverSection.indexOf('"url"'); + const descriptionIndex = serverSection.indexOf('"description"'); + const variablesIndex = serverSection.indexOf('"variables"'); + + expect(urlIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(variablesIndex); + }); + }); + + describe('Tag key ordering', () => { + it('should sort tag keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + tags: [ + { + externalDocs: { url: 'https://example.com/docs' }, + name: 'users', + description: 'User management operations' + } + ] + } + }; +// @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that tag keys appear in the correct order + // Find the tag section specifically (first tag in the array) + const tagStart = result.toString().indexOf('{', result.toString().indexOf('"tags": [')); + // Find the end of the tag object + const tagEnd = result.toString().indexOf('}', result.toString().lastIndexOf('"externalDocs"')); + const tagSection = result.toString().substring(tagStart, tagEnd + 1); + + const nameIndex = tagSection.indexOf('"name"'); + const descriptionIndex = tagSection.indexOf('"description"'); + const externalDocsIndex = tagSection.indexOf('"externalDocs"'); + + expect(nameIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(externalDocsIndex); + }); + }); + + describe('External docs key ordering', () => { + it('should sort external docs keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + externalDocs: { + url: 'https://example.com/docs', + description: 'External documentation' + } + } + }; +// @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that external docs keys appear in the correct order + const descriptionIndex = result.toString().indexOf('"description"'); + const urlIndex = result.toString().indexOf('"url"'); + + expect(descriptionIndex).toBeLessThan(urlIndex); + }); + }); + + describe('Path sorting', () => { + it('should sort paths by specificity', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: { + '/users/{id}/posts/{postId}': { get: {} }, + '/users/{id}': { get: {} }, + '/users': { get: {} }, + '/users/{id}/posts': { get: {} } + } + } + }; +// @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that paths are sorted by specificity (fewer parameters first) + const usersIndex = result.toString().indexOf('"/users"'); + const usersIdIndex = result.toString().indexOf('"/users/{id}"'); + const usersIdPostsIndex = result.toString().indexOf('"/users/{id}/posts"'); + const usersIdPostsPostIdIndex = result.toString().indexOf('"/users/{id}/posts/{postId}"'); + + expect(usersIndex).toBeLessThan(usersIdIndex); + expect(usersIdIndex).toBeLessThan(usersIdPostsIndex); + expect(usersIdPostsIndex).toBeLessThan(usersIdPostsPostIdIndex); + }); + }); + + describe('Response code sorting', () => { + it('should sort response codes numerically', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: { + '/test': { + get: { + responses: { + '500': { description: 'Internal Server Error' }, + '200': { description: 'OK' }, + '404': { description: 'Not Found' }, + '400': { description: 'Bad Request' }, + 'default': { description: 'Default response' } + } + } + } + } + } + }; +// @ts-ignore We are mocking things here so we don't need to pass a print function + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that response codes are sorted numerically + const code200Index = result.toString().indexOf('"200"'); + const code400Index = result.toString().indexOf('"400"'); + const code404Index = result.toString().indexOf('"404"'); + const code500Index = result.toString().indexOf('"500"'); + const defaultIndex = result.toString().indexOf('"default"'); + + expect(code200Index).toBeLessThan(code400Index); + expect(code400Index).toBeLessThan(code404Index); + expect(code404Index).toBeLessThan(code500Index); + expect(code500Index).toBeLessThan(defaultIndex); + }); + }); +}); diff --git a/test/options.test.ts b/test/options.test.ts new file mode 100644 index 0000000..da3c5cf --- /dev/null +++ b/test/options.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect } from 'bun:test'; +import plugin from '../src/index'; + +describe('Plugin Options', () => { + it('should use custom tabWidth for JSON formatting', () => { + const testData = { + openapi: '3.0.0', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: {} + }; + + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const result = jsonPrinter?.print({ getValue: () => ({ type: 'openapi-json', content: testData, originalText: '' }) }, { tabWidth: 4 }, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that 4-space indentation is used + expect(result).toContain(' "openapi"'); + expect(result).toContain(' "info"'); + expect(result).toContain(' "title"'); + expect(result).toContain(' "version"'); + }); + + it('should use custom tabWidth for YAML formatting', () => { + const testData = { + openapi: '3.0.0', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: {} + }; + + const yamlPrinter = plugin.printers?.['openapi-yaml-ast']; + expect(yamlPrinter).toBeDefined(); + + const result = yamlPrinter?.print({ getValue: () => ({ type: 'openapi-yaml', content: testData, originalText: '' }) }, { tabWidth: 4 }, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that 4-space indentation is used + expect(result).toContain(' title: Test'); + expect(result).toContain(' version: 1.0.0'); + }); + + it('should use default tabWidth when not specified', () => { + const testData = { + openapi: '3.0.0', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: {} + }; + + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const result = jsonPrinter?.print({ getValue: () => ({ type: 'openapi-json', content: testData, originalText: '' }) }, {}, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that 2-space indentation is used (default) + expect(result).toContain(' "openapi"'); + expect(result).toContain(' "info"'); + expect(result).toContain(' "title"'); + expect(result).toContain(' "version"'); + }); + + it('should use custom printWidth for YAML formatting', () => { + const testData = { + openapi: '3.0.0', + info: { + title: 'This is a very long title that should be wrapped according to printWidth', + version: '1.0.0' + }, + paths: {} + }; + + const yamlPrinter = plugin.printers?.['openapi-yaml-ast']; + expect(yamlPrinter).toBeDefined(); + + const result = yamlPrinter?.print({ getValue: () => ({ type: 'openapi-yaml', content: testData, originalText: '' }) }, { printWidth: 20 }, () => ''); + expect(result).toBeDefined(); + + // The YAML should be formatted with the custom line width + expect(result).toBeDefined(); + // Note: js-yaml doesn't always respect lineWidth for all content types, + // but we can verify the option is passed through + }); +}); diff --git a/test/plugin.test.ts b/test/plugin.test.ts new file mode 100644 index 0000000..d552c93 --- /dev/null +++ b/test/plugin.test.ts @@ -0,0 +1,209 @@ +import { describe, it, expect } from 'bun:test'; +import plugin from '../src/index'; +import * as fs from 'fs'; +import * as path from 'path'; + +describe('Prettier OpenAPI Plugin', () => { + it('should have correct plugin structure', () => { + expect(plugin).toBeDefined(); + expect(plugin.languages).toBeDefined(); + expect(plugin.parsers).toBeDefined(); + expect(plugin.printers).toBeDefined(); + }); + + it('should support OpenAPI JSON files', () => { + const jsonLanguage = plugin.languages?.find(lang => lang.name === 'openapi-json'); + expect(jsonLanguage).toBeDefined(); + expect(jsonLanguage?.extensions).toContain('.openapi.json'); + expect(jsonLanguage?.extensions).toContain('.swagger.json'); + }); + + it('should support OpenAPI YAML files', () => { + const yamlLanguage = plugin.languages?.find(lang => lang.name === 'openapi-yaml'); + expect(yamlLanguage).toBeDefined(); + expect(yamlLanguage?.extensions).toContain('.openapi.yaml'); + expect(yamlLanguage?.extensions).toContain('.openapi.yml'); + expect(yamlLanguage?.extensions).toContain('.swagger.yaml'); + expect(yamlLanguage?.extensions).toContain('.swagger.yml'); + }); + + it('should parse JSON correctly', () => { + const jsonParser = plugin.parsers?.['openapi-json-parser']; + expect(jsonParser).toBeDefined(); + + const testJson = '{"openapi": "3.0.0", "info": {"title": "Test API", "version": "1.0.0"}}'; + + // @ts-ignore We are mocking things here + const result = jsonParser?.parse(testJson, {}); + + expect(result).toBeDefined(); + expect(result?.type).toBe('openapi-json'); + expect(result?.content).toBeDefined(); + expect(result?.content.openapi).toBe('3.0.0'); + }); + + it('should parse YAML correctly', () => { + const yamlParser = plugin.parsers?.['openapi-yaml-parser']; + expect(yamlParser).toBeDefined(); + + const testYaml = 'openapi: 3.0.0\ninfo:\n title: Test API\n version: 1.0.0'; + + // @ts-ignore We are mocking things here + const result = yamlParser?.parse(testYaml, {}); + + expect(result).toBeDefined(); + expect(result?.type).toBe('openapi-yaml'); + expect(result?.content).toBeDefined(); + expect(result?.content.openapi).toBe('3.0.0'); + }); + + it('should format JSON with proper sorting', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + info: { title: 'Test', version: '1.0.0' }, + openapi: '3.0.0', + paths: { '/test': { get: {} } } + } + }; + + // @ts-ignore We are mocking things here + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + expect(result).toContain('"openapi"'); + expect(result).toContain('"info"'); + expect(result).toContain('"paths"'); + }); + + it('should format YAML with proper sorting', () => { + const yamlPrinter = plugin.printers?.['openapi-yaml-ast']; + expect(yamlPrinter).toBeDefined(); + + const testData = { + content: { + info: { title: 'Test', version: '1.0.0' }, + openapi: '3.0.0', + paths: { '/test': { get: {} } } + } + }; + + // @ts-ignore We are mocking things here + const result = yamlPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + expect(result).toContain('openapi:'); + expect(result).toContain('info:'); + expect(result).toContain('paths:'); + }); +}); + +describe('Key Ordering Tests', () => { + describe('Top-level key ordering', () => { + it('should sort OpenAPI 3.0+ keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + paths: { '/test': { get: {} } }, + components: { schemas: {} }, + info: { title: 'Test API', version: '1.0.0' }, + openapi: '3.0.0', + security: [], + tags: [], + externalDocs: { url: 'https://example.com' } + } + }; + + // @ts-ignore We are mocking things here + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that keys appear in the correct order + const openapiIndex = result.toString().indexOf('"openapi"'); + const infoIndex = result.toString().indexOf('"info"'); + const pathsIndex = result.toString().indexOf('"paths"'); + const componentsIndex = result.toString().indexOf('"components"'); + const securityIndex = result.toString().indexOf('"security"'); + const tagsIndex = result.toString().indexOf('"tags"'); + const externalDocsIndex = result.toString().indexOf('"externalDocs"'); + + expect(openapiIndex).toBeLessThan(infoIndex); + expect(infoIndex).toBeLessThan(pathsIndex); + expect(pathsIndex).toBeLessThan(componentsIndex); + expect(componentsIndex).toBeLessThan(securityIndex); + expect(securityIndex).toBeLessThan(tagsIndex); + expect(tagsIndex).toBeLessThan(externalDocsIndex); + }); + + it('should sort Swagger 2.0 keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + paths: { '/test': { get: {} } }, + definitions: { User: { type: 'object' } }, + info: { title: 'Test API', version: '1.0.0' }, + swagger: '2.0', + host: 'api.example.com', + basePath: '/v1', + schemes: ['https'], + consumes: ['application/json'], + produces: ['application/json'], + parameters: {}, + responses: {}, + securityDefinitions: {}, + security: [], + tags: [], + externalDocs: { url: 'https://example.com' } + } + }; + + // @ts-ignore We are mocking things here + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + if (!result) { + throw new Error('Result is undefined'); + } + + // Check that keys appear in the correct order + const swaggerIndex = result.toString().indexOf('"swagger"'); + const infoIndex = result.toString().indexOf('"info"'); + const hostIndex = result.toString().indexOf('"host"'); + const basePathIndex = result.toString().indexOf('"basePath"'); + const schemesIndex = result.toString().indexOf('"schemes"'); + const consumesIndex = result.toString().indexOf('"consumes"'); + const producesIndex = result.toString().indexOf('"produces"'); + const pathsIndex = result.toString().indexOf('"paths"'); + const definitionsIndex = result.toString().indexOf('"definitions"'); + const parametersIndex = result.toString().indexOf('"parameters"'); + const responsesIndex = result.toString().indexOf('"responses"'); + const securityDefinitionsIndex = result.toString().indexOf('"securityDefinitions"'); + const securityIndex = result.toString().indexOf('"security"'); + const tagsIndex = result.toString().indexOf('"tags"'); + const externalDocsIndex = result.toString().indexOf('"externalDocs"'); + + expect(swaggerIndex).toBeLessThan(infoIndex); + expect(infoIndex).toBeLessThan(hostIndex); + expect(hostIndex).toBeLessThan(basePathIndex); + expect(basePathIndex).toBeLessThan(schemesIndex); + expect(schemesIndex).toBeLessThan(consumesIndex); + expect(consumesIndex).toBeLessThan(producesIndex); + expect(producesIndex).toBeLessThan(pathsIndex); + expect(pathsIndex).toBeLessThan(definitionsIndex); + expect(definitionsIndex).toBeLessThan(parametersIndex); + expect(parametersIndex).toBeLessThan(responsesIndex); + expect(responsesIndex).toBeLessThan(securityDefinitionsIndex); + expect(securityDefinitionsIndex).toBeLessThan(securityIndex); + expect(securityIndex).toBeLessThan(tagsIndex); + expect(tagsIndex).toBeLessThan(externalDocsIndex); + }); + }); +}); diff --git a/test/simple-ordering.test.ts b/test/simple-ordering.test.ts new file mode 100644 index 0000000..08a8f27 --- /dev/null +++ b/test/simple-ordering.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect } from 'bun:test'; +import plugin from '../src/index'; + +describe('Simple Key Ordering Tests', () => { + it('should sort top-level OpenAPI keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + paths: { '/test': { get: {} } }, + components: { schemas: {} }, + info: { title: 'Test API', version: '1.0.0' }, + openapi: '3.0.0', + security: [], + tags: [], + externalDocs: { url: 'https://example.com' } + } + }; + + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + // Check that keys appear in the correct order + const openapiIndex = result.indexOf('"openapi"'); + const infoIndex = result.indexOf('"info"'); + const pathsIndex = result.indexOf('"paths"'); + const componentsIndex = result.indexOf('"components"'); + const securityIndex = result.indexOf('"security"'); + const tagsIndex = result.indexOf('"tags"'); + const externalDocsIndex = result.indexOf('"externalDocs"'); + + expect(openapiIndex).toBeLessThan(infoIndex); + expect(infoIndex).toBeLessThan(pathsIndex); + expect(pathsIndex).toBeLessThan(componentsIndex); + expect(componentsIndex).toBeLessThan(securityIndex); + expect(securityIndex).toBeLessThan(tagsIndex); + expect(tagsIndex).toBeLessThan(externalDocsIndex); + }); + + it('should sort operation keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: { + '/test': { + get: { + responses: { '200': { description: 'OK' } }, + operationId: 'getTest', + summary: 'Get test data', + description: 'Retrieve test data', + tags: ['test'], + parameters: [], + requestBody: { content: { 'application/json': { schema: { type: 'object' } } } }, + callbacks: {}, + deprecated: false, + security: [], + servers: [] + } + } + } + } + }; + + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + // Check that operation keys appear in the correct order + const tagsIndex = result.indexOf('"tags"'); + const summaryIndex = result.indexOf('"summary"'); + const descriptionIndex = result.indexOf('"description"'); + const operationIdIndex = result.indexOf('"operationId"'); + const parametersIndex = result.indexOf('"parameters"'); + const requestBodyIndex = result.indexOf('"requestBody"'); + const responsesIndex = result.indexOf('"responses"'); + const callbacksIndex = result.indexOf('"callbacks"'); + const deprecatedIndex = result.indexOf('"deprecated"'); + const securityIndex = result.indexOf('"security"'); + const serversIndex = result.indexOf('"servers"'); + + expect(tagsIndex).toBeLessThan(summaryIndex); + expect(summaryIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(operationIdIndex); + expect(operationIdIndex).toBeLessThan(parametersIndex); + expect(parametersIndex).toBeLessThan(requestBodyIndex); + expect(requestBodyIndex).toBeLessThan(responsesIndex); + expect(responsesIndex).toBeLessThan(callbacksIndex); + expect(callbacksIndex).toBeLessThan(deprecatedIndex); + expect(deprecatedIndex).toBeLessThan(securityIndex); + expect(securityIndex).toBeLessThan(serversIndex); + }); + + it('should sort info keys correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + openapi: '3.0.0', + info: { + version: '1.0.0', + termsOfService: 'https://example.com/terms', + title: 'Test API', + description: 'A test API', + contact: { name: 'API Team', email: 'api@example.com' }, + license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' } + } + } + }; + + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + // Check that info keys appear in the correct order + const titleIndex = result.indexOf('"title"'); + const descriptionIndex = result.indexOf('"description"'); + const versionIndex = result.indexOf('"version"'); + const termsOfServiceIndex = result.indexOf('"termsOfService"'); + const contactIndex = result.indexOf('"contact"'); + const licenseIndex = result.indexOf('"license"'); + + expect(titleIndex).toBeLessThan(descriptionIndex); + expect(descriptionIndex).toBeLessThan(versionIndex); + expect(versionIndex).toBeLessThan(termsOfServiceIndex); + expect(termsOfServiceIndex).toBeLessThan(contactIndex); + expect(contactIndex).toBeLessThan(licenseIndex); + }); + + it('should handle custom extensions correctly', () => { + const jsonPrinter = plugin.printers?.['openapi-json-ast']; + expect(jsonPrinter).toBeDefined(); + + const testData = { + content: { + 'x-custom-field': 'value', + 'openapi': '3.0.0', + 'info': { 'title': 'Test API', 'version': '1.0.0' }, + 'paths': {}, + 'x-metadata': { 'custom': 'data' } + } + }; + + const result = jsonPrinter?.print({ getValue: () => testData }, { tabWidth: 2 }, () => ''); + expect(result).toBeDefined(); + + // Custom extensions should come after standard keys + const openapiIndex = result.indexOf('"openapi"'); + const infoIndex = result.indexOf('"info"'); + const pathsIndex = result.indexOf('"paths"'); + const xCustomFieldIndex = result.indexOf('"x-custom-field"'); + const xMetadataIndex = result.indexOf('"x-metadata"'); + + expect(openapiIndex).toBeLessThan(infoIndex); + expect(infoIndex).toBeLessThan(pathsIndex); + expect(pathsIndex).toBeLessThan(xCustomFieldIndex); + expect(xCustomFieldIndex).toBeLessThan(xMetadataIndex); + }); +}); diff --git a/test/vendor.test.ts b/test/vendor.test.ts new file mode 100644 index 0000000..76f7c04 --- /dev/null +++ b/test/vendor.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'bun:test'; +import { getVendorExtensions } from '../src/extensions'; + +describe('Vendor Extension System', () => { + it('should load vendor extensions from TS files', () => { + const vendorExtensions = getVendorExtensions(); + + // Should have loaded Speakeasy and Redoc extensions + expect(vendorExtensions).toBeDefined(); + expect(typeof vendorExtensions).toBe('object'); + + // Check if extensions were loaded + expect(vendorExtensions['top-level']).toBeDefined(); + expect(vendorExtensions['top-level']['x-speakeasy-sdk']).toBe(2); // before('info') = position 2 + expect(vendorExtensions['top-level']['x-speakeasy-auth']).toBe(11); // after('paths') = position 11 + }); + + + it('should handle vendor extensions gracefully', () => { + // This test ensures the system doesn't crash when loading extensions + const vendorExtensions = getVendorExtensions(); + expect(vendorExtensions).toBeDefined(); + expect(typeof vendorExtensions).toBe('object'); + }); + + it('should have proper extension structure', () => { + const vendorExtensions = getVendorExtensions(); + + // Check that extensions have the right structure + expect(vendorExtensions['top-level']).toBeDefined(); + expect(vendorExtensions['info']).toBeDefined(); + expect(vendorExtensions['operation']).toBeDefined(); + expect(vendorExtensions['schema']).toBeDefined(); + + // Check specific extensions + expect(vendorExtensions['top-level']['x-speakeasy-sdk']).toBe(2); // before('info') = position 2 + expect(vendorExtensions['top-level']['x-speakeasy-auth']).toBe(11); // after('paths') = position 11 + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7ad6200 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": false, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +}