From b12d915a8a56e46b7d7db4f516c595e442524f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Fri, 6 Oct 2023 15:08:07 +0200 Subject: [PATCH] feat(ns-openapi-2): add support for External Documentation Object (#3235) Refs #3097 --- packages/apidom-ns-openapi-2/README.md | 2 +- .../src/elements/ExternalDocumentation.ts | 26 ++++++++ packages/apidom-ns-openapi-2/src/index.ts | 2 + packages/apidom-ns-openapi-2/src/namespace.ts | 2 + .../apidom-ns-openapi-2/src/predicates.ts | 11 ++++ .../src/refractor/registration.ts | 9 +++ .../src/refractor/specification.ts | 8 +++ .../external-documentation/index.ts | 18 ++++++ .../src/traversal/visitor.ts | 1 + .../apidom-ns-openapi-2/test/predicates.ts | 59 +++++++++++++++++++ .../__snapshots__/index.ts.snap | 11 ++++ .../elements/ExternalDocumentation/index.ts | 39 ++++++++++++ 12 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 packages/apidom-ns-openapi-2/src/elements/ExternalDocumentation.ts create mode 100644 packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/external-documentation/index.ts create mode 100644 packages/apidom-ns-openapi-2/test/refractor/elements/ExternalDocumentation/__snapshots__/index.ts.snap create mode 100644 packages/apidom-ns-openapi-2/test/refractor/elements/ExternalDocumentation/index.ts diff --git a/packages/apidom-ns-openapi-2/README.md b/packages/apidom-ns-openapi-2/README.md index 2c8d3f78e5..c500fe69bf 100644 --- a/packages/apidom-ns-openapi-2/README.md +++ b/packages/apidom-ns-openapi-2/README.md @@ -191,7 +191,7 @@ Only fully implemented specification objects should be checked here. - [ ] [Paths Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-paths-object) - [ ] [Path Item Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-path-item-object) - [ ] [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-operation-object) -- [ ] [External Documentation Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-external-documentation-object) +- [x] [External Documentation Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-external-documentation-object) - [ ] [Parameter Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-parameter-object) - [ ] [Items Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-items-object) - [ ] [Responses Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-responses-object) diff --git a/packages/apidom-ns-openapi-2/src/elements/ExternalDocumentation.ts b/packages/apidom-ns-openapi-2/src/elements/ExternalDocumentation.ts new file mode 100644 index 0000000000..6e1644f6be --- /dev/null +++ b/packages/apidom-ns-openapi-2/src/elements/ExternalDocumentation.ts @@ -0,0 +1,26 @@ +import { StringElement, ObjectElement, Attributes, Meta } from '@swagger-api/apidom-core'; + +class ExternalDocumentation extends ObjectElement { + constructor(content?: Record, meta?: Meta, attributes?: Attributes) { + super(content, meta, attributes); + this.element = 'externalDocumentation'; + } + + get description(): StringElement | undefined { + return this.get('description'); + } + + set description(description: StringElement | undefined) { + this.set('description', description); + } + + get url(): StringElement | undefined { + return this.get('url'); + } + + set url(url: StringElement | undefined) { + this.set('url', url); + } +} + +export default ExternalDocumentation; diff --git a/packages/apidom-ns-openapi-2/src/index.ts b/packages/apidom-ns-openapi-2/src/index.ts index 17b8373d23..7688e200b2 100644 --- a/packages/apidom-ns-openapi-2/src/index.ts +++ b/packages/apidom-ns-openapi-2/src/index.ts @@ -20,6 +20,7 @@ export { default as refract, createRefractor } from './refractor'; export { default as specificationObj } from './refractor/specification'; export { + isExternalDocumentationElement, isXmlElement, isSecurityDefinitionsElement, isSecuritySchemeElement, @@ -31,6 +32,7 @@ export { keyMap, getNodeType } from './traversal/visitor'; // OpenAPI 2.0 elements export { + ExternalDocumentationElement, XmlElement, SecurityDefinitionsElement, SecuritySchemeElement, diff --git a/packages/apidom-ns-openapi-2/src/namespace.ts b/packages/apidom-ns-openapi-2/src/namespace.ts index 57d1a2884d..d4feccaabb 100644 --- a/packages/apidom-ns-openapi-2/src/namespace.ts +++ b/packages/apidom-ns-openapi-2/src/namespace.ts @@ -1,5 +1,6 @@ import { NamespacePluginOptions } from '@swagger-api/apidom-core'; +import ExternalDocumentation from './elements/ExternalDocumentation'; import XmlElement from './elements/Xml'; import SecurityDefinitionsElement from './elements/SecurityDefinitions'; import SecuritySchemeElement from './elements/SecurityScheme'; @@ -10,6 +11,7 @@ const openApi2 = { namespace: (options: NamespacePluginOptions) => { const { base } = options; + base.register('externalDocumentation', ExternalDocumentation); base.register('xml', XmlElement); base.register('securityDefinitions', SecurityDefinitionsElement); base.register('securityScheme', SecuritySchemeElement); diff --git a/packages/apidom-ns-openapi-2/src/predicates.ts b/packages/apidom-ns-openapi-2/src/predicates.ts index 536be42db2..62d253c3d2 100644 --- a/packages/apidom-ns-openapi-2/src/predicates.ts +++ b/packages/apidom-ns-openapi-2/src/predicates.ts @@ -1,11 +1,22 @@ import { createPredicate } from '@swagger-api/apidom-core'; +import ExternalDocumentation from './elements/ExternalDocumentation'; import XmlElement from './elements/Xml'; import SecurityDefinitionsElement from './elements/SecurityDefinitions'; import SecuritySchemeElement from './elements/SecurityScheme'; import SecurityRequirementElement from './elements/SecurityRequirement'; import ScopesElement from './elements/Scopes'; +export const isExternalDocumentationElement = createPredicate( + ({ hasBasicElementProps, isElementType, primitiveEq }) => { + return (element: any) => + element instanceof ExternalDocumentation || + (hasBasicElementProps(element) && + isElementType('externalDocumentation', element) && + primitiveEq('object', element)); + }, +); + export const isXmlElement = createPredicate( ({ hasBasicElementProps, isElementType, primitiveEq }) => { return (element: any) => diff --git a/packages/apidom-ns-openapi-2/src/refractor/registration.ts b/packages/apidom-ns-openapi-2/src/refractor/registration.ts index 9ffe5b1e34..79618a68a7 100644 --- a/packages/apidom-ns-openapi-2/src/refractor/registration.ts +++ b/packages/apidom-ns-openapi-2/src/refractor/registration.ts @@ -1,3 +1,4 @@ +import ExternalDocumentationElement from '../elements/ExternalDocumentation'; import XmlElement from '../elements/Xml'; import SecurityDefinitionsElement from '../elements/SecurityDefinitions'; import SecuritySchemeElement from '../elements/SecurityScheme'; @@ -6,6 +7,13 @@ import SecurityRequirementElement from '../elements/SecurityRequirement'; import { createRefractor } from './index'; // register refractors specific to element types +ExternalDocumentationElement.refract = createRefractor([ + 'visitors', + 'document', + 'objects', + 'ExternalDocumentation', + '$visitor', +]); XmlElement.refract = createRefractor(['visitors', 'document', 'objects', 'XML', '$visitor']); SecurityDefinitionsElement.refract = createRefractor([ 'visitors', @@ -31,6 +39,7 @@ SecurityRequirementElement.refract = createRefractor([ ]); export { + ExternalDocumentationElement, XmlElement, SecurityDefinitionsElement, SecuritySchemeElement, diff --git a/packages/apidom-ns-openapi-2/src/refractor/specification.ts b/packages/apidom-ns-openapi-2/src/refractor/specification.ts index 772e2ed556..cf76514b81 100644 --- a/packages/apidom-ns-openapi-2/src/refractor/specification.ts +++ b/packages/apidom-ns-openapi-2/src/refractor/specification.ts @@ -1,4 +1,5 @@ import FallbackVisitor from './visitors/FallbackVisitor'; +import ExternalDocumentationElement from './visitors/open-api-2/external-documentation'; import XmlVisitor from './visitors/open-api-2/xml'; import SecurityDefinitionsVisitor from './visitors/open-api-2/security-definitions'; import SecuritySchemeVisitor from './visitors/open-api-2/security-scheme'; @@ -20,6 +21,13 @@ const specification = { value: FallbackVisitor, document: { objects: { + ExternalDocumentation: { + $visitor: ExternalDocumentationElement, + fixedFields: { + description: FallbackVisitor, + url: FallbackVisitor, + }, + }, XML: { $visitor: XmlVisitor, fixedFields: { diff --git a/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/external-documentation/index.ts b/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/external-documentation/index.ts new file mode 100644 index 0000000000..ae87ae2f2c --- /dev/null +++ b/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/external-documentation/index.ts @@ -0,0 +1,18 @@ +import stampit from 'stampit'; +import { always } from 'ramda'; + +import ExternalDocumentationElement from '../../../../elements/ExternalDocumentation'; +import FixedFieldsVisitor from '../../generics/FixedFieldsVisitor'; +import FallbackVisitor from '../../FallbackVisitor'; + +const ExternalDocumentationVisitor = stampit(FixedFieldsVisitor, FallbackVisitor, { + props: { + specPath: always(['document', 'objects', 'ExternalDocumentation']), + canSupportSpecificationExtensions: true, + }, + init() { + this.element = new ExternalDocumentationElement(); + }, +}); + +export default ExternalDocumentationVisitor; diff --git a/packages/apidom-ns-openapi-2/src/traversal/visitor.ts b/packages/apidom-ns-openapi-2/src/traversal/visitor.ts index 33895b84e8..989a8930ce 100644 --- a/packages/apidom-ns-openapi-2/src/traversal/visitor.ts +++ b/packages/apidom-ns-openapi-2/src/traversal/visitor.ts @@ -19,6 +19,7 @@ export const getNodeType = (element: T): string | undefined = */ export const keyMap = { + ExternalDocumentationElement: ['content'], XmlElement: ['content'], SecurityDefinitionsElement: ['content'], SecuritySchemeElement: ['content'], diff --git a/packages/apidom-ns-openapi-2/test/predicates.ts b/packages/apidom-ns-openapi-2/test/predicates.ts index 42ba4c6db3..a884b22c79 100644 --- a/packages/apidom-ns-openapi-2/test/predicates.ts +++ b/packages/apidom-ns-openapi-2/test/predicates.ts @@ -1,11 +1,13 @@ import { assert } from 'chai'; import { + ExternalDocumentationElement, XmlElement, SecurityDefinitionsElement, SecuritySchemeElement, ScopesElement, SecurityRequirementElement, + isExternalDocumentationElement, isXmlElement, isSecurityDefinitionsElement, isSecuritySchemeElement, @@ -14,6 +16,63 @@ import { } from '../src'; describe('predicates', function () { + context('isExternalDocumentationElement', function () { + context('given ExternalDocumentationElement instance value', function () { + specify('should return true', function () { + const element = new ExternalDocumentationElement(); + + assert.isTrue(isExternalDocumentationElement(element)); + }); + }); + + context('given subtype instance value', function () { + specify('should return true', function () { + // eslint-disable-next-line @typescript-eslint/naming-convention + class ExternalDocumentationSubElement extends ExternalDocumentationElement {} + + assert.isTrue(isExternalDocumentationElement(new ExternalDocumentationSubElement())); + }); + }); + + context('given non ExternalDocumentationSubElement instance value', function () { + specify('should return false', function () { + assert.isFalse(isExternalDocumentationElement(1)); + assert.isFalse(isExternalDocumentationElement(null)); + assert.isFalse(isExternalDocumentationElement(undefined)); + assert.isFalse(isExternalDocumentationElement({})); + assert.isFalse(isExternalDocumentationElement([])); + assert.isFalse(isExternalDocumentationElement('string')); + }); + }); + + specify('should support duck-typing', function () { + const externalDocumentationElementDuck = { + _storedElement: 'externalDocumentation', + _content: [], + primitive() { + return 'object'; + }, + get element() { + return this._storedElement; + }, + }; + + const externalDocumentationElementSwan = { + _storedElement: undefined, + _content: undefined, + primitive() { + return 'swan'; + }, + get length() { + return 0; + }, + }; + + assert.isTrue(isExternalDocumentationElement(externalDocumentationElementDuck)); + assert.isFalse(isExternalDocumentationElement(externalDocumentationElementSwan)); + }); + }); + context('isXmlElement', function () { context('given XmlElement instance value', function () { specify('should return true', function () { diff --git a/packages/apidom-ns-openapi-2/test/refractor/elements/ExternalDocumentation/__snapshots__/index.ts.snap b/packages/apidom-ns-openapi-2/test/refractor/elements/ExternalDocumentation/__snapshots__/index.ts.snap new file mode 100644 index 0000000000..d4930307ad --- /dev/null +++ b/packages/apidom-ns-openapi-2/test/refractor/elements/ExternalDocumentation/__snapshots__/index.ts.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`refractor elements ExternalDocumentationElement should refract to semantic ApiDOM tree 1`] = ` +(ExternalDocumentationElement + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (StringElement))) +`; diff --git a/packages/apidom-ns-openapi-2/test/refractor/elements/ExternalDocumentation/index.ts b/packages/apidom-ns-openapi-2/test/refractor/elements/ExternalDocumentation/index.ts new file mode 100644 index 0000000000..d8b270b26e --- /dev/null +++ b/packages/apidom-ns-openapi-2/test/refractor/elements/ExternalDocumentation/index.ts @@ -0,0 +1,39 @@ +import { expect, assert } from 'chai'; +import { sexprs, includesClasses } from '@swagger-api/apidom-core'; + +import { ExternalDocumentationElement } from '../../../../src'; + +describe('refractor', function () { + context('elements', function () { + context('ExternalDocumentationElement', function () { + specify('should refract to semantic ApiDOM tree', function () { + const externalDocumentationElement = ExternalDocumentationElement.refract({ + description: 'Find more info here', + url: 'https://swagger.io', + }); + + expect(sexprs(externalDocumentationElement)).toMatchSnapshot(); + }); + + specify('should support specification extensions', function () { + const externalDocumentationElement = ExternalDocumentationElement.refract({ + description: 'Find more info here', + 'x-extension': 'extension', + }) as ExternalDocumentationElement; + + assert.isFalse( + includesClasses( + ['specification-extension'], + externalDocumentationElement.getMember('description'), + ), + ); + assert.isTrue( + includesClasses( + ['specification-extension'], + externalDocumentationElement.getMember('x-extension'), + ), + ); + }); + }); + }); +});