diff --git a/package.json b/package.json index 838e48a..bd2f8c6 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "simplify" ], "dependencies": { - "@netcracker/qubership-apihub-json-crawl": "1.0.4", + "@netcracker/qubership-apihub-json-crawl": "feature-support-oas-extension-changes-classification", "object-hash": "3.0.0", "fast-equals": "4.0.3" }, diff --git a/src/rules/openapi.ts b/src/rules/openapi.ts index 3baccd5..d4bd02b 100644 --- a/src/rules/openapi.ts +++ b/src/rules/openapi.ts @@ -244,21 +244,17 @@ const OPEN_API_COMPONENTS_REPLACES: Record = { [OPEN_API_PROPERTY_EXAMPLES]: TO_EMPTY_OBJECT_MAPPING, } -const openApiExtensionRulesFunction: (elseRules: NormalizationRules | (() => NormalizationRules)) => NormalizationRules = (elseRules) => ({ - '/*': (ctx) => { - const { key } = ctx - return typeof key === 'string' && key.startsWith('x-') - ? { - isExtension: true, - validate: checkType(...TYPE_JSON_ANY), - merge: resolvers.last, - '/**': { validate: checkType(...TYPE_JSON_ANY) }, - } - : typeof elseRules === 'function' ? elseRules() : elseRules - }, -}) - -const openApiExtensionRules: NormalizationRules = openApiExtensionRulesFunction({ validate: () => false }) +const openApiSpecificationExtensionRules = { + '/^': { + 'x-': { + isExtension: true, + validate: checkType(...TYPE_JSON_ANY), + merge: resolvers.last, + '/*': { validate: checkType(...TYPE_JSON_ANY) }, + '/**': { validate: checkType(...TYPE_JSON_ANY) }, + }, + }, +} as NormalizationRules const openApiExternalDocsRules: NormalizationRules = { '/externalDocs': { @@ -266,7 +262,7 @@ const openApiExternalDocsRules: NormalizationRules = { merge: resolvers.last, '/description': { validate: checkType(TYPE_STRING) }, '/url': { validate: checkType(TYPE_STRING) }, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, }, } @@ -282,9 +278,8 @@ const openApiExamplesRules: NormalizationRules = { validate: checkType(TYPE_OBJECT), merge: resolvers.last, '/*': { - ...openApiExtensionRulesFunction({ - validate: checkType(...TYPE_JSON_ANY), - }), + '/*': { validate: checkType(...TYPE_JSON_ANY) }, + ...openApiSpecificationExtensionRules, }, '/**': { validate: checkType(...TYPE_JSON_ANY) }, }, @@ -306,12 +301,12 @@ const openApiServerRules: NormalizationRules = { }, '/default': { validate: checkType(TYPE_STRING) }, '/description': { validate: checkType(TYPE_STRING) }, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, validate: checkType(TYPE_OBJECT), }, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), } @@ -350,7 +345,7 @@ const openApiLinksRules: NormalizationRules = { }, '/description': { validate: checkType(TYPE_STRING) }, '/server': openApiServerRules, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, validate: checkType(TYPE_OBJECT), @@ -363,8 +358,9 @@ const openApiJsonSchemaExtensionRules = (): NormalizationRules => ({ unify: [ valueDefaults(OPEN_API_XML_DEFAULTS), ], - ...openApiExtensionRulesFunction({ validate: checkType(...TYPE_JSON_ANY) }), + '/*': { validate: checkType(...TYPE_JSON_ANY) }, '/**': { validate: checkType(...TYPE_JSON_ANY) }, + ...openApiSpecificationExtensionRules, }, '/discriminator': { validate: checkType(TYPE_OBJECT), @@ -383,7 +379,7 @@ const openApiJsonSchemaExtensionRules = (): NormalizationRules => ({ }, }, ...openApiExternalDocsRules, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, }) const customFor30JsonSchemaRulesFactory = (): NormalizationRules => { @@ -488,11 +484,11 @@ const openApiMediaTypesRules = (version: OpenApiSpecVersion): NormalizationRules valueReplaces(OPEN_API_ENCODING_REPLACES), ], validate: checkType(TYPE_OBJECT), - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, }, validate: checkType(TYPE_OBJECT), }, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, unify: [ valueDefaults(OPEN_API_MEDIA_TYPE_DEFAULTS), valueReplaces(OPEN_API_MEDIA_TYPE_REPLACES), @@ -520,7 +516,7 @@ const openApiHeadersRules = (version: OpenApiSpecVersion): NormalizationRules => ...openApiExampleRules, ...openApiExamplesRules, '/schema': openApiJsonSchemaRules(version), - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), unify: [ valueDefaults(OPEN_API_HEADER_DEFAULTS), @@ -580,7 +576,7 @@ const openApiParametersRules = (version: OpenApiSpecVersion): NormalizationRules ...openApiJsonSchemaRules(version), newDataLayer: true, }), - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), unify: [ valueDefaults(OPEN_API_PARAMETER_DEFAULTS), @@ -596,7 +592,7 @@ const openApiRequestRules = (version: OpenApiSpecVersion): NormalizationRules => '/description': { validate: checkType(TYPE_STRING) }, '/required': { validate: checkType(TYPE_BOOLEAN) }, '/content': openApiMediaTypesRules(version), - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, unify: [ valueDefaults(OPEN_API_REQUEST_BODY_DEFAULTS), ], @@ -607,12 +603,12 @@ const openApiRequestRules = (version: OpenApiSpecVersion): NormalizationRules => }) const openApiResponsesRules = (version: OpenApiSpecVersion): NormalizationRules => ({ - ...openApiExtensionRulesFunction({ + '/*': { '/description': { validate: checkType(TYPE_STRING) }, '/headers': openApiHeadersRules(version), '/content': openApiMediaTypesRules(version), '/links': openApiLinksRules, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, unify: [ valueDefaults(OPEN_API_RESPONSE_DEFAULTS), valueReplaces(OPEN_API_RESPONSE_REPLACES), @@ -621,7 +617,8 @@ const openApiResponsesRules = (version: OpenApiSpecVersion): NormalizationRules deprecation: { inlineDescriptionSuffixCalculator: ctx => `${ctx.suffix} '${ctx.key.toString()}'`, }, - }), + }, + ...openApiSpecificationExtensionRules, deprecation: { inlineDescriptionSuffixCalculator: () => 'in response' }, validate: checkType(TYPE_OBJECT), }) @@ -634,7 +631,7 @@ const openApiPathItemRules = (version: OpenApiSpecVersion): NormalizationRules = '/*': openApiServerRules, validate: checkType(TYPE_ARRAY), }, - ...openApiExtensionRulesFunction({ + '/*': { deprecation: { deprecationResolver: (ctx) => OPEN_API_DEPRECATION_RESOLVER(ctx), descriptionCalculator: ctx => `[Deprecated] operation ${ctx.key.toString().toUpperCase()} ${ctx.suffix}`, @@ -648,8 +645,9 @@ const openApiPathItemRules = (version: OpenApiSpecVersion): NormalizationRules = ...openApiExternalDocsRules, '/operationId': { validate: checkType(TYPE_STRING) }, '/callbacks': { - '/*': { - ...openApiExtensionRulesFunction(() => openApiPathItemRules(version)), + '/*': { + '/*': () => openApiPathItemRules(version), + ...openApiSpecificationExtensionRules, }, }, '/deprecated': { validate: checkType(TYPE_BOOLEAN) }, @@ -658,13 +656,14 @@ const openApiPathItemRules = (version: OpenApiSpecVersion): NormalizationRules = '/parameters': openApiParametersRules(version), '/requestBody': openApiRequestRules(version), '/responses': openApiResponsesRules(version), - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, unify: [ valueDefaults(OPEN_API_OPERATION_DEFAULTS), valueReplaces(OPEN_API_OPERATION_REPLACES), ], validate: checkType(TYPE_OBJECT), - }), + }, + ...openApiSpecificationExtensionRules, '/parameters': openApiParametersRules(version), validate: checkType(TYPE_OBJECT), unify: pathItemsUnification, @@ -681,17 +680,17 @@ export const openApiRules = (version: OpenApiSpecVersion): NormalizationRules => '/name': { validate: checkType(TYPE_STRING) }, '/url': { validate: checkType(TYPE_STRING) }, '/email': { validate: checkType(TYPE_STRING) }, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, '/license': { '/name': { validate: checkType(TYPE_STRING) }, '/url': { validate: checkType(TYPE_STRING) }, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, '/version': { validate: checkType(TYPE_STRING) }, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, ...openApiExternalDocsRules, @@ -702,13 +701,14 @@ export const openApiRules = (version: OpenApiSpecVersion): NormalizationRules => '/name': { validate: checkType(TYPE_STRING) }, '/description': { validate: checkType(TYPE_STRING) }, ...openApiExternalDocsRules, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, validate: checkType(TYPE_ARRAY), }, '/paths': { - ...openApiExtensionRulesFunction(openApiPathItemRules(version)), + '/*': openApiPathItemRules(version), + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, '/components': { @@ -725,21 +725,21 @@ export const openApiRules = (version: OpenApiSpecVersion): NormalizationRules => '/authorizationUrl': { validate: checkType(TYPE_STRING) }, '/refreshUrl': { validate: checkType(TYPE_STRING) }, '/scopes': openApiOAuthScopesRules, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, '/password': { '/tokenUrl': { validate: checkType(TYPE_STRING) }, '/refreshUrl': { validate: checkType(TYPE_STRING) }, '/scopes': openApiOAuthScopesRules, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, '/clientCredentials': { '/tokenUrl': { validate: checkType(TYPE_STRING) }, '/refreshUrl': { validate: checkType(TYPE_STRING) }, '/scopes': openApiOAuthScopesRules, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, '/authorizationCode': { @@ -747,15 +747,15 @@ export const openApiRules = (version: OpenApiSpecVersion): NormalizationRules => '/tokenUrl': { validate: checkType(TYPE_STRING) }, '/refreshUrl': { validate: checkType(TYPE_STRING) }, '/scopes': openApiOAuthScopesRules, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), }, '/openIdConnectUrl': { validate: checkType(TYPE_STRING) }, validate: checkType(TYPE_OBJECT), - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, }, validate: checkType(TYPE_OBJECT), }, @@ -776,18 +776,19 @@ export const openApiRules = (version: OpenApiSpecVersion): NormalizationRules => '/headers': openApiHeadersRules(version), '/callbacks': { '/*': { - ...openApiExtensionRulesFunction(() => openApiPathItemRules(version)), + '/*': () => openApiPathItemRules(version), + ...openApiSpecificationExtensionRules, }, }, ...openApiExamplesRules, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, validate: checkType(TYPE_OBJECT), unify: [ valueDefaults(OPEN_API_COMPONENTS_DEFAULTS), valueReplaces(OPEN_API_COMPONENTS_REPLACES), ], }, - ...openApiExtensionRules, + ...openApiSpecificationExtensionRules, unify: [ valueDefaults(OPEN_API_ROOT_DEFAULTS), valueReplaces(OPEN_API_ROOT_REPLACES), diff --git a/test/oas/validate.test.ts b/test/oas/validate.test.ts index 4ca578f..1a99b4a 100644 --- a/test/oas/validate.test.ts +++ b/test/oas/validate.test.ts @@ -650,4 +650,23 @@ describe('OAS 3.1 Type validations: array of types', () => { expect(result).toHaveProperty([...schemaPath, 'type'], 'string') }) + + it('validates OAS extensions of Paths Object with complex values', () => { + const spec = { + openapi: "3.0.4", + info: { + title: "Test API", + version: "1.0.0" + }, + paths: { + "x-custom-extension": { + key: "value" + } + } + } + + const result = validate(spec, { validate: true, }) + + expect(result).toMatchObject(spec) + }) }) \ No newline at end of file