From 4226eab89e664c21951b08f974666ae89b17ec30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Thu, 5 Oct 2023 12:53:48 +0200 Subject: [PATCH] feat(ns-openapi-2): add support for Scopes Object (#3226) Refs #3097 --- packages/apidom-ns-openapi-2/README.md | 2 +- .../src/elements/ScopesElement.ts | 10 ++ packages/apidom-ns-openapi-2/src/index.ts | 3 +- packages/apidom-ns-openapi-2/src/namespace.ts | 2 + .../apidom-ns-openapi-2/src/predicates.ts | 12 +- .../src/refractor/registration.ts | 5 +- .../src/refractor/specification.ts | 4 + .../visitors/open-api-2/scopes/index.ts | 18 +++ .../apidom-ns-openapi-2/test/predicates.ts | 124 ++++++++++++++++++ .../Scopes/__snapshots__/index.ts.snap | 14 ++ .../test/refractor/elements/Scopes/index.ts | 35 +++++ 11 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 packages/apidom-ns-openapi-2/src/elements/ScopesElement.ts create mode 100644 packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/scopes/index.ts create mode 100644 packages/apidom-ns-openapi-2/test/predicates.ts create mode 100644 packages/apidom-ns-openapi-2/test/refractor/elements/Scopes/__snapshots__/index.ts.snap create mode 100644 packages/apidom-ns-openapi-2/test/refractor/elements/Scopes/index.ts diff --git a/packages/apidom-ns-openapi-2/README.md b/packages/apidom-ns-openapi-2/README.md index 81789361e..b4db963bd 100644 --- a/packages/apidom-ns-openapi-2/README.md +++ b/packages/apidom-ns-openapi-2/README.md @@ -208,7 +208,7 @@ Only fully implemented specification objects should be checked here. - [ ] [Responses Definitions Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-responses-definitions-object) - [ ] [Security Definitions Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-security-definitions-object) - [ ] [Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-security-scheme-object) -- [ ] [Scopes Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-scopes-object) +- [x] [Scopes Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-scopes-object) - [x] [Security Requirement Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-security-requirement-object) - [ ] [Specification extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-specification-extensions) diff --git a/packages/apidom-ns-openapi-2/src/elements/ScopesElement.ts b/packages/apidom-ns-openapi-2/src/elements/ScopesElement.ts new file mode 100644 index 000000000..129d2f1e0 --- /dev/null +++ b/packages/apidom-ns-openapi-2/src/elements/ScopesElement.ts @@ -0,0 +1,10 @@ +import { ObjectElement, Attributes, Meta } from '@swagger-api/apidom-core'; + +class Scopes extends ObjectElement { + constructor(content?: Record, meta?: Meta, attributes?: Attributes) { + super(content, meta, attributes); + this.element = 'scopes'; + } +} + +export default Scopes; diff --git a/packages/apidom-ns-openapi-2/src/index.ts b/packages/apidom-ns-openapi-2/src/index.ts index a831ef6ef..e274cad62 100644 --- a/packages/apidom-ns-openapi-2/src/index.ts +++ b/packages/apidom-ns-openapi-2/src/index.ts @@ -19,10 +19,11 @@ export { default } from './namespace'; export { default as refract, createRefractor } from './refractor'; export { default as specificationObj } from './refractor/specification'; -export { isSecurityRequirementElement } from './predicates'; +export { isScopesElement, isSecurityRequirementElement } from './predicates'; export { keyMap, getNodeType } from './traversal/visitor'; // OpenAPI 2.0 elements +export { ScopesElement } from './refractor/registration'; export { SecurityRequirementElement } from './refractor/registration'; // NCE types diff --git a/packages/apidom-ns-openapi-2/src/namespace.ts b/packages/apidom-ns-openapi-2/src/namespace.ts index c8b635d33..ed7cd6ab4 100644 --- a/packages/apidom-ns-openapi-2/src/namespace.ts +++ b/packages/apidom-ns-openapi-2/src/namespace.ts @@ -1,11 +1,13 @@ import { NamespacePluginOptions } from '@swagger-api/apidom-core'; +import ScopesElement from './elements/ScopesElement'; import SecurityRequirementElement from './elements/SecurityRequirement'; const openApi2 = { namespace: (options: NamespacePluginOptions) => { const { base } = options; + base.register('scopes', ScopesElement); base.register('securityRequirement', SecurityRequirementElement); return base; diff --git a/packages/apidom-ns-openapi-2/src/predicates.ts b/packages/apidom-ns-openapi-2/src/predicates.ts index a8c49b5e4..2c2c41b45 100644 --- a/packages/apidom-ns-openapi-2/src/predicates.ts +++ b/packages/apidom-ns-openapi-2/src/predicates.ts @@ -1,8 +1,18 @@ import { createPredicate } from '@swagger-api/apidom-core'; import SecurityRequirementElement from './elements/SecurityRequirement'; +import ScopesElement from './elements/ScopesElement'; + +export const isScopesElement = createPredicate( + ({ hasBasicElementProps, isElementType, primitiveEq }) => { + return (element: any) => + element instanceof ScopesElement || + (hasBasicElementProps(element) && + isElementType('scopes', element) && + primitiveEq('object', element)); + }, +); -// eslint-disable-next-line import/prefer-default-export export const isSecurityRequirementElement = 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 6fddd4f42..dee6a60ba 100644 --- a/packages/apidom-ns-openapi-2/src/refractor/registration.ts +++ b/packages/apidom-ns-openapi-2/src/refractor/registration.ts @@ -1,7 +1,9 @@ +import ScopesElement from '../elements/ScopesElement'; import SecurityRequirementElement from '../elements/SecurityRequirement'; import { createRefractor } from './index'; // register refractors specific to element types +ScopesElement.refract = createRefractor(['visitors', 'document', 'objects', 'Scopes', '$visitor']); SecurityRequirementElement.refract = createRefractor([ 'visitors', 'document', @@ -10,5 +12,4 @@ SecurityRequirementElement.refract = createRefractor([ '$visitor', ]); -// eslint-disable-next-line import/prefer-default-export -export { SecurityRequirementElement }; +export { ScopesElement, SecurityRequirementElement }; diff --git a/packages/apidom-ns-openapi-2/src/refractor/specification.ts b/packages/apidom-ns-openapi-2/src/refractor/specification.ts index 56b5180e2..e2dd4c3e1 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 ScopesVisitor from './visitors/open-api-2/scopes'; import SecurityRequirementVisitor from './visitors/open-api-2/security-requirement'; import SpecificationExtensionVisitor from './visitors/SpecificationExtensionVisitor'; @@ -19,6 +20,9 @@ const specification = { SecurityRequirement: { $visitor: SecurityRequirementVisitor, }, + Scopes: { + $visitor: ScopesVisitor, + }, }, extension: { $visitor: SpecificationExtensionVisitor, diff --git a/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/scopes/index.ts b/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/scopes/index.ts new file mode 100644 index 000000000..679da8416 --- /dev/null +++ b/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/scopes/index.ts @@ -0,0 +1,18 @@ +import stampit from 'stampit'; +import { always } from 'ramda'; + +import ScopesElement from '../../../../elements/ScopesElement'; +import MapVisitor from '../../generics/MapVisitor'; +import FallbackVisitor from '../../FallbackVisitor'; + +const ScopesVisitor = stampit(MapVisitor, FallbackVisitor, { + props: { + specPath: always(['value']), + canSupportSpecificationExtensions: true, + }, + init() { + this.element = new ScopesElement(); + }, +}); + +export default ScopesVisitor; diff --git a/packages/apidom-ns-openapi-2/test/predicates.ts b/packages/apidom-ns-openapi-2/test/predicates.ts new file mode 100644 index 000000000..add2053c4 --- /dev/null +++ b/packages/apidom-ns-openapi-2/test/predicates.ts @@ -0,0 +1,124 @@ +import { assert } from 'chai'; + +import { + ScopesElement, + SecurityRequirementElement, + isScopesElement, + isSecurityRequirementElement, +} from '../src'; + +describe('predicates', function () { + context('isScopesElement', function () { + context('given ScopesElement instance value', function () { + specify('should return true', function () { + const element = new ScopesElement(); + + assert.isTrue(isScopesElement(element)); + }); + }); + + context('given subtype instance value', function () { + specify('should return true', function () { + // eslint-disable-next-line @typescript-eslint/naming-convention + class ScopesSubElement extends ScopesElement {} + + assert.isTrue(isScopesElement(new ScopesSubElement())); + }); + }); + + context('given non ScopesSubElement instance value', function () { + specify('should return false', function () { + assert.isFalse(isScopesElement(1)); + assert.isFalse(isScopesElement(null)); + assert.isFalse(isScopesElement(undefined)); + assert.isFalse(isScopesElement({})); + assert.isFalse(isScopesElement([])); + assert.isFalse(isScopesElement('string')); + }); + }); + + specify('should support duck-typing', function () { + const scopesElementDuck = { + _storedElement: 'scopes', + _content: [], + primitive() { + return 'object'; + }, + get element() { + return this._storedElement; + }, + }; + + const scopesElementSwan = { + _storedElement: undefined, + _content: undefined, + primitive() { + return 'swan'; + }, + get length() { + return 0; + }, + }; + + assert.isTrue(isScopesElement(scopesElementDuck)); + assert.isFalse(isScopesElement(scopesElementSwan)); + }); + }); + + context('isSecurityRequirementElement', function () { + context('given SecurityRequirementElement instance value', function () { + specify('should return true', function () { + const element = new SecurityRequirementElement(); + + assert.isTrue(isSecurityRequirementElement(element)); + }); + }); + + context('given subtype instance value', function () { + specify('should return true', function () { + // eslint-disable-next-line @typescript-eslint/naming-convention + class SecurityRequirementSubElement extends SecurityRequirementElement {} + + assert.isTrue(isSecurityRequirementElement(new SecurityRequirementSubElement())); + }); + }); + + context('given non SecurityRequirementSubElement instance value', function () { + specify('should return false', function () { + assert.isFalse(isSecurityRequirementElement(1)); + assert.isFalse(isSecurityRequirementElement(null)); + assert.isFalse(isSecurityRequirementElement(undefined)); + assert.isFalse(isSecurityRequirementElement({})); + assert.isFalse(isSecurityRequirementElement([])); + assert.isFalse(isSecurityRequirementElement('string')); + }); + }); + + specify('should support duck-typing', function () { + const securityRequirementElementDuck = { + _storedElement: 'securityRequirement', + _content: [], + primitive() { + return 'object'; + }, + get element() { + return this._storedElement; + }, + }; + + const securityRequirementElementSwan = { + _storedElement: undefined, + _content: undefined, + primitive() { + return 'swan'; + }, + get length() { + return 0; + }, + }; + + assert.isTrue(isSecurityRequirementElement(securityRequirementElementDuck)); + assert.isFalse(isSecurityRequirementElement(securityRequirementElementSwan)); + }); + }); +}); diff --git a/packages/apidom-ns-openapi-2/test/refractor/elements/Scopes/__snapshots__/index.ts.snap b/packages/apidom-ns-openapi-2/test/refractor/elements/Scopes/__snapshots__/index.ts.snap new file mode 100644 index 000000000..b5f5a0c22 --- /dev/null +++ b/packages/apidom-ns-openapi-2/test/refractor/elements/Scopes/__snapshots__/index.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`refractor elements ScopesElement should refract to semantic ApiDOM tree 1`] = ` +(ScopesElement + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (StringElement))) +`; diff --git a/packages/apidom-ns-openapi-2/test/refractor/elements/Scopes/index.ts b/packages/apidom-ns-openapi-2/test/refractor/elements/Scopes/index.ts new file mode 100644 index 000000000..5f72e15f4 --- /dev/null +++ b/packages/apidom-ns-openapi-2/test/refractor/elements/Scopes/index.ts @@ -0,0 +1,35 @@ +import { expect, assert } from 'chai'; +import { sexprs, includesClasses } from '@swagger-api/apidom-core'; + +import { ScopesElement } from '../../../../src'; + +describe('refractor', function () { + context('elements', function () { + context('ScopesElement', function () { + specify('should refract to semantic ApiDOM tree', function () { + const scopesElement = ScopesElement.refract({ + 'write:pets': 'modify pets in your account', + 'read:pets': 'read your pets', + 'x-extension': 'extension', + }); + + expect(sexprs(scopesElement)).toMatchSnapshot(); + }); + + specify('should support specification extensions', function () { + const scopesElement = ScopesElement.refract({ + 'write:pets': 'modify pets in your account', + 'read:pets': 'read your pets', + 'x-extension': 'extension', + }) as ScopesElement; + + assert.isFalse( + includesClasses(['specification-extension'], scopesElement.getMember('write:pets')), + ); + assert.isTrue( + includesClasses(['specification-extension'], scopesElement.getMember('x-extension')), + ); + }); + }); + }); +});