diff --git a/packages/apidom-ns-openapi-2/README.md b/packages/apidom-ns-openapi-2/README.md index a2c130e58e..2bf802a208 100644 --- a/packages/apidom-ns-openapi-2/README.md +++ b/packages/apidom-ns-openapi-2/README.md @@ -198,7 +198,7 @@ Only fully implemented specification objects should be checked here. - [ ] [Response Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-response-object) - [ ] [Headers Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-headers-object) - [x] [Example Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-example-object) -- [ ] [Header Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-header-object) +- [x] [Header Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-header-object) - [x] [Tag Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-tag-object) - [ ] [Reference Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-reference-object) - [ ] [Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#user-content-schema-object) diff --git a/packages/apidom-ns-openapi-2/src/elements/Header.ts b/packages/apidom-ns-openapi-2/src/elements/Header.ts new file mode 100644 index 0000000000..69c990d53a --- /dev/null +++ b/packages/apidom-ns-openapi-2/src/elements/Header.ts @@ -0,0 +1,281 @@ +import { + StringElement, + BooleanElement, + Attributes, + Meta, + NumberElement, + ArrayElement, + ObjectElement, +} from '@swagger-api/apidom-core'; +import { UnsupportedOperationError } from '@swagger-api/apidom-error'; +import { + JSONReferenceElement, + JSONSchemaElement, + MediaElement, +} from '@swagger-api/apidom-ns-json-schema-draft-4'; + +/* eslint-disable class-methods-use-this */ +class Header extends JSONSchemaElement { + constructor(content?: Record, meta?: Meta, attributes?: Attributes) { + super(content, meta, attributes); + this.element = 'header'; + this.classes.push('json-schema-draft-4'); + } + + /** + * Core vocabulary + * + * URI: https://tools.ietf.org/html/draft-wright-json-schema-00 + */ + + get idProp(): StringElement | undefined { + throw new UnsupportedOperationError('idProp getter in Header class is not not supported.'); + } + + set idProp(idProps: StringElement | undefined) { + throw new UnsupportedOperationError('idProp setter in Header class is not not supported.'); + } + + get $schema(): StringElement | undefined { + throw new UnsupportedOperationError('$schema getter in Header class is not not supported.'); + } + + set $schema($schema: StringElement | undefined) { + throw new UnsupportedOperationError('$schema setter in Header class is not not supported.'); + } + + /** + * Validation keywords for arrays + */ + + get additionalItems(): this | JSONReferenceElement | BooleanElement | undefined { + throw new UnsupportedOperationError( + 'additionalItems getter in Header class is not not supported.', + ); + } + + set additionalItems(additionalItems: this | JSONReferenceElement | BooleanElement | undefined) { + throw new UnsupportedOperationError( + 'additionalItems setter in Header class is not not supported.', + ); + } + + get items(): this | undefined { + return this.get('items'); + } + + set items(items: this | undefined) { + this.set('items', items); + } + + /** + * Validation keywords for objects + */ + + get maxProperties(): NumberElement | undefined { + throw new UnsupportedOperationError( + 'maxProperties getter in Header class is not not supported.', + ); + } + + set maxProperties(maxProperties: NumberElement | undefined) { + throw new UnsupportedOperationError( + 'maxProperties setter in Header class is not not supported.', + ); + } + + get minProperties(): NumberElement | undefined { + throw new UnsupportedOperationError( + 'minProperties getter in Header class is not not supported.', + ); + } + + set minProperties(minProperties: NumberElement | undefined) { + throw new UnsupportedOperationError( + 'minProperties setter in Header class is not not supported.', + ); + } + + get required(): ArrayElement | undefined { + throw new UnsupportedOperationError('required getter in Header class is not not supported.'); + } + + set required(required: ArrayElement | undefined) { + throw new UnsupportedOperationError('required setter in Header class is not not supported.'); + } + + get properties(): ObjectElement | undefined { + throw new UnsupportedOperationError('properties getter in Header class is not not supported.'); + } + + set properties(properties: ObjectElement | undefined) { + throw new UnsupportedOperationError('properties setter in Header class is not not supported.'); + } + + get additionalProperties(): this | JSONReferenceElement | BooleanElement | undefined { + throw new UnsupportedOperationError( + 'additionalProperties getter in Header class is not not supported.', + ); + } + + set additionalProperties( + additionalProperties: this | JSONReferenceElement | BooleanElement | undefined, + ) { + throw new UnsupportedOperationError( + 'additionalProperties setter in Header class is not not supported.', + ); + } + + get patternProperties(): ObjectElement | undefined { + throw new UnsupportedOperationError( + 'patternProperties getter in Header class is not not supported.', + ); + } + + set patternProperties(patternProperties: ObjectElement | undefined) { + throw new UnsupportedOperationError( + 'patternProperties setter in Header class is not not supported.', + ); + } + + get dependencies(): ObjectElement | undefined { + throw new UnsupportedOperationError( + 'dependencies getter in Header class is not not supported.', + ); + } + + set dependencies(dependencies: ObjectElement | undefined) { + throw new UnsupportedOperationError( + 'dependencies setter in Header class is not not supported.', + ); + } + + /** + * Validation keywords for any instance type + */ + + get type(): StringElement | undefined { + return this.get('type'); + } + + set type(type: StringElement | undefined) { + this.set('type', type); + } + + get allOf(): ArrayElement | undefined { + throw new UnsupportedOperationError('allOf getter in Header class is not not supported.'); + } + + set allOf(allOf: ArrayElement | undefined) { + throw new UnsupportedOperationError('allOf setter in Header class is not not supported.'); + } + + get anyOf(): ArrayElement | undefined { + throw new UnsupportedOperationError('anyOf getter in Header class is not not supported.'); + } + + set anyOf(anyOf: ArrayElement | undefined) { + throw new UnsupportedOperationError('anyOf setter in Header class is not not supported.'); + } + + get oneOf(): ArrayElement | undefined { + throw new UnsupportedOperationError('oneOf getter in Header class is not not supported.'); + } + + set oneOf(oneOf: ArrayElement | undefined) { + throw new UnsupportedOperationError('oneOf setter in Header class is not not supported.'); + } + + get not(): this | JSONReferenceElement | undefined { + throw new UnsupportedOperationError('not getter in Header class is not not supported.'); + } + + set not(not: this | JSONReferenceElement | undefined) { + throw new UnsupportedOperationError('not setter in Header class is not not supported.'); + } + + get definitions(): ObjectElement | undefined { + throw new UnsupportedOperationError('definitions getter in Header class is not not supported.'); + } + + set definitions(definitions: ObjectElement | undefined) { + throw new UnsupportedOperationError('definitions setter in Header class is not not supported.'); + } + + /** + * Metadata keywords + * + * URI: https://datatracker.ietf.org/doc/html/draft-wright-json-schema-validation-00#section-6 + */ + + get title(): StringElement | undefined { + throw new UnsupportedOperationError('title getter in Header class is not not supported.'); + } + + set title(title: StringElement | undefined) { + throw new UnsupportedOperationError('title setter in Header class is not not supported.'); + } + + get description(): StringElement | undefined { + return this.get('description'); + } + + set description(description: StringElement | undefined) { + this.set('description,', description); + } + + /** + * Semantic validation with "format" + * + * URI: https://datatracker.ietf.org/doc/html/draft-wright-json-schema-validation-00#section-7 + */ + + get format(): StringElement | undefined { + return this.get('format'); + } + + set format(format: StringElement | undefined) { + this.set('format', format); + } + + /** + * JSON Hyper-Schema + * + * URI: https://datatracker.ietf.org/doc/html/draft-wright-json-schema-hyperschema-00 + */ + + get base(): StringElement | undefined { + throw new UnsupportedOperationError('base getter in Header class is not not supported.'); + } + + set base(base: StringElement | undefined) { + throw new UnsupportedOperationError('base setter in Header class is not not supported.'); + } + + get links(): ArrayElement | undefined { + throw new UnsupportedOperationError('links getter in Header class is not not supported.'); + } + + set links(links: ArrayElement | undefined) { + throw new UnsupportedOperationError('links setter in Header class is not not supported.'); + } + + get media(): MediaElement | undefined { + throw new UnsupportedOperationError('media getter in Header class is not not supported.'); + } + + set media(media: MediaElement | undefined) { + throw new UnsupportedOperationError('media setter in Header class is not not supported.'); + } + + get readOnly(): BooleanElement | undefined { + throw new UnsupportedOperationError('readOnly getter in Header class is not not supported.'); + } + + set readOnly(readOnly: BooleanElement | undefined) { + throw new UnsupportedOperationError('readOnly setter in Header class is not not supported.'); + } +} +/* eslint-enable class-methods-use-this */ + +export default Header; diff --git a/packages/apidom-ns-openapi-2/src/index.ts b/packages/apidom-ns-openapi-2/src/index.ts index e3288e9abb..601c74ea0f 100644 --- a/packages/apidom-ns-openapi-2/src/index.ts +++ b/packages/apidom-ns-openapi-2/src/index.ts @@ -26,6 +26,7 @@ export { isExternalDocumentationElement, isItemsElement, isExampleElement, + isHeaderElement, isTagElement, isXmlElement, isSecurityDefinitionsElement, @@ -44,6 +45,7 @@ export { ExternalDocumentationElement, ItemsElement, ExampleElement, + HeaderElement, TagElement, XmlElement, SecurityDefinitionsElement, diff --git a/packages/apidom-ns-openapi-2/src/namespace.ts b/packages/apidom-ns-openapi-2/src/namespace.ts index b611613dcb..7912b9c2db 100644 --- a/packages/apidom-ns-openapi-2/src/namespace.ts +++ b/packages/apidom-ns-openapi-2/src/namespace.ts @@ -6,6 +6,7 @@ import ContactElement from './elements/Contact'; import ExternalDocumentation from './elements/ExternalDocumentation'; import ItemsElement from './elements/Items'; import ExampleElement from './elements/Example'; +import HeaderElement from './elements/Header'; import TagElement from './elements/Tag'; import XmlElement from './elements/Xml'; import SecurityDefinitionsElement from './elements/SecurityDefinitions'; @@ -23,6 +24,7 @@ const openApi2 = { base.register('externalDocumentation', ExternalDocumentation); base.register('items', ItemsElement); base.register('example', ExampleElement); + base.register('header', HeaderElement); base.register('tag', TagElement); base.register('xml', XmlElement); base.register('securityDefinitions', SecurityDefinitionsElement); diff --git a/packages/apidom-ns-openapi-2/src/predicates.ts b/packages/apidom-ns-openapi-2/src/predicates.ts index e8b7781337..7d985fd8a5 100644 --- a/packages/apidom-ns-openapi-2/src/predicates.ts +++ b/packages/apidom-ns-openapi-2/src/predicates.ts @@ -6,6 +6,7 @@ import ContactElement from './elements/Contact'; import ExternalDocumentation from './elements/ExternalDocumentation'; import ItemsElement from './elements/Items'; import ExampleElement from './elements/Example'; +import HeaderElement from './elements/Header'; import TagElement from './elements/Tag'; import XmlElement from './elements/Xml'; import SecurityDefinitionsElement from './elements/SecurityDefinitions'; @@ -72,6 +73,16 @@ export const isExampleElement = createPredicate( }, ); +export const isHeaderElement = createPredicate( + ({ hasBasicElementProps, isElementType, primitiveEq }) => { + return (element: unknown) => + element instanceof HeaderElement || + (hasBasicElementProps(element) && + isElementType('header', element) && + primitiveEq('object', element)); + }, +); + export const isTagElement = createPredicate( ({ hasBasicElementProps, isElementType, primitiveEq }) => { return (element: unknown) => diff --git a/packages/apidom-ns-openapi-2/src/refractor/registration.ts b/packages/apidom-ns-openapi-2/src/refractor/registration.ts index 967128d0c0..547831c809 100644 --- a/packages/apidom-ns-openapi-2/src/refractor/registration.ts +++ b/packages/apidom-ns-openapi-2/src/refractor/registration.ts @@ -4,6 +4,7 @@ import ContactElement from '../elements/Contact'; import ExternalDocumentationElement from '../elements/ExternalDocumentation'; import ItemsElement from '../elements/Items'; import ExampleElement from '../elements/Example'; +import HeaderElement from '../elements/Header'; import TagElement from '../elements/Tag'; import XmlElement from '../elements/Xml'; import SecurityDefinitionsElement from '../elements/SecurityDefinitions'; @@ -43,6 +44,7 @@ ExampleElement.refract = createRefractor([ 'Example', '$visitor', ]); +HeaderElement.refract = createRefractor(['visitors', 'document', 'objects', 'Header', '$visitor']); TagElement.refract = createRefractor(['visitors', 'document', 'objects', 'Tag', '$visitor']); XmlElement.refract = createRefractor(['visitors', 'document', 'objects', 'XML', '$visitor']); SecurityDefinitionsElement.refract = createRefractor([ @@ -75,6 +77,7 @@ export { ExternalDocumentationElement, ItemsElement, ExampleElement, + HeaderElement, TagElement, XmlElement, SecurityDefinitionsElement, diff --git a/packages/apidom-ns-openapi-2/src/refractor/specification.ts b/packages/apidom-ns-openapi-2/src/refractor/specification.ts index 5e66a54a50..3cd655f3e6 100644 --- a/packages/apidom-ns-openapi-2/src/refractor/specification.ts +++ b/packages/apidom-ns-openapi-2/src/refractor/specification.ts @@ -8,6 +8,7 @@ import ContactVisitor from './visitors/open-api-2/contact'; import ExternalDocumentationElement from './visitors/open-api-2/external-documentation'; import ItemsVisitor from './visitors/open-api-2/items'; import ExampleVisitor from './visitors/open-api-2/example'; +import HeaderVisitor from './visitors/open-api-2/header'; import TagVisitor from './visitors/open-api-2/tag'; import XmlVisitor from './visitors/open-api-2/xml'; import SecurityDefinitionsVisitor from './visitors/open-api-2/security-definitions'; @@ -97,6 +98,31 @@ const specification = { Example: { $visitor: ExampleVisitor, }, + Header: { + $visitor: HeaderVisitor, + fixedFields: { + description: jsonSchemaFixedFields.description, + type: jsonSchemaFixedFields.type, + format: jsonSchemaFixedFields.format, + items: { + $ref: '#/visitors/document/objects/Items', + }, + collectionFormat: FallbackVisitor, + default: jsonSchemaFixedFields.default, + maximum: jsonSchemaFixedFields.maximum, + exclusiveMaximum: jsonSchemaFixedFields.exclusiveMaximum, + minimum: jsonSchemaFixedFields.minimum, + exclusiveMinimum: jsonSchemaFixedFields.exclusiveMinimum, + maxLength: jsonSchemaFixedFields.maxLength, + minLength: jsonSchemaFixedFields.minLength, + pattern: jsonSchemaFixedFields.pattern, + maxItems: jsonSchemaFixedFields.maxItems, + minItems: jsonSchemaFixedFields.minItems, + uniqueItems: jsonSchemaFixedFields.uniqueItems, + enum: jsonSchemaFixedFields.enum, + multipleOf: jsonSchemaFixedFields.multipleOf, + }, + }, Tag: { $visitor: TagVisitor, fixedFields: { diff --git a/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/header/index.ts b/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/header/index.ts new file mode 100644 index 0000000000..c8f9590966 --- /dev/null +++ b/packages/apidom-ns-openapi-2/src/refractor/visitors/open-api-2/header/index.ts @@ -0,0 +1,18 @@ +import stampit from 'stampit'; +import { always } from 'ramda'; + +import HeaderElement from '../../../../elements/Header'; +import FallbackVisitor from '../../FallbackVisitor'; +import FixedFieldsVisitor from '../../generics/FixedFieldsVisitor'; + +const HeaderVisitor = stampit(FixedFieldsVisitor, FallbackVisitor, { + props: { + specPath: always(['document', 'objects', 'Header']), + canSupportSpecificationExtensions: true, + }, + init() { + this.element = new HeaderElement(); + }, +}); + +export default HeaderVisitor; diff --git a/packages/apidom-ns-openapi-2/src/traversal/visitor.ts b/packages/apidom-ns-openapi-2/src/traversal/visitor.ts index 9d1c25c4d2..a103fd7c50 100644 --- a/packages/apidom-ns-openapi-2/src/traversal/visitor.ts +++ b/packages/apidom-ns-openapi-2/src/traversal/visitor.ts @@ -25,6 +25,7 @@ export const keyMap = { ExternalDocumentationElement: ['content'], ItemsElement: ['content'], ExampleElement: ['content'], + HeaderElement: ['content'], TagElement: ['content'], XmlElement: ['content'], SecurityDefinitionsElement: ['content'], diff --git a/packages/apidom-ns-openapi-2/test/predicates.ts b/packages/apidom-ns-openapi-2/test/predicates.ts index e171c6d5dd..333b4711e1 100644 --- a/packages/apidom-ns-openapi-2/test/predicates.ts +++ b/packages/apidom-ns-openapi-2/test/predicates.ts @@ -7,6 +7,7 @@ import { ExternalDocumentationElement, ItemsElement, ExampleElement, + HeaderElement, TagElement, XmlElement, SecurityDefinitionsElement, @@ -19,6 +20,7 @@ import { isExternalDocumentationElement, isItemsElement, isExampleElement, + isHeaderElement, isTagElement, isXmlElement, isSecurityDefinitionsElement, @@ -370,6 +372,63 @@ describe('predicates', function () { }); }); + context('isHeaderElement', function () { + context('given HeaderElement instance value', function () { + specify('should return true', function () { + const element = new HeaderElement(); + + assert.isTrue(isHeaderElement(element)); + }); + }); + + context('given subtype instance value', function () { + specify('should return true', function () { + // eslint-disable-next-line @typescript-eslint/naming-convention + class HeaderSubElement extends HeaderElement {} + + assert.isTrue(isHeaderElement(new HeaderSubElement())); + }); + }); + + context('given non HeaderSubElement instance value', function () { + specify('should return false', function () { + assert.isFalse(isHeaderElement(1)); + assert.isFalse(isHeaderElement(null)); + assert.isFalse(isHeaderElement(undefined)); + assert.isFalse(isHeaderElement({})); + assert.isFalse(isHeaderElement([])); + assert.isFalse(isHeaderElement('string')); + }); + }); + + specify('should support duck-typing', function () { + const headerElementDuck = { + _storedElement: 'header', + _content: [], + primitive() { + return 'object'; + }, + get element() { + return this._storedElement; + }, + }; + + const headerElementSwan = { + _storedElement: undefined, + _content: undefined, + primitive() { + return 'swan'; + }, + get length() { + return 0; + }, + }; + + assert.isTrue(isHeaderElement(headerElementDuck)); + assert.isFalse(isHeaderElement(headerElementSwan)); + }); + }); + context('isTagElement', function () { context('given TagElement instance value', function () { specify('should return true', function () { diff --git a/packages/apidom-ns-openapi-2/test/refractor/elements/Header/__snapshots__/index.ts.snap b/packages/apidom-ns-openapi-2/test/refractor/elements/Header/__snapshots__/index.ts.snap new file mode 100644 index 0000000000..efc8c80c62 --- /dev/null +++ b/packages/apidom-ns-openapi-2/test/refractor/elements/Header/__snapshots__/index.ts.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`refractor elements HeaderElement should refract to semantic ApiDOM tree 1`] = ` +(HeaderElement + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (ItemsElement + (MemberElement + (StringElement) + (StringElement)) + (MemberElement + (StringElement) + (NumberElement)) + (MemberElement + (StringElement) + (NumberElement)) + (MemberElement + (StringElement) + (ItemsElement + (MemberElement + (StringElement) + (ItemsElement + (MemberElement + (StringElement) + (ItemsElement))))))))) +`; diff --git a/packages/apidom-ns-openapi-2/test/refractor/elements/Header/index.ts b/packages/apidom-ns-openapi-2/test/refractor/elements/Header/index.ts new file mode 100644 index 0000000000..b2fd6e8eb8 --- /dev/null +++ b/packages/apidom-ns-openapi-2/test/refractor/elements/Header/index.ts @@ -0,0 +1,43 @@ +import { assert, expect } from 'chai'; +import { includesClasses, sexprs } from '@swagger-api/apidom-core'; + +import { HeaderElement } from '../../../../src'; + +describe('refractor', function () { + context('elements', function () { + context('HeaderElement', function () { + specify('should refract to semantic ApiDOM tree', function () { + const headerElement = HeaderElement.refract({ + description: 'The number of allowed requests in the current period', + type: 'array', + items: { + type: 'integer', + minimum: 0, + maximum: 63, + items: { + items: { + items: {}, + }, + }, + }, + }); + + expect(sexprs(headerElement)).toMatchSnapshot(); + }); + + specify('should support specification extensions', function () { + const headerElement = HeaderElement.refract({ + type: 'array', + 'x-extension': 'extension', + }) as HeaderElement; + + assert.isFalse( + includesClasses(['specification-extension'], headerElement.getMember('type')), + ); + assert.isTrue( + includesClasses(['specification-extension'], headerElement.getMember('x-extension')), + ); + }); + }); + }); +});