From b3fc9d8b333afab8a214a5101ded2840336fbe81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Thu, 5 Oct 2023 13:32:33 +0200 Subject: [PATCH] feat(ns-openapi-2): add support for Security Scheme Object (#3227) Refs #3097 --- packages/apidom-ns-openapi-2/README.md | 4 +- .../src/elements/SecurityScheme.ts | 80 +++++++++++++++++++ packages/apidom-ns-openapi-2/src/index.ts | 13 ++- packages/apidom-ns-openapi-2/src/namespace.ts | 2 + .../apidom-ns-openapi-2/src/predicates.ts | 11 +++ .../src/refractor/registration.ts | 10 ++- .../src/refractor/specification.ts | 20 ++++- .../open-api-2/security-scheme/index.ts | 18 +++++ .../src/traversal/visitor.ts | 2 + .../apidom-ns-openapi-2/test/predicates.ts | 59 ++++++++++++++ .../__snapshots__/index.ts.snap | 35 ++++++++ .../elements/SecurityScheme/index.ts | 45 +++++++++++ 12 files changed, 291 insertions(+), 8 deletions(-) create mode 100644 packages/apidom-ns-openapi-2/src/elements/SecurityScheme.ts create mode 100644 packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/security-scheme/index.ts create mode 100644 packages/apidom-ns-openapi-2/test/refractor/elements/SecurityScheme/__snapshots__/index.ts.snap create mode 100644 packages/apidom-ns-openapi-2/test/refractor/elements/SecurityScheme/index.ts diff --git a/packages/apidom-ns-openapi-2/README.md b/packages/apidom-ns-openapi-2/README.md index b4db963bd2..ec777c0836 100644 --- a/packages/apidom-ns-openapi-2/README.md +++ b/packages/apidom-ns-openapi-2/README.md @@ -207,8 +207,8 @@ Only fully implemented specification objects should be checked here. - [ ] [Parameters Definitions Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-paramters-definitions-object) - [ ] [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) +- [x] [Security Scheme Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-security-scheme-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) +- [x] [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/SecurityScheme.ts b/packages/apidom-ns-openapi-2/src/elements/SecurityScheme.ts new file mode 100644 index 0000000000..95fda61509 --- /dev/null +++ b/packages/apidom-ns-openapi-2/src/elements/SecurityScheme.ts @@ -0,0 +1,80 @@ +import { + ObjectElement, + ArrayElement, + StringElement, + Attributes, + Meta, +} from '@swagger-api/apidom-core'; + +class SecurityScheme extends ObjectElement { + constructor(content?: Record, meta?: Meta, attributes?: Attributes) { + super(content, meta, attributes); + this.element = 'securityScheme'; + } + + get type(): StringElement | undefined { + return this.get('type'); + } + + set type(type: StringElement | undefined) { + this.set('type', type); + } + + get description(): StringElement | undefined { + return this.get('description'); + } + + set description(description: StringElement | undefined) { + this.set('description', description); + } + + get name(): StringElement | undefined { + return this.get('name'); + } + + set name(name: StringElement | undefined) { + this.set('name', name); + } + + get in(): StringElement | undefined { + return this.get('in'); + } + + set in(inVal: StringElement | undefined) { + this.set('in', inVal); + } + + get flow(): StringElement | undefined { + return this.get('flow'); + } + + set flow(flow: StringElement | undefined) { + this.set('flow', flow); + } + + get authorizationUrl(): StringElement | undefined { + return this.get('authorizationUrl'); + } + + set authorizationUrl(authorizationUrl: StringElement | undefined) { + this.set('authorizationUrl', authorizationUrl); + } + + get tokenUrl(): StringElement | undefined { + return this.get('tokenUrl'); + } + + set tokenUrl(tokenUrl: StringElement | undefined) { + this.set('tokenUrl', tokenUrl); + } + + get scopes(): ArrayElement | undefined { + return this.get('scopes'); + } + + set scopes(scopes: ArrayElement | undefined) { + this.set('scopes', scopes); + } +} + +export default SecurityScheme; diff --git a/packages/apidom-ns-openapi-2/src/index.ts b/packages/apidom-ns-openapi-2/src/index.ts index e274cad626..11743fc633 100644 --- a/packages/apidom-ns-openapi-2/src/index.ts +++ b/packages/apidom-ns-openapi-2/src/index.ts @@ -19,11 +19,18 @@ export { default } from './namespace'; export { default as refract, createRefractor } from './refractor'; export { default as specificationObj } from './refractor/specification'; -export { isScopesElement, isSecurityRequirementElement } from './predicates'; +export { + isSecuritySchemeElement, + isScopesElement, + isSecurityRequirementElement, +} from './predicates'; export { keyMap, getNodeType } from './traversal/visitor'; // OpenAPI 2.0 elements -export { ScopesElement } from './refractor/registration'; -export { SecurityRequirementElement } from './refractor/registration'; +export { + SecuritySchemeElement, + ScopesElement, + 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 ed7cd6ab40..48f45d164e 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 SecuritySchemeElement from './elements/SecurityScheme'; import ScopesElement from './elements/ScopesElement'; import SecurityRequirementElement from './elements/SecurityRequirement'; @@ -9,6 +10,7 @@ const openApi2 = { base.register('scopes', ScopesElement); base.register('securityRequirement', SecurityRequirementElement); + base.register('securityScheme', SecuritySchemeElement); return base; }, diff --git a/packages/apidom-ns-openapi-2/src/predicates.ts b/packages/apidom-ns-openapi-2/src/predicates.ts index 2c2c41b451..a2edb5eb23 100644 --- a/packages/apidom-ns-openapi-2/src/predicates.ts +++ b/packages/apidom-ns-openapi-2/src/predicates.ts @@ -1,8 +1,19 @@ import { createPredicate } from '@swagger-api/apidom-core'; +import SecuritySchemeElement from './elements/SecurityScheme'; import SecurityRequirementElement from './elements/SecurityRequirement'; import ScopesElement from './elements/ScopesElement'; +export const isSecuritySchemeElement = createPredicate( + ({ hasBasicElementProps, isElementType, primitiveEq }) => { + return (element: any) => + element instanceof SecuritySchemeElement || + (hasBasicElementProps(element) && + isElementType('securityScheme', element) && + primitiveEq('object', element)); + }, +); + export const isScopesElement = 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 dee6a60ba4..c52b4cc422 100644 --- a/packages/apidom-ns-openapi-2/src/refractor/registration.ts +++ b/packages/apidom-ns-openapi-2/src/refractor/registration.ts @@ -1,8 +1,16 @@ +import SecuritySchemeElement from '../elements/SecurityScheme'; import ScopesElement from '../elements/ScopesElement'; import SecurityRequirementElement from '../elements/SecurityRequirement'; import { createRefractor } from './index'; // register refractors specific to element types +SecuritySchemeElement.refract = createRefractor([ + 'visitors', + 'document', + 'objects', + 'SecurityScheme', + '$visitor', +]); ScopesElement.refract = createRefractor(['visitors', 'document', 'objects', 'Scopes', '$visitor']); SecurityRequirementElement.refract = createRefractor([ 'visitors', @@ -12,4 +20,4 @@ SecurityRequirementElement.refract = createRefractor([ '$visitor', ]); -export { ScopesElement, SecurityRequirementElement }; +export { SecuritySchemeElement, 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 e2dd4c3e1a..71841fe224 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 SecuritySchemeVisitor from './visitors/open-api-2/security-scheme'; import ScopesVisitor from './visitors/open-api-2/scopes'; import SecurityRequirementVisitor from './visitors/open-api-2/security-requirement'; import SpecificationExtensionVisitor from './visitors/SpecificationExtensionVisitor'; @@ -17,12 +18,27 @@ const specification = { value: FallbackVisitor, document: { objects: { - SecurityRequirement: { - $visitor: SecurityRequirementVisitor, + SecurityScheme: { + $visitor: SecuritySchemeVisitor, + fixedFields: { + type: FallbackVisitor, + description: FallbackVisitor, + name: FallbackVisitor, + in: FallbackVisitor, + flow: FallbackVisitor, + authorizationUrl: FallbackVisitor, + token: FallbackVisitor, + scopes: { + $ref: '#/visitors/document/objects/Scopes', + }, + }, }, Scopes: { $visitor: ScopesVisitor, }, + SecurityRequirement: { + $visitor: SecurityRequirementVisitor, + }, }, extension: { $visitor: SpecificationExtensionVisitor, diff --git a/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/security-scheme/index.ts b/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/security-scheme/index.ts new file mode 100644 index 0000000000..492c55a3e9 --- /dev/null +++ b/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/security-scheme/index.ts @@ -0,0 +1,18 @@ +import stampit from 'stampit'; +import { always } from 'ramda'; + +import SecuritySchemeElement from '../../../../elements/SecurityScheme'; +import FallbackVisitor from '../../FallbackVisitor'; +import FixedFieldsVisitor from '../../generics/FixedFieldsVisitor'; + +const SecuritySchemeVisitor = stampit(FixedFieldsVisitor, FallbackVisitor, { + props: { + specPath: always(['document', 'objects', 'SecurityScheme']), + canSupportSpecificationExtensions: true, + }, + init() { + this.element = new SecuritySchemeElement(); + }, +}); + +export default SecuritySchemeVisitor; diff --git a/packages/apidom-ns-openapi-2/src/traversal/visitor.ts b/packages/apidom-ns-openapi-2/src/traversal/visitor.ts index f72ed92d1b..f832541382 100644 --- a/packages/apidom-ns-openapi-2/src/traversal/visitor.ts +++ b/packages/apidom-ns-openapi-2/src/traversal/visitor.ts @@ -19,6 +19,8 @@ export const getNodeType = (element: T): string | undefined = */ export const keyMap = { + SecuritySchemeElement: ['content'], + ScopesElement: ['content'], SecurityRequirementElement: ['content'], ...keyMapBase, }; diff --git a/packages/apidom-ns-openapi-2/test/predicates.ts b/packages/apidom-ns-openapi-2/test/predicates.ts index add2053c49..f758664351 100644 --- a/packages/apidom-ns-openapi-2/test/predicates.ts +++ b/packages/apidom-ns-openapi-2/test/predicates.ts @@ -1,13 +1,72 @@ import { assert } from 'chai'; import { + SecuritySchemeElement, ScopesElement, SecurityRequirementElement, + isSecuritySchemeElement, isScopesElement, isSecurityRequirementElement, } from '../src'; describe('predicates', function () { + context('isSecuritySchemeElement', function () { + context('given SecuritySchemeElement instance value', function () { + specify('should return true', function () { + const element = new SecuritySchemeElement(); + + assert.isTrue(isSecuritySchemeElement(element)); + }); + }); + + context('given subtype instance value', function () { + specify('should return true', function () { + // eslint-disable-next-line @typescript-eslint/naming-convention + class SecuritySchemeSubElement extends SecuritySchemeElement {} + + assert.isTrue(isSecuritySchemeElement(new SecuritySchemeSubElement())); + }); + }); + + context('given non SecuritySchemeSubElement instance value', function () { + specify('should return false', function () { + assert.isFalse(isSecuritySchemeElement(1)); + assert.isFalse(isSecuritySchemeElement(null)); + assert.isFalse(isSecuritySchemeElement(undefined)); + assert.isFalse(isSecuritySchemeElement({})); + assert.isFalse(isSecuritySchemeElement([])); + assert.isFalse(isSecuritySchemeElement('string')); + }); + }); + + specify('should support duck-typing', function () { + const securitySchemeElementDuck = { + _storedElement: 'securityScheme', + _content: [], + primitive() { + return 'object'; + }, + get element() { + return this._storedElement; + }, + }; + + const securitySchemeElementSwan = { + _storedElement: undefined, + _content: undefined, + primitive() { + return 'swan'; + }, + get length() { + return 0; + }, + }; + + assert.isTrue(isSecuritySchemeElement(securitySchemeElementDuck)); + assert.isFalse(isSecuritySchemeElement(securitySchemeElementSwan)); + }); + }); + context('isScopesElement', function () { context('given ScopesElement instance value', function () { specify('should return true', function () { diff --git a/packages/apidom-ns-openapi-2/test/refractor/elements/SecurityScheme/__snapshots__/index.ts.snap b/packages/apidom-ns-openapi-2/test/refractor/elements/SecurityScheme/__snapshots__/index.ts.snap new file mode 100644 index 0000000000..5046a07778 --- /dev/null +++ b/packages/apidom-ns-openapi-2/test/refractor/elements/SecurityScheme/__snapshots__/index.ts.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`refractor elements SecuritySchemeElement should refract to semantic ApiDOM tree 1`] = ` +(SecuritySchemeElement + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (ScopesElement + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (StringElement))))) +`; diff --git a/packages/apidom-ns-openapi-2/test/refractor/elements/SecurityScheme/index.ts b/packages/apidom-ns-openapi-2/test/refractor/elements/SecurityScheme/index.ts new file mode 100644 index 0000000000..b858dc91d6 --- /dev/null +++ b/packages/apidom-ns-openapi-2/test/refractor/elements/SecurityScheme/index.ts @@ -0,0 +1,45 @@ +import { expect, assert } from 'chai'; +import { sexprs, includesClasses } from '@swagger-api/apidom-core'; + +import { SecuritySchemeElement } from '../../../../src'; + +describe('refractor', function () { + context('elements', function () { + context('SecuritySchemeElement', function () { + specify('should refract to semantic ApiDOM tree', function () { + const securitySchemeElement = SecuritySchemeElement.refract({ + type: 'apiKey', + description: 'simple description', + name: 'api_key', + in: 'header', + flow: 'implicit', + authorizationUrl: 'https://swagger.io/api/oauth/dialog', + tokenUrl: 'https://swagger.io/api/oauth/token-url', + scopes: { + 'write:pets': 'modify pets in your account', + 'read:pets': 'read your pets', + }, + }); + + expect(sexprs(securitySchemeElement)).toMatchSnapshot(); + }); + + specify('should support specification extensions', function () { + const securitySchemeElement = SecuritySchemeElement.refract({ + type: 'apiKey', + 'x-extension': 'extension', + }) as SecuritySchemeElement; + + assert.isFalse( + includesClasses(['specification-extension'], securitySchemeElement.getMember('type')), + ); + assert.isTrue( + includesClasses( + ['specification-extension'], + securitySchemeElement.getMember('x-extension'), + ), + ); + }); + }); + }); +});