From 8ffbcc25facc6337b50c2318b4cd9b3cdca1c9a5 Mon Sep 17 00:00:00 2001 From: Luke Hagar Date: Thu, 25 Sep 2025 16:32:08 +0000 Subject: [PATCH] Refactor OpenAPI plugin: remove unused index.ts, update package.json with author info, enhance file detection logic, and add comprehensive tests for OpenAPI file detection. --- index.ts | 1 - package.json | 12 +- src/extensions/vendor-loader.ts | 34 +- src/extensions/vendor/example-usage.ts | 6 +- src/extensions/vendor/postman.ts | 10 +- src/extensions/vendor/redoc.ts | 12 +- src/extensions/vendor/speakeasy.ts | 30 +- src/index.ts | 882 ++++++------------------- src/keys.ts | 3 +- test/file-detection.test.ts | 192 ++++++ 10 files changed, 424 insertions(+), 758 deletions(-) delete mode 100644 index.ts create mode 100644 test/file-detection.test.ts diff --git a/index.ts b/index.ts deleted file mode 100644 index f67b2c6..0000000 --- a/index.ts +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json index da56737..7b28512 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,19 @@ "name": "prettier-plugin-openapi", "version": "1.0.0", "description": "A Prettier plugin for formatting OpenAPI/Swagger JSON and YAML files", + "author": { + "name": "Luke Hagar", + "email": "lukeslakemail@gmail.com", + "url": "https://LukeHagar.com/" + }, + "license": "MIT", "main": "dist/index.js", "module": "dist/index.js", "type": "module", "files": [ - "dist" + "dist", + "README.md", + "LICENSE" ], "scripts": { "build": "tsc", @@ -23,8 +31,6 @@ "json", "formatting" ], - "author": "", - "license": "MIT", "peerDependencies": { "prettier": "^3.0.0" }, diff --git a/src/extensions/vendor-loader.ts b/src/extensions/vendor-loader.ts index 398daa0..197f7c5 100644 --- a/src/extensions/vendor-loader.ts +++ b/src/extensions/vendor-loader.ts @@ -85,37 +85,9 @@ export function getVendorExtensions(): Record> { // Try automatic discovery first return loadAllVendorExtensions(); } catch (error) { - console.warn('Automatic vendor discovery failed, falling back to manual list:', error); + console.warn('Automatic vendor discovery failed, falling back to empty extensions:', error); - // Fallback to manual list - const extensions: Record> = {}; - - const vendorModules = [ - require('./vendor/speakeasy'), - require('./vendor/example-usage'), - // Add more vendor files here as they are created - ]; - - for (const vendorModule of vendorModules) { - if (vendorModule && vendorModule.extensions) { - for (const [context, contextFunction] of Object.entries(vendorModule.extensions)) { - if (typeof contextFunction === 'function') { - // Create context-specific before/after functions - const contextBefore = (key: string) => before(context as keyof ContextKeys, key); - const contextAfter = (key: string) => after(context as keyof ContextKeys, key); - - // Execute the function to get the extensions - const contextExtensions = contextFunction(contextBefore, contextAfter); - - if (!extensions[context]) { - extensions[context] = {}; - } - Object.assign(extensions[context], contextExtensions); - } - } - } - } - - return extensions; + // Return empty extensions if automatic discovery fails + return {}; } } diff --git a/src/extensions/vendor/example-usage.ts b/src/extensions/vendor/example-usage.ts index 95c59c2..8af52aa 100644 --- a/src/extensions/vendor/example-usage.ts +++ b/src/extensions/vendor/example-usage.ts @@ -4,19 +4,19 @@ // Function-based extensions with before/after helpers export const extensions = { - 'top-level': (before, after) => { + 'top-level': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-example-before-info': before('info'), // Before 'info' 'x-example-after-paths': after('paths'), // After 'paths' }; }, - 'operation': (before, after) => { + 'operation': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-example-before-parameters': before('parameters'), // Before 'parameters' 'x-example-after-responses': after('responses'), // After 'responses' }; }, - 'schema': (before, after) => { + 'schema': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-example-validation': after('type'), // After 'type' 'x-example-example': after('example'), // After 'example' diff --git a/src/extensions/vendor/postman.ts b/src/extensions/vendor/postman.ts index 1d22e78..a41bda2 100644 --- a/src/extensions/vendor/postman.ts +++ b/src/extensions/vendor/postman.ts @@ -8,23 +8,23 @@ import { defineVendorExtensions } from ".."; // Function-based extensions with before/after helpers -export const extensions = defineVendorExtensions({ - 'top-level': (before, after) => { +export const extensions = { + 'top-level': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-postman-collection': before('info'), // Before 'info' 'x-postman-version': after('paths'), // After 'paths' }; }, - 'operation': (before, after) => { + 'operation': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-postman-test': after('responses'), // After 'responses' 'x-postman-pre-request': before('parameters'), // Before 'parameters' }; }, - 'schema': (before, after) => { + 'schema': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-postman-example': after('example'), // After 'example' 'x-postman-mock': after('deprecated'), // After 'deprecated' }; } -}); +}; diff --git a/src/extensions/vendor/redoc.ts b/src/extensions/vendor/redoc.ts index 27a02f3..c3752df 100644 --- a/src/extensions/vendor/redoc.ts +++ b/src/extensions/vendor/redoc.ts @@ -8,28 +8,28 @@ import { defineVendorExtensions } from ".."; // Function-based extensions with before/after helpers -export const extensions = defineVendorExtensions({ - 'top-level': (before, after) => { +export const extensions = { + 'top-level': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-redoc-version': before('info'), // Before 'info' 'x-redoc-theme': after('paths'), // After 'paths' }; }, - 'info': (before, after) => { + 'info': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-redoc-info': after('version'), // After 'version' }; }, - 'operation': (before, after) => { + 'operation': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-redoc-group': after('tags'), // After 'tags' 'x-redoc-hide': before('responses'), // Before 'responses' }; }, - 'schema': (before, after) => { + 'schema': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-redoc-example': after('example'), // After 'example' 'x-redoc-readonly': after('deprecated'), // After 'deprecated' }; } -}); +}; diff --git a/src/extensions/vendor/speakeasy.ts b/src/extensions/vendor/speakeasy.ts index 44cb51d..eab0111 100644 --- a/src/extensions/vendor/speakeasy.ts +++ b/src/extensions/vendor/speakeasy.ts @@ -8,74 +8,74 @@ import { defineVendorExtensions } from '../index'; // Function-based extensions with before/after helpers -export const extensions = defineVendorExtensions({ - 'top-level': (before, after) => { +export const extensions = { + 'top-level': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-sdk': before('info'), // Before 'info' 'x-speakeasy-auth': after('paths'), // After 'paths' }; }, - 'info': (before, after) => { + 'info': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-info': after('version'), // After 'version' }; }, - 'operation': (before, after) => { + 'operation': (before: (key: string) => number, after: (key: string) => number) => { 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) => { + 'schema': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-validation': after('type'), // After 'type' 'x-speakeasy-example': after('example'), // After 'example' }; }, - 'parameter': (before, after) => { + 'parameter': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-param': after('schema'), // After 'schema' }; }, - 'response': (before, after) => { + 'response': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-response': after('description'), // After 'description' }; }, - 'securityScheme': (before, after) => { + 'securityScheme': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-auth': after('type'), // After 'type' }; }, - 'server': (before, after) => { + 'server': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-server': after('url'), // After 'url' }; }, - 'tag': (before, after) => { + 'tag': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-tag': after('name'), // After 'name' }; }, - 'externalDocs': (before, after) => { + 'externalDocs': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-docs': after('url'), // After 'url' }; }, - 'webhook': (before, after) => { + 'webhook': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-webhook': after('operationId'), // After 'operationId' }; }, - 'definitions': (before, after) => { + 'definitions': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-definition': after('type'), // After 'type' }; }, - 'securityDefinitions': (before, after) => { + 'securityDefinitions': (before: (key: string) => number, after: (key: string) => number) => { return { 'x-speakeasy-security': after('type'), // After 'type' }; } -}); \ No newline at end of file +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index c050a81..136ca90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ import { Plugin } from 'prettier'; import * as yaml from 'js-yaml'; import { getVendorExtensions } from './extensions'; + import { TOP_LEVEL_KEYS, INFO_KEYS, @@ -17,7 +18,6 @@ import { SERVER_VARIABLE_KEYS, TAG_KEYS, EXTERNAL_DOCS_KEYS, - SWAGGER_2_0_KEYS, WEBHOOK_KEYS } from './keys'; @@ -37,25 +37,6 @@ interface OpenAPIPluginOptions { printWidth?: number; } -// ============================================================================ -// KEY ORDERING CONFIGURATION -// ============================================================================ -// Customize the order of keys by modifying these arrays and maps - - - - -// ============================================================================ -// CUSTOM EXTENSION CONFIGURATION -// ============================================================================ -// Add your custom extensions here with their desired positions - -// Base custom extensions for top-level OpenAPI keys -const BASE_CUSTOM_TOP_LEVEL_EXTENSIONS: Record = { - // Example: 'x-custom-field': 2, // Will be placed after 'info' (position 1) - // Example: 'x-api-version': 0, // Will be placed before 'openapi' -}; - // Load vendor extensions let vendorExtensions: any = {}; @@ -67,86 +48,109 @@ try { vendorExtensions = {}; } -// Use base extensions as default -const CUSTOM_TOP_LEVEL_EXTENSIONS = BASE_CUSTOM_TOP_LEVEL_EXTENSIONS; +// ============================================================================ +// FILE DETECTION FUNCTIONS +// ============================================================================ -// Custom extensions for info section -const CUSTOM_INFO_EXTENSIONS: Record = { - // Example: 'x-api-id': 1, // Will be placed after 'title' (position 0) - // Example: 'x-version-info': 3, // Will be placed after 'version' (position 2) -}; +/** + * Detects if a file is an OpenAPI-related file based on content and structure + */ +function isOpenAPIFile(content: any, filePath?: string): boolean { + if (!content || typeof content !== 'object') { + return false; + } -// Custom extensions for components section -const CUSTOM_COMPONENTS_EXTENSIONS: Record = { - // Example: 'x-custom-schemas': 0, // Will be placed before 'schemas' - // Example: 'x-api-metadata': 9, // Will be placed after 'callbacks' -}; + // Check for root-level OpenAPI indicators + if (content.openapi || content.swagger) { + return true; + } -// Custom extensions for operation objects -const CUSTOM_OPERATION_EXTENSIONS: Record = { - // Example: 'x-rate-limit': 5, // Will be placed after 'parameters' (position 4) - // Example: 'x-custom-auth': 10, // Will be placed after 'servers' (position 9) -}; + // Check for component-like structures + if (content.components || content.definitions || content.parameters || content.responses || content.securityDefinitions) { + return true; + } -// Custom extensions for parameter objects -const CUSTOM_PARAMETER_EXTENSIONS: Record = { - // Example: 'x-validation': 3, // Will be placed after 'description' (position 2) - // Example: 'x-custom-format': 11, // Will be placed after 'examples' (position 10) -}; + // Check for path-like structures (operations) + if (content.paths || isPathObject(content)) { + return true; + } -// Custom extensions for schema objects -const CUSTOM_SCHEMA_EXTENSIONS: Record = { - // Example: 'x-custom-type': 0, // Will be placed before 'type' - // Example: 'x-validation-rules': 30, // Will be placed after 'deprecated' (position 29) -}; + // Check file path patterns for common OpenAPI file structures + // Only accept files in OpenAPI-related directories + if (filePath) { + const path = filePath.toLowerCase(); + + // Check for component directory patterns + if (path.includes('/components/') || + path.includes('/schemas/') || + path.includes('/parameters/') || + path.includes('/responses/') || + path.includes('/requestbodies/') || + path.includes('/headers/') || + path.includes('/examples/') || + path.includes('/securityschemes/') || + path.includes('/links/') || + path.includes('/callbacks/') || + path.includes('/webhooks/') || + path.includes('/paths/')) { + return true; + } + } -// Custom extensions for response objects -const CUSTOM_RESPONSE_EXTENSIONS: Record = { - // Example: 'x-response-time': 1, // Will be placed after 'description' (position 0) - // Example: 'x-cache-info': 4, // Will be placed after 'links' (position 3) -}; + // Check for schema-like structures (but be more strict) + if (isSchemaObject(content)) { + return true; + } -// Custom extensions for security scheme objects -const CUSTOM_SECURITY_SCHEME_EXTENSIONS: Record = { - // Example: 'x-auth-provider': 1, // Will be placed after 'type' (position 0) - // Example: 'x-token-info': 7, // Will be placed after 'openIdConnectUrl' (position 6) -}; + // Check for parameter-like structures + if (isParameterObject(content)) { + return true; + } -// Custom extensions for server objects -const CUSTOM_SERVER_EXTENSIONS: Record = { - // Example: 'x-server-region': 1, // Will be placed after 'url' (position 0) - // Example: 'x-load-balancer': 3, // Will be placed after 'variables' (position 2) -}; + // Check for response-like structures + if (isResponseObject(content)) { + return true; + } -// Custom extensions for tag objects -const CUSTOM_TAG_EXTENSIONS: Record = { - // Example: 'x-tag-color': 1, // Will be placed after 'name' (position 0) - // Example: 'x-tag-priority': 3, // Will be placed after 'externalDocs' (position 2) -}; + // Check for security scheme-like structures + if (isSecuritySchemeObject(content)) { + return true; + } -// Custom extensions for external docs objects -const CUSTOM_EXTERNAL_DOCS_EXTENSIONS: Record = { - // Example: 'x-doc-version': 0, // Will be placed before 'description' - // Example: 'x-doc-language': 2, // Will be placed after 'url' (position 1) -}; + // Check for server-like structures + if (isServerObject(content)) { + return true; + } -// Custom extensions for webhook objects (OpenAPI 3.1+) -const CUSTOM_WEBHOOK_EXTENSIONS: Record = { - // Example: 'x-webhook-secret': 5, // Will be placed after 'parameters' (position 4) - // Example: 'x-webhook-retry': 10, // Will be placed after 'servers' (position 9) -}; + // Check for tag-like structures + if (isTagObject(content)) { + return true; + } -// Custom extensions for Swagger 2.0 definitions -const CUSTOM_DEFINITIONS_EXTENSIONS: Record = { - // Example: 'x-model-version': 0, // Will be placed before 'type' - // Example: 'x-model-category': 30, // Will be placed after 'deprecated' (position 29) -}; + // Check for external docs-like structures + if (isExternalDocsObject(content)) { + return true; + } -// Custom extensions for Swagger 2.0 security definitions -const CUSTOM_SECURITY_DEFINITIONS_EXTENSIONS: Record = { - // Example: 'x-auth-provider': 1, // Will be placed after 'type' (position 0) - // Example: 'x-token-info': 7, // Will be placed after 'scopes' (position 6) -}; + // Check for webhook-like structures + if (isWebhookObject(content)) { + return true; + } + + return false; +} + +/** + * Detects if an object represents a path with operations + */ +function isPathObject(obj: any): boolean { + if (!obj || typeof obj !== 'object') { + return false; + } + + const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']; + return Object.keys(obj).some(key => httpMethods.includes(key.toLowerCase())); +} // Map of path patterns to their key ordering const KEY_ORDERING_MAP: Record = { @@ -180,34 +184,25 @@ const OPERATION_KEY_ORDERING_MAP: Record = { 'webhook': WEBHOOK_KEYS, }; -// Map of custom extensions by context (using vendor extensions) -const CUSTOM_EXTENSIONS_MAP: Record> = { - 'top-level': { ...CUSTOM_TOP_LEVEL_EXTENSIONS, ...vendorExtensions['top-level'] }, - 'info': { ...CUSTOM_INFO_EXTENSIONS, ...vendorExtensions['info'] }, - 'components': { ...CUSTOM_COMPONENTS_EXTENSIONS, ...vendorExtensions['components'] }, - 'operation': { ...CUSTOM_OPERATION_EXTENSIONS, ...vendorExtensions['operation'] }, - 'parameter': { ...CUSTOM_PARAMETER_EXTENSIONS, ...vendorExtensions['parameter'] }, - 'schema': { ...CUSTOM_SCHEMA_EXTENSIONS, ...vendorExtensions['schema'] }, - 'response': { ...CUSTOM_RESPONSE_EXTENSIONS, ...vendorExtensions['response'] }, - 'securityScheme': { ...CUSTOM_SECURITY_SCHEME_EXTENSIONS, ...vendorExtensions['securityScheme'] }, - 'server': { ...CUSTOM_SERVER_EXTENSIONS, ...vendorExtensions['server'] }, - 'tag': { ...CUSTOM_TAG_EXTENSIONS, ...vendorExtensions['tag'] }, - 'externalDocs': { ...CUSTOM_EXTERNAL_DOCS_EXTENSIONS, ...vendorExtensions['externalDocs'] }, - 'webhook': { ...CUSTOM_WEBHOOK_EXTENSIONS, ...vendorExtensions['webhook'] }, - 'definitions': { ...CUSTOM_DEFINITIONS_EXTENSIONS, ...vendorExtensions['definitions'] }, - 'securityDefinitions': { ...CUSTOM_SECURITY_DEFINITIONS_EXTENSIONS, ...vendorExtensions['securityDefinitions'] }, -}; - const plugin: Plugin = { languages: [ { name: 'openapi-json', - extensions: ['.openapi.json', '.swagger.json'], + extensions: [ + '.openapi.json', '.swagger.json', + // Support for component files + '.json' + ], parsers: ['openapi-json-parser'], }, { name: 'openapi-yaml', - extensions: ['.openapi.yaml', '.openapi.yml', '.swagger.yaml', '.swagger.yml'], + extensions: [ + '.openapi.yaml', '.openapi.yml', + '.swagger.yaml', '.swagger.yml', + // Support for component files + '.yaml', '.yml' + ], parsers: ['openapi-yaml-parser'], }, ], @@ -216,6 +211,12 @@ const plugin: Plugin = { parse: (text: string, options?: any): OpenAPINode => { try { const parsed = JSON.parse(text); + + // Check if this is an OpenAPI file + if (!isOpenAPIFile(parsed, options?.filepath)) { + throw new Error('Not an OpenAPI file'); + } + return { type: 'openapi-json', content: parsed, @@ -239,6 +240,12 @@ const plugin: Plugin = { console.warn('YAML parsing warning:', warning); } }); + + // Check if this is an OpenAPI file + if (!isOpenAPIFile(parsed, options?.filepath)) { + throw new Error('Not an OpenAPI file'); + } + return { type: 'openapi-yaml', content: parsed, @@ -297,62 +304,12 @@ function sortOpenAPIKeys(obj: any): any { return obj; } + // Get vendor extensions for top-level + const topLevelExtensions = vendorExtensions['top-level'] || {}; + const sortedKeys = Object.keys(obj).sort((a, b) => { - // Check for custom extensions first - const aCustomPos = CUSTOM_TOP_LEVEL_EXTENSIONS[a]; - const bCustomPos = CUSTOM_TOP_LEVEL_EXTENSIONS[b]; - - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - // Check if custom position is within standard keys range - if (aCustomPos < TOP_LEVEL_KEYS.length) { - return -1; // Custom key should come before standard keys - } - } - - if (bCustomPos !== undefined) { - // Check if custom position is within standard keys range - if (bCustomPos < TOP_LEVEL_KEYS.length) { - return 1; // Custom key should come before standard keys - } - } - - const aIndex = TOP_LEVEL_KEYS.indexOf(a as any); - const bIndex = TOP_LEVEL_KEYS.indexOf(b as any); - - // If both keys are in the order list, sort by their position - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - // If only one key is in the order list, prioritize it - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - // Handle custom extensions that are positioned after standard keys - if (aCustomPos !== undefined) { - return -1; // Custom extensions come after standard keys - } - if (bCustomPos !== undefined) { - return 1; // Custom extensions come after standard keys - } - - // Handle x- prefixed keys (custom extensions) vs unknown keys - const aIsCustomExtension = a.startsWith('x-'); - const bIsCustomExtension = b.startsWith('x-'); - - if (aIsCustomExtension && !bIsCustomExtension) { - return -1; // Custom extensions come before unknown keys - } - if (!aIsCustomExtension && bIsCustomExtension) { - return 1; // Unknown keys come after custom extensions - } - - // For unknown keys (not in standard list or custom extensions), sort alphabetically at the end - return a.localeCompare(b); + // Use the unified sorting function + return sortKeys(a, b, TOP_LEVEL_KEYS, topLevelExtensions); }); const sortedObj: any = {}; @@ -374,48 +331,11 @@ function sortOpenAPIKeysEnhanced(obj: any, path: string = ''): any { return obj.map((item, index) => sortOpenAPIKeysEnhanced(item, `${path}[${index}]`)); } + const contextKey = getContextKey(path, obj); + const standardKeys = getStandardKeysForContext(contextKey); + const customExtensions = vendorExtensions[contextKey] || {}; + const sortedKeys = Object.keys(obj).sort((a, b) => { - // Get custom extensions for the current context - const contextKey = getContextKey(path, obj); - const customExtensions = CUSTOM_EXTENSIONS_MAP[contextKey] || {}; - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - // Handle custom extensions first - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - // Check if custom position is within standard keys range - const standardKeys = getStandardKeysForContext(contextKey); - if (aCustomPos < standardKeys.length) { - return -1; // Custom key should come before standard keys - } - } - - if (bCustomPos !== undefined) { - // Check if custom position is within standard keys range - const standardKeys = getStandardKeysForContext(contextKey); - if (bCustomPos < standardKeys.length) { - return 1; // Custom key should come before standard keys - } - } - - // Get the key ordering for the current path - const currentPathOrder = KEY_ORDERING_MAP[path] || []; - const aIndex = currentPathOrder.indexOf(a); - const bIndex = currentPathOrder.indexOf(b); - - // If both keys are in the order list, sort by their position - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - // If only one key is in the order list, prioritize it - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - // Special handling for paths (sort by path pattern) if (path === 'paths') { return sortPathKeys(a, b); @@ -426,61 +346,8 @@ function sortOpenAPIKeysEnhanced(obj: any, path: string = ''): any { return sortResponseCodes(a, b); } - // Use context-based sorting - if (contextKey === 'operation') { - return sortOperationKeysWithExtensions(a, b, CUSTOM_OPERATION_EXTENSIONS); - } - - if (contextKey === 'parameter') { - return sortParameterKeysWithExtensions(a, b, CUSTOM_PARAMETER_EXTENSIONS); - } - - if (contextKey === 'schema') { - return sortSchemaKeysWithExtensions(a, b, CUSTOM_SCHEMA_EXTENSIONS); - } - - if (contextKey === 'response') { - return sortResponseKeysWithExtensions(a, b, CUSTOM_RESPONSE_EXTENSIONS); - } - - if (contextKey === 'securityScheme') { - return sortSecuritySchemeKeysWithExtensions(a, b, CUSTOM_SECURITY_SCHEME_EXTENSIONS); - } - - if (contextKey === 'server') { - return sortServerKeysWithExtensions(a, b, CUSTOM_SERVER_EXTENSIONS); - } - - if (contextKey === 'tag') { - return sortTagKeysWithExtensions(a, b, CUSTOM_TAG_EXTENSIONS); - } - - if (contextKey === 'externalDocs') { - return sortExternalDocsKeysWithExtensions(a, b, CUSTOM_EXTERNAL_DOCS_EXTENSIONS); - } - - if (contextKey === 'webhook') { - return sortWebhookKeysWithExtensions(a, b, CUSTOM_WEBHOOK_EXTENSIONS); - } - - if (contextKey === 'definitions') { - return sortDefinitionsKeysWithExtensions(a, b, CUSTOM_DEFINITIONS_EXTENSIONS); - } - - if (contextKey === 'securityDefinitions') { - return sortSecurityDefinitionsKeysWithExtensions(a, b, CUSTOM_SECURITY_DEFINITIONS_EXTENSIONS); - } - - // Handle custom extensions that are positioned after standard keys - if (aCustomPos !== undefined) { - return -1; // Custom extensions come after standard keys - } - if (bCustomPos !== undefined) { - return 1; // Custom extensions come after standard keys - } - - // For unknown keys (not in standard list or custom extensions), sort alphabetically at the end - return a.localeCompare(b); + // Use the unified sorting function for all other cases + return sortKeys(a, b, standardKeys, customExtensions); }); const sortedObj: any = {}; @@ -516,9 +383,7 @@ function sortResponseCodes(a: string, b: string): number { return a.localeCompare(b); } -// ============================================================================ -// OBJECT TYPE DETECTION FUNCTIONS -// ============================================================================ +//#region Object type detection functions function isOperationObject(obj: any): boolean { const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']; @@ -530,7 +395,17 @@ function isParameterObject(obj: any): boolean { } function isSchemaObject(obj: any): boolean { - return obj && typeof obj === 'object' && ('type' in obj || 'properties' in obj || '$ref' in obj); + if (!obj || typeof obj !== 'object') { + return false; + } + + // Check for JSON Schema keywords - be very strict + const hasSchemaKeywords = '$ref' in obj || 'allOf' in obj || 'oneOf' in obj || 'anyOf' in obj || 'not' in obj; + const hasValidType = 'type' in obj && obj.type && ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null'].includes(obj.type); + + // Only return true if we have clear schema indicators + // Must have either schema keywords OR valid type with schema properties + return hasSchemaKeywords || (hasValidType && ('properties' in obj || 'items' in obj || 'enum' in obj)); } function isResponseObject(obj: any): boolean { @@ -559,13 +434,41 @@ function isWebhookObject(obj: any): boolean { return obj && typeof obj === 'object' && Object.keys(obj).some(key => httpMethods.includes(key.toLowerCase())); } -// ============================================================================ -// SORTING FUNCTIONS USING CONFIGURATION ARRAYS -// ============================================================================ +//#endregion -function sortOperationKeys(a: string, b: string): number { - const aIndex = OPERATION_KEYS.indexOf(a as any); - const bIndex = OPERATION_KEYS.indexOf(b as any); +//#region Unified sorting function +/** + * Universal sorting function that handles all OpenAPI key sorting + * @param a First key to compare + * @param b Second key to compare + * @param standardKeys Array of standard keys in order + * @param customExtensions Custom extension positions + * @returns Comparison result + */ +function sortKeys(a: string, b: string, standardKeys: readonly string[], customExtensions: Record = {}): number { + const aCustomPos = customExtensions[a]; + const bCustomPos = customExtensions[b]; + + // Handle custom extensions first + if (aCustomPos !== undefined && bCustomPos !== undefined) { + return aCustomPos - bCustomPos; + } + + if (aCustomPos !== undefined) { + if (aCustomPos < standardKeys.length) { + return -1; // Custom key should come before standard keys + } + } + + if (bCustomPos !== undefined) { + if (bCustomPos < standardKeys.length) { + return 1; // Custom key should come before standard keys + } + } + + // Standard sorting + const aIndex = standardKeys.indexOf(a); + const bIndex = standardKeys.indexOf(b); if (aIndex !== -1 && bIndex !== -1) { return aIndex - bIndex; @@ -574,110 +477,16 @@ function sortOperationKeys(a: string, b: string): number { if (aIndex !== -1) return -1; if (bIndex !== -1) return 1; + // Handle custom extensions after standard keys + if (aCustomPos !== undefined) return -1; + if (bCustomPos !== undefined) return 1; + + // For unknown keys, sort alphabetically at the end return a.localeCompare(b); } +//#endregion -function sortParameterKeys(a: string, b: string): number { - const aIndex = PARAMETER_KEYS.indexOf(a as any); - const bIndex = PARAMETER_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - return a.localeCompare(b); -} - -function sortSchemaKeys(a: string, b: string): number { - const aIndex = SCHEMA_KEYS.indexOf(a as any); - const bIndex = SCHEMA_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - return a.localeCompare(b); -} - -function sortResponseKeys(a: string, b: string): number { - const aIndex = RESPONSE_KEYS.indexOf(a as any); - const bIndex = RESPONSE_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - return a.localeCompare(b); -} - -function sortSecuritySchemeKeys(a: string, b: string): number { - const aIndex = SECURITY_SCHEME_KEYS.indexOf(a as any); - const bIndex = SECURITY_SCHEME_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - return a.localeCompare(b); -} - -function sortServerKeys(a: string, b: string): number { - const aIndex = SERVER_KEYS.indexOf(a as any); - const bIndex = SERVER_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - return a.localeCompare(b); -} - -function sortTagKeys(a: string, b: string): number { - const aIndex = TAG_KEYS.indexOf(a as any); - const bIndex = TAG_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - return a.localeCompare(b); -} - -function sortExternalDocsKeys(a: string, b: string): number { - const aIndex = EXTERNAL_DOCS_KEYS.indexOf(a as any); - const bIndex = EXTERNAL_DOCS_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - return a.localeCompare(b); -} - -// ============================================================================ -// HELPER FUNCTIONS FOR CUSTOM EXTENSIONS -// ============================================================================ +//#region Helper functions for custom extensions function getContextKey(path: string, obj: any): string { // Determine the context based on path and object properties @@ -750,362 +559,51 @@ function getStandardKeysForContext(contextKey: string): readonly string[] { } // ============================================================================ -// SORTING FUNCTIONS WITH EXTENSIONS SUPPORT +// CONTEXT-SPECIFIC SORTING FUNCTIONS (using unified sortKeys) // ============================================================================ function sortOperationKeysWithExtensions(a: string, b: string, customExtensions: Record): number { - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - // Handle custom extensions - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - if (aCustomPos < OPERATION_KEYS.length) return -1; - } - - if (bCustomPos !== undefined) { - if (bCustomPos < OPERATION_KEYS.length) return 1; - } - - // Standard sorting - const aIndex = OPERATION_KEYS.indexOf(a as any); - const bIndex = OPERATION_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - // Handle custom extensions after standard keys - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - return a.localeCompare(b); + return sortKeys(a, b, OPERATION_KEYS, customExtensions); } function sortParameterKeysWithExtensions(a: string, b: string, customExtensions: Record): number { - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - if (aCustomPos < PARAMETER_KEYS.length) return -1; - } - - if (bCustomPos !== undefined) { - if (bCustomPos < PARAMETER_KEYS.length) return 1; - } - - const aIndex = PARAMETER_KEYS.indexOf(a as any); - const bIndex = PARAMETER_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - return a.localeCompare(b); + return sortKeys(a, b, PARAMETER_KEYS, customExtensions); } function sortSchemaKeysWithExtensions(a: string, b: string, customExtensions: Record): number { - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - if (aCustomPos < SCHEMA_KEYS.length) return -1; - } - - if (bCustomPos !== undefined) { - if (bCustomPos < SCHEMA_KEYS.length) return 1; - } - - const aIndex = SCHEMA_KEYS.indexOf(a as any); - const bIndex = SCHEMA_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - return a.localeCompare(b); + return sortKeys(a, b, SCHEMA_KEYS, customExtensions); } function sortResponseKeysWithExtensions(a: string, b: string, customExtensions: Record): number { - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - if (aCustomPos < RESPONSE_KEYS.length) return -1; - } - - if (bCustomPos !== undefined) { - if (bCustomPos < RESPONSE_KEYS.length) return 1; - } - - const aIndex = RESPONSE_KEYS.indexOf(a as any); - const bIndex = RESPONSE_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - return a.localeCompare(b); + return sortKeys(a, b, RESPONSE_KEYS, customExtensions); } function sortSecuritySchemeKeysWithExtensions(a: string, b: string, customExtensions: Record): number { - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - if (aCustomPos < SECURITY_SCHEME_KEYS.length) return -1; - } - - if (bCustomPos !== undefined) { - if (bCustomPos < SECURITY_SCHEME_KEYS.length) return 1; - } - - const aIndex = SECURITY_SCHEME_KEYS.indexOf(a as any); - const bIndex = SECURITY_SCHEME_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - return a.localeCompare(b); + return sortKeys(a, b, SECURITY_SCHEME_KEYS, customExtensions); } function sortServerKeysWithExtensions(a: string, b: string, customExtensions: Record): number { - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - if (aCustomPos < SERVER_KEYS.length) return -1; - } - - if (bCustomPos !== undefined) { - if (bCustomPos < SERVER_KEYS.length) return 1; - } - - const aIndex = SERVER_KEYS.indexOf(a as any); - const bIndex = SERVER_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - return a.localeCompare(b); + return sortKeys(a, b, SERVER_KEYS, customExtensions); } function sortTagKeysWithExtensions(a: string, b: string, customExtensions: Record): number { - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - if (aCustomPos < TAG_KEYS.length) return -1; - } - - if (bCustomPos !== undefined) { - if (bCustomPos < TAG_KEYS.length) return 1; - } - - const aIndex = TAG_KEYS.indexOf(a as any); - const bIndex = TAG_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - return a.localeCompare(b); + return sortKeys(a, b, TAG_KEYS, customExtensions); } function sortExternalDocsKeysWithExtensions(a: string, b: string, customExtensions: Record): number { - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - if (aCustomPos < EXTERNAL_DOCS_KEYS.length) return -1; - } - - if (bCustomPos !== undefined) { - if (bCustomPos < EXTERNAL_DOCS_KEYS.length) return 1; - } - - const aIndex = EXTERNAL_DOCS_KEYS.indexOf(a as any); - const bIndex = EXTERNAL_DOCS_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - return a.localeCompare(b); + return sortKeys(a, b, EXTERNAL_DOCS_KEYS, customExtensions); } function sortWebhookKeysWithExtensions(a: string, b: string, customExtensions: Record): number { - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - if (aCustomPos < WEBHOOK_KEYS.length) return -1; - } - - if (bCustomPos !== undefined) { - if (bCustomPos < WEBHOOK_KEYS.length) return 1; - } - - const aIndex = WEBHOOK_KEYS.indexOf(a as any); - const bIndex = WEBHOOK_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - return a.localeCompare(b); + return sortKeys(a, b, WEBHOOK_KEYS, customExtensions); } function sortDefinitionsKeysWithExtensions(a: string, b: string, customExtensions: Record): number { - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - if (aCustomPos < SCHEMA_KEYS.length) return -1; - } - - if (bCustomPos !== undefined) { - if (bCustomPos < SCHEMA_KEYS.length) return 1; - } - - const aIndex = SCHEMA_KEYS.indexOf(a as any); - const bIndex = SCHEMA_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - return a.localeCompare(b); + return sortKeys(a, b, SCHEMA_KEYS, customExtensions); } function sortSecurityDefinitionsKeysWithExtensions(a: string, b: string, customExtensions: Record): number { - const aCustomPos = customExtensions[a]; - const bCustomPos = customExtensions[b]; - - if (aCustomPos !== undefined && bCustomPos !== undefined) { - return aCustomPos - bCustomPos; - } - - if (aCustomPos !== undefined) { - if (aCustomPos < SECURITY_SCHEME_KEYS.length) return -1; - } - - if (bCustomPos !== undefined) { - if (bCustomPos < SECURITY_SCHEME_KEYS.length) return 1; - } - - const aIndex = SECURITY_SCHEME_KEYS.indexOf(a as any); - const bIndex = SECURITY_SCHEME_KEYS.indexOf(b as any); - - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex; - } - - if (aIndex !== -1) return -1; - if (bIndex !== -1) return 1; - - if (aCustomPos !== undefined) return -1; - if (bCustomPos !== undefined) return 1; - - return a.localeCompare(b); + return sortKeys(a, b, SECURITY_SCHEME_KEYS, customExtensions); } export default plugin; diff --git a/src/keys.ts b/src/keys.ts index 885d3d3..7cd0662 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -87,6 +87,7 @@ export const OPERATION_KEYS = [ 'deprecated', 'security', 'servers', // OpenAPI 3.0+ + 'externalDocs', // OpenAPI 3.0+ ] as const; // Parameter keys in preferred order @@ -197,8 +198,6 @@ export const SCHEMA_KEYS = [ '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 diff --git a/test/file-detection.test.ts b/test/file-detection.test.ts new file mode 100644 index 0000000..e76c252 --- /dev/null +++ b/test/file-detection.test.ts @@ -0,0 +1,192 @@ +import { describe, it, expect } from 'bun:test'; +import plugin from '../src/index'; + +describe('File Detection Tests', () => { + it('should detect OpenAPI root files', () => { + const yamlParser = plugin.parsers?.['openapi-yaml-parser']; + expect(yamlParser).toBeDefined(); + + const testYaml = `openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: Success`; + + // @ts-ignore We are mocking things here + const result = yamlParser?.parse(testYaml, { filepath: 'openapi.yaml' }); + + expect(result).toBeDefined(); + expect(result?.type).toBe('openapi-yaml'); + expect(result?.content.openapi).toBe('3.0.0'); + }); + + it('should detect partial schema files', () => { + const yamlParser = plugin.parsers?.['openapi-yaml-parser']; + expect(yamlParser).toBeDefined(); + + const schemaYaml = `type: object +properties: + id: + type: integer + name: + type: string +required: + - id + - name`; + + // @ts-ignore We are mocking things here + const result = yamlParser?.parse(schemaYaml, { filepath: 'components/schemas/User.yaml' }); + + expect(result).toBeDefined(); + expect(result?.type).toBe('openapi-yaml'); + expect(result?.content.type).toBe('object'); + }); + + it('should detect parameter files', () => { + const yamlParser = plugin.parsers?.['openapi-yaml-parser']; + expect(yamlParser).toBeDefined(); + + const parameterYaml = `name: id +in: path +required: true +description: User ID +schema: + type: integer`; + + // @ts-ignore We are mocking things here + const result = yamlParser?.parse(parameterYaml, { filepath: 'components/parameters/UserId.yaml' }); + + expect(result).toBeDefined(); + expect(result?.type).toBe('openapi-yaml'); + expect(result?.content.name).toBe('id'); + }); + + it('should detect response files', () => { + const yamlParser = plugin.parsers?.['openapi-yaml-parser']; + expect(yamlParser).toBeDefined(); + + const responseYaml = `description: User response +content: + application/json: + schema: + type: object + properties: + id: + type: integer + name: + type: string`; + + // @ts-ignore We are mocking things here + const result = yamlParser?.parse(responseYaml, { filepath: 'components/responses/UserResponse.yaml' }); + + expect(result).toBeDefined(); + expect(result?.type).toBe('openapi-yaml'); + expect(result?.content.description).toBe('User response'); + }); + + it('should detect path files', () => { + const yamlParser = plugin.parsers?.['openapi-yaml-parser']; + expect(yamlParser).toBeDefined(); + + const pathYaml = `get: + summary: Get users + responses: + '200': + description: Success +post: + summary: Create user + requestBody: + content: + application/json: + schema: + type: object`; + + // @ts-ignore We are mocking things here + const result = yamlParser?.parse(pathYaml, { filepath: 'paths/users.yaml' }); + + expect(result).toBeDefined(); + expect(result?.type).toBe('openapi-yaml'); + expect(result?.content.get).toBeDefined(); + expect(result?.content.post).toBeDefined(); + }); + + it('should detect security scheme files', () => { + const yamlParser = plugin.parsers?.['openapi-yaml-parser']; + expect(yamlParser).toBeDefined(); + + const securityYaml = `type: http +scheme: bearer +bearerFormat: JWT +description: JWT authentication`; + + // @ts-ignore We are mocking things here + const result = yamlParser?.parse(securityYaml, { filepath: 'components/securitySchemes/BearerAuth.yaml' }); + + expect(result).toBeDefined(); + expect(result?.type).toBe('openapi-yaml'); + expect(result?.content.type).toBe('http'); + }); + + it('should reject non-OpenAPI files', () => { + const yamlParser = plugin.parsers?.['openapi-yaml-parser']; + expect(yamlParser).toBeDefined(); + + const nonOpenAPIYaml = `name: John +age: 30 +city: New York`; + + // @ts-ignore We are mocking things here + expect(() => yamlParser?.parse(nonOpenAPIYaml, { filepath: 'config/data.yaml' })).toThrow('Not an OpenAPI file'); + }); + + it('should accept files in OpenAPI directories even with simple content', () => { + const yamlParser = plugin.parsers?.['openapi-yaml-parser']; + expect(yamlParser).toBeDefined(); + + const simpleYaml = `name: John +age: 30 +city: New York`; + + // @ts-ignore We are mocking things here + const result = yamlParser?.parse(simpleYaml, { filepath: 'components/schemas/User.yaml' }); + expect(result).toBeDefined(); + expect(result?.type).toBe('openapi-yaml'); + }); + + it('should support component directory patterns', () => { + const yamlParser = plugin.parsers?.['openapi-yaml-parser']; + expect(yamlParser).toBeDefined(); + + const componentYaml = `type: object +properties: + message: + type: string`; + + // Test various component directory patterns + const paths = [ + 'components/schemas/Error.yaml', + 'components/parameters/CommonPagination.yaml', + 'components/responses/ErrorResponse.yaml', + 'components/requestBodies/UserCreateBody.yaml', + 'components/headers/RateLimitHeaders.yaml', + 'components/examples/UserExample.yaml', + 'components/securitySchemes/BearerAuth.yaml', + 'components/links/UserCreatedLink.yaml', + 'components/callbacks/NewMessageCallback.yaml', + 'webhooks/messageCreated.yaml', + 'paths/users.yaml' + ]; + + paths.forEach(path => { + // @ts-ignore We are mocking things here + const result = yamlParser?.parse(componentYaml, { filepath: path }); + expect(result).toBeDefined(); + expect(result?.type).toBe('openapi-yaml'); + }); + }); +});