mirror of
https://github.com/LukeHagar/prettier-plugin-openapi.git
synced 2025-12-06 12:47:47 +00:00
Saving current state
This commit is contained in:
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
@@ -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
|
||||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["./dist/index.js"],
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 80,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
550
CUSTOMIZATION.md
Normal file
550
CUSTOMIZATION.md
Normal file
@@ -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<string, number> = {
|
||||||
|
'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<string, number> = {
|
||||||
|
'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<string, number> = {
|
||||||
|
'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<string, number> = {
|
||||||
|
'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<string, number> = {
|
||||||
|
'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<string, number> = {
|
||||||
|
'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<string, number> = {
|
||||||
|
'x-api-id': 1, // After 'title'
|
||||||
|
'x-version-info': 3, // After 'version'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Operation custom extensions
|
||||||
|
const CUSTOM_OPERATION_EXTENSIONS: Record<string, number> = {
|
||||||
|
'x-rate-limit': 5, // After 'parameters'
|
||||||
|
'x-custom-auth': 10, // After 'servers'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Schema custom extensions
|
||||||
|
const CUSTOM_SCHEMA_EXTENSIONS: Record<string, number> = {
|
||||||
|
'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
|
||||||
248
EXTENSIONS.md
Normal file
248
EXTENSIONS.md
Normal file
@@ -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<string, number> = {
|
||||||
|
'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<string, number> = {
|
||||||
|
'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<string, number> = {
|
||||||
|
'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<string, number> = {
|
||||||
|
'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<string, number> = {
|
||||||
|
'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<string, number> = {
|
||||||
|
'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.
|
||||||
204
README.md
Normal file
204
README.md
Normal file
@@ -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
|
||||||
428
SUPPORTED_KEYS.md
Normal file
428
SUPPORTED_KEYS.md
Normal file
@@ -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
|
||||||
221
VENDOR_DYNAMIC_FINAL.md
Normal file
221
VENDOR_DYNAMIC_FINAL.md
Normal file
@@ -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! 🚀
|
||||||
180
VENDOR_FUNCTION_BASED_FINAL.md
Normal file
180
VENDOR_FUNCTION_BASED_FINAL.md
Normal file
@@ -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! 🚀
|
||||||
216
VENDOR_HELPER_FUNCTIONS.md
Normal file
216
VENDOR_HELPER_FUNCTIONS.md
Normal file
@@ -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! 🎉
|
||||||
164
VENDOR_HELPER_FUNCTIONS_FINAL.md
Normal file
164
VENDOR_HELPER_FUNCTIONS_FINAL.md
Normal file
@@ -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! 🚀
|
||||||
183
VENDOR_SIMPLIFIED_FINAL.md
Normal file
183
VENDOR_SIMPLIFIED_FINAL.md
Normal file
@@ -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! 🚀
|
||||||
222
VENDOR_SYSTEM.md
Normal file
222
VENDOR_SYSTEM.md
Normal file
@@ -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.
|
||||||
127
VENDOR_SYSTEM_FINAL.md
Normal file
127
VENDOR_SYSTEM_FINAL.md
Normal file
@@ -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! 🚀
|
||||||
155
VENDOR_SYSTEM_SIMPLE.md
Normal file
155
VENDOR_SYSTEM_SIMPLE.md
Normal file
@@ -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! 🎉
|
||||||
47
bun.lock
Normal file
47
bun.lock
Normal file
@@ -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=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
156
examples/custom-extensions-example.yaml
Normal file
156
examples/custom-extensions-example.yaml
Normal file
@@ -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
|
||||||
224
examples/petstore.yaml
Normal file
224
examples/petstore.yaml
Normal file
@@ -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
|
||||||
170
examples/usage.md
Normal file
170
examples/usage.md
Normal file
@@ -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 .
|
||||||
|
```
|
||||||
40
package.json
Normal file
40
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
196
src/extensions/README.md
Normal file
196
src/extensions/README.md
Normal file
@@ -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! 🚀
|
||||||
29
src/extensions/example-usage.ts
Normal file
29
src/extensions/example-usage.ts
Normal file
@@ -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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
136
src/extensions/index.ts
Normal file
136
src/extensions/index.ts
Normal file
@@ -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<T extends keyof ContextKeys>(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<T extends keyof ContextKeys>(context: T, key: string): number {
|
||||||
|
const keys = getContextKeys(context);
|
||||||
|
return keys.indexOf(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for easy positioning
|
||||||
|
export function before<T extends keyof ContextKeys>(context: T, key: string): number {
|
||||||
|
const position = getKeyPosition(context, key);
|
||||||
|
return position === -1 ? 0 : position;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function after<T extends keyof ContextKeys>(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<string, Record<string, number>> {
|
||||||
|
return loadVendorExtensions();
|
||||||
|
}
|
||||||
|
|
||||||
121
src/extensions/vendor-loader.ts
Normal file
121
src/extensions/vendor-loader.ts
Normal file
@@ -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<string, Record<string, number>> {
|
||||||
|
const extensions: Record<string, Record<string, number>> = {};
|
||||||
|
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<string, Record<string, number>> {
|
||||||
|
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<string, Record<string, number>> = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/extensions/vendor/example-usage.ts
vendored
Normal file
25
src/extensions/vendor/example-usage.ts
vendored
Normal file
@@ -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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
30
src/extensions/vendor/postman.ts
vendored
Normal file
30
src/extensions/vendor/postman.ts
vendored
Normal file
@@ -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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
35
src/extensions/vendor/redoc.ts
vendored
Normal file
35
src/extensions/vendor/redoc.ts
vendored
Normal file
@@ -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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
81
src/extensions/vendor/speakeasy.ts
vendored
Normal file
81
src/extensions/vendor/speakeasy.ts
vendored
Normal file
@@ -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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
1111
src/index.ts
Normal file
1111
src/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
301
src/keys.ts
Normal file
301
src/keys.ts
Normal file
@@ -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;
|
||||||
395
test/custom-extensions.test.ts
Normal file
395
test/custom-extensions.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
88
test/demo.ts
Normal file
88
test/demo.ts
Normal file
@@ -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);
|
||||||
618
test/key-ordering.test.ts
Normal file
618
test/key-ordering.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
105
test/options.test.ts
Normal file
105
test/options.test.ts
Normal file
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
209
test/plugin.test.ts
Normal file
209
test/plugin.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
162
test/simple-ordering.test.ts
Normal file
162
test/simple-ordering.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
39
test/vendor.test.ts
Normal file
39
test/vendor.test.ts
Normal file
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user