diff --git a/apidom/packages/@types/minim.d.ts b/apidom/packages/@types/minim.d.ts index 665b4e9196..a19586e15c 100644 --- a/apidom/packages/@types/minim.d.ts +++ b/apidom/packages/@types/minim.d.ts @@ -64,6 +64,8 @@ declare module 'minim' { contains(value: any): boolean; push(value: any): ArrayElement; + + [Symbol.iterator](): IterableIterator; } export class ObjectElement extends ArrayElement { diff --git a/apidom/packages/apidom-ns-openapi3-1/src/elements/Operation.ts b/apidom/packages/apidom-ns-openapi3-1/src/elements/Operation.ts new file mode 100644 index 0000000000..8dfa383c3c --- /dev/null +++ b/apidom/packages/apidom-ns-openapi3-1/src/elements/Operation.ts @@ -0,0 +1,10 @@ +import { Attributes, Meta, ObjectElement } from 'minim'; + +class Operation extends ObjectElement { + constructor(content?: Array, meta?: Meta, attributes?: Attributes) { + super(content, meta, attributes); + this.element = 'operation'; + } +} + +export default Operation; diff --git a/apidom/packages/apidom-ns-openapi3-1/src/elements/PathItem.ts b/apidom/packages/apidom-ns-openapi3-1/src/elements/PathItem.ts new file mode 100644 index 0000000000..d81d3d39eb --- /dev/null +++ b/apidom/packages/apidom-ns-openapi3-1/src/elements/PathItem.ts @@ -0,0 +1,62 @@ +import { Attributes, Meta, ObjectElement, StringElement } from 'minim'; + +import ServerElement from './Server'; +import OperationElement from './Operation'; + +class PathItem extends ObjectElement { + constructor(content?: Array, meta?: Meta, attributes?: Attributes) { + super(content, meta, attributes); + this.element = 'pathItem'; + } + + get $ref(): StringElement { + return this.get('$ref'); + } + + get summary(): StringElement { + return this.get('summary'); + } + + get description(): StringElement { + return this.get('description'); + } + + get GET(): OperationElement { + return this.get('get'); + } + + get PUT(): OperationElement { + return this.get('put'); + } + + get POST(): OperationElement { + return this.get('post'); + } + + get DELETE(): OperationElement { + return this.get('delete'); + } + + get OPTIONS(): OperationElement { + return this.get('options'); + } + + get HEAD(): OperationElement { + return this.get('head'); + } + + get PATCH(): OperationElement { + return this.get('patch'); + } + + get TRACE(): OperationElement { + return this.get('trace'); + } + + get servers(): ServerElement[] { + return this.get('servers'); + } + + // @todo(vladimir.gorej@gmail.com): implement `parameters` field here +} +export default PathItem; diff --git a/apidom/packages/apidom-ns-openapi3-1/src/elements/Paths.ts b/apidom/packages/apidom-ns-openapi3-1/src/elements/Paths.ts new file mode 100644 index 0000000000..ddb2451881 --- /dev/null +++ b/apidom/packages/apidom-ns-openapi3-1/src/elements/Paths.ts @@ -0,0 +1,10 @@ +import { Attributes, Meta, ObjectElement } from 'minim'; + +class Paths extends ObjectElement { + constructor(content?: Array, meta?: Meta, attributes?: Attributes) { + super(content, meta, attributes); + this.element = 'paths'; + } +} + +export default Paths; diff --git a/apidom/packages/apidom-ns-openapi3-1/src/index.ts b/apidom/packages/apidom-ns-openapi3-1/src/index.ts index 31ef875944..c005faac19 100644 --- a/apidom/packages/apidom-ns-openapi3-1/src/index.ts +++ b/apidom/packages/apidom-ns-openapi3-1/src/index.ts @@ -23,6 +23,8 @@ export { isOpenapiElement, isServerElement, isServerVariableElement, + isPathsElement, + isPathItemElement, } from './predicates'; export { default as ComponentsElement } from './elements/Components'; @@ -34,3 +36,6 @@ export { default as OpenApi3_1Element } from './elements/OpenApi3-1'; export { default as SchemaElement } from './elements/Schema'; export { default as ServerElement } from './elements/Server'; export { default as ServerVariableElement } from './elements/ServerVariable'; +export { default as PathsElement } from './elements/Paths'; +export { default as PathItemElement } from './elements/PathItem'; +export { default as OperationElement } from './elements/Operation'; diff --git a/apidom/packages/apidom-ns-openapi3-1/src/namespace.ts b/apidom/packages/apidom-ns-openapi3-1/src/namespace.ts index 4f5b757b15..38021a5144 100644 --- a/apidom/packages/apidom-ns-openapi3-1/src/namespace.ts +++ b/apidom/packages/apidom-ns-openapi3-1/src/namespace.ts @@ -8,6 +8,9 @@ import Components from './elements/Components'; import Schema from './elements/Schema'; import Server from './elements/Server'; import ServerVariable from './elements/ServerVariable'; +import Paths from './elements/Paths'; +import PathItem from './elements/PathItem'; +import Operation from './elements/Operation'; const openApi3_1 = { namespace: (options: NamespacePluginOptions) => { @@ -22,6 +25,9 @@ const openApi3_1 = { base.register('schema', Schema); base.register('server', Server); base.register('serverVariable', ServerVariable); + base.register('paths', Paths); + base.register('pathItem', PathItem); + base.register('operation', Operation); return base; }, diff --git a/apidom/packages/apidom-ns-openapi3-1/src/predicates.ts b/apidom/packages/apidom-ns-openapi3-1/src/predicates.ts index 74bd1c62b5..8f66ea4fb0 100644 --- a/apidom/packages/apidom-ns-openapi3-1/src/predicates.ts +++ b/apidom/packages/apidom-ns-openapi3-1/src/predicates.ts @@ -10,6 +10,8 @@ import OpenApi3_1Element from './elements/OpenApi3-1'; import SchemaElement from './elements/Schema'; import ServerElement from './elements/Server'; import ServerVariableElement from './elements/ServerVariable'; +import PathsElement from './elements/Paths'; +import PathItemElement from './elements/PathItem'; export const isOpenApiApi3_1Element = createPredicate( ({ hasBasicElementProps, isElementType, primitiveEq, hasGetter, hasClass }) => { @@ -192,3 +194,53 @@ export const isServerVariableElement = createPredicate( ); }, ); + +export const isPathsElement = createPredicate( + ({ hasBasicElementProps, isElementType, primitiveEq }) => { + const isElementTypeInfo = isElementType('paths'); + const primitiveEqObject = primitiveEq('object'); + + return either( + is(PathsElement), + allPass([hasBasicElementProps, isElementTypeInfo, primitiveEqObject]), + ); + }, +); + +export const isPathItemElement = createPredicate( + ({ hasBasicElementProps, isElementType, primitiveEq, hasGetter }) => { + const isElementTypeInfo = isElementType('pathItem'); + const primitiveEqObject = primitiveEq('object'); + const hasGetter$Ref = hasGetter('$ref'); + const hasGetterSummary = hasGetter('summary'); + const hasGetterDescription = hasGetter('description'); + const hasGetterGET = hasGetter('GET'); + const hasGetterPUT = hasGetter('PUT'); + const hasGetterPOST = hasGetter('POST'); + const hasGetterDELETE = hasGetter('DELETE'); + const hasGetterOPTIONS = hasGetter('OPTIONS'); + const hasGetterHEAD = hasGetter('HEAD'); + const hasGetterPATCH = hasGetter('PATCH'); + const hasGetterTRACE = hasGetter('TRACE'); + + return either( + is(PathItemElement), + allPass([ + hasBasicElementProps, + isElementTypeInfo, + primitiveEqObject, + hasGetter$Ref, + hasGetterSummary, + hasGetterDescription, + hasGetterGET, + hasGetterPUT, + hasGetterPOST, + hasGetterDELETE, + hasGetterOPTIONS, + hasGetterHEAD, + hasGetterPATCH, + hasGetterTRACE, + ]), + ); + }, +); diff --git a/apidom/packages/apidom-ns-openapi3-1/test/predicates.ts b/apidom/packages/apidom-ns-openapi3-1/test/predicates.ts index 87f27e22c6..d4c08c4967 100644 --- a/apidom/packages/apidom-ns-openapi3-1/test/predicates.ts +++ b/apidom/packages/apidom-ns-openapi3-1/test/predicates.ts @@ -11,6 +11,8 @@ import { isOpenApiApi3_1Element, isServerElement, isServerVariableElement, + isPathsElement, + isPathItemElement, OpenApi3_1Element, OpenapiElement, SchemaElement, @@ -20,6 +22,8 @@ import { ContactElement, ServerElement, ServerVariableElement, + PathsElement, + PathItemElement, } from '../src'; describe('predicates', function () { @@ -551,4 +555,140 @@ describe('predicates', function () { assert.isFalse(isServerVariableElement(serverVariableElementSwan)); }); }); + + context('isPathsElement', function () { + context('given PathsElement instance value', function () { + specify('should return true', function () { + const element = new PathsElement(); + + assert.isTrue(isPathsElement(element)); + }); + }); + + context('given subtype instance value', function () { + specify('should return true', function () { + class PathsSubElement extends PathsElement {} + + assert.isTrue(isPathsElement(new PathsSubElement())); + }); + }); + + context('given non PathsElement instance value', function () { + specify('should return false', function () { + assert.isFalse(isPathsElement(1)); + assert.isFalse(isPathsElement(null)); + assert.isFalse(isPathsElement(undefined)); + assert.isFalse(isPathsElement({})); + assert.isFalse(isPathsElement([])); + assert.isFalse(isPathsElement('string')); + }); + }); + + specify('should support duck-typing', function () { + const pathsElementDuck = { + element: 'paths', + content: [], + primitive() { + return 'object'; + }, + }; + + const pathsElementSwan = { + element: undefined, + content: undefined, + primitive() { + return 'swan'; + }, + }; + + assert.isTrue(isPathsElement(pathsElementDuck)); + assert.isFalse(isPathsElement(pathsElementSwan)); + }); + }); + + context('isPathItemElement', function () { + context('given PathItemElement instance value', function () { + specify('should return true', function () { + const element = new PathItemElement(); + + assert.isTrue(isPathItemElement(element)); + }); + }); + + context('given subtype instance value', function () { + specify('should return true', function () { + class PathItemSubElement extends PathItemElement {} + + assert.isTrue(isPathItemElement(new PathItemSubElement())); + }); + }); + + context('given non PathItemElement instance value', function () { + specify('should return false', function () { + assert.isFalse(isPathItemElement(1)); + assert.isFalse(isPathItemElement(null)); + assert.isFalse(isPathItemElement(undefined)); + assert.isFalse(isPathItemElement({})); + assert.isFalse(isPathItemElement([])); + assert.isFalse(isPathItemElement('string')); + }); + }); + + specify('should support duck-typing', function () { + const pathItemElementDuck = { + element: 'pathItem', + content: [], + primitive() { + return 'object'; + }, + get $ref() { + return '$ref'; + }, + get summary() { + return 'summary'; + }, + get description() { + return 'description'; + }, + get GET() { + return 'get'; + }, + get PUT() { + return 'put'; + }, + get POST() { + return 'post'; + }, + get DELETE() { + return 'delete'; + }, + get OPTIONS() { + return 'options'; + }, + get HEAD() { + return 'head'; + }, + get PATCH() { + return 'patch'; + }, + get TRACE() { + return 'trace'; + }, + get servers() { + return 'servers'; + }, + }; + + const pathItemElementSwan = { + element: undefined, + content: undefined, + primitive() { + return 'swan'; + }, + }; + + assert.isTrue(isPathItemElement(pathItemElementDuck)); + assert.isFalse(isPathItemElement(pathItemElementSwan)); + }); + }); }); diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/specification.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/specification.ts index 2024f1ba1a..533ce5a85b 100644 --- a/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/specification.ts +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/specification.ts @@ -28,6 +28,11 @@ import ServerVariableDescriptionVisitor from './visitors/open-api-3-1/server-var import ComponentsVisitor from './visitors/open-api-3-1/components'; import SchemasVisitor from './visitors/open-api-3-1/components/SchemasVisitor'; import SchemaVisitor from './visitors/open-api-3-1/SchemaVisitor'; +import PathsVisitor from './visitors/open-api-3-1/PathsVisitor'; +import PathItemVisitor from './visitors/open-api-3-1/path-item'; +import PathItem$RefVisitor from './visitors/open-api-3-1/path-item/$RefVisitor'; +import PathItemSummaryVisitor from './visitors/open-api-3-1/path-item/SummaryVisitor'; +import PathItemDescriptionVisitor from './visitors/open-api-3-1/path-item/DescriptionVisitor'; import ErrorVisitor from './visitors/ErrorVisitor'; import { ValueVisitor, ObjectVisitor, ArrayVisitor } from './visitors/generics'; @@ -59,6 +64,9 @@ const specification = { components: { $ref: '#/visitors/document/objects/Components', }, + paths: { + $ref: '#/visitors/document/objects/Paths', + }, }, }, Info: { @@ -118,6 +126,17 @@ const specification = { schemas: SchemasVisitor, }, }, + Paths: { + $visitor: PathsVisitor, + }, + PathItem: { + $visitor: PathItemVisitor, + fields: { + $ref: PathItem$RefVisitor, + summary: PathItemSummaryVisitor, + description: PathItemDescriptionVisitor, + }, + }, }, extension: SpecificationExtensionVisitor, }, diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/PathsVisitor.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/PathsVisitor.ts new file mode 100644 index 0000000000..9ca9883a33 --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/PathsVisitor.ts @@ -0,0 +1,45 @@ +import stampit from 'stampit'; +import { BREAK } from '..'; +import SpecificationVisitor from '../SpecificationVisitor'; +import { isOpenApiExtension } from '../../predicates'; + +const PathsVisitor = stampit(SpecificationVisitor, { + props: { + keyElement: null, + }, + methods: { + key(keyNode) { + this.keyElement = this.maybeAddSourceMap( + keyNode, + new this.namespace.elements.String('paths'), + ); + }, + + object(objectNode) { + const pathsElement = new this.namespace.elements.Paths(); + const { MemberElement } = this.namespace.elements.Element.prototype; + + // @ts-ignore + objectNode.properties.forEach((propertyNode) => { + if (isOpenApiExtension({}, propertyNode)) { + pathsElement.content.push( + this.mapPropertyNodeToMemberElement(['document', 'extension'], propertyNode), + ); + } else { + pathsElement.content.push( + this.mapPropertyNodeToMemberElement(['document', 'objects', 'PathItem'], propertyNode), + ); + } + }); + + this.element = new MemberElement( + this.keyElement, + this.maybeAddSourceMap(objectNode, pathsElement), + ); + + return BREAK; + }, + }, +}); + +export default PathsVisitor; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/index.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/index.ts index cb3cf1e2bd..a246629ceb 100644 --- a/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/index.ts +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/index.ts @@ -8,7 +8,7 @@ const OpenApi3_1Visitor = stampit(SpecificationVisitor, { object(objectNode) { const openApi3_1Element = new this.namespace.elements.OpenApi3_1(); - const supportedProps = ['openapi', 'info', 'servers', 'components']; + const supportedProps = ['openapi', 'info', 'servers', 'components', 'paths']; // @ts-ignore objectNode.properties.forEach((propertyNode) => { diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/$RefVisitor.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/$RefVisitor.ts new file mode 100644 index 0000000000..297669a221 --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/$RefVisitor.ts @@ -0,0 +1,11 @@ +import stampit from 'stampit'; +import PropertyVisitor from '../../generics/property-visitor'; + +const $RefVisitor = stampit(PropertyVisitor, { + props: { + name: '$ref', + type: 'String', + }, +}); + +export default $RefVisitor; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/DescriptionVisitor.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/DescriptionVisitor.ts new file mode 100644 index 0000000000..e125a3d586 --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/DescriptionVisitor.ts @@ -0,0 +1,11 @@ +import stampit from 'stampit'; +import PropertyVisitor from '../../generics/property-visitor'; + +const DescriptionVisitor = stampit(PropertyVisitor, { + props: { + name: 'description', + type: 'String', + }, +}); + +export default DescriptionVisitor; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/SummaryVisitor.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/SummaryVisitor.ts new file mode 100644 index 0000000000..92794e79a1 --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/SummaryVisitor.ts @@ -0,0 +1,11 @@ +import stampit from 'stampit'; +import PropertyVisitor from '../../generics/property-visitor'; + +const DescriptionVisitor = stampit(PropertyVisitor, { + props: { + name: 'summary', + type: 'String', + }, +}); + +export default DescriptionVisitor; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/index.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/index.ts new file mode 100644 index 0000000000..c4102623d1 --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/open-api-3-1/path-item/index.ts @@ -0,0 +1,61 @@ +import stampit from 'stampit'; +import { BREAK } from '../..'; +import { isOpenApiExtension } from '../../../predicates'; +import SpecificationVisitor from '../../SpecificationVisitor'; + +const PathItemVisitor = stampit(SpecificationVisitor, { + props: { + keyElement: null, + }, + methods: { + key(keyNode) { + this.keyElement = this.maybeAddSourceMap( + keyNode, + new this.namespace.elements.String(keyNode.value), + ); + }, + + object(objectNode) { + const pathItemElement = new this.namespace.elements.PathItem(); + const { MemberElement } = this.namespace.elements.Element.prototype; + const supportedProps = [ + '$ref', + 'description', + 'summary', + // 'get', + // 'put', + // 'post', + // 'delete', + // 'options', + // 'head', + // 'patch', + // 'trace', + ]; + + // @ts-ignore + objectNode.properties.forEach((propertyNode) => { + if (supportedProps.includes(propertyNode.key.value)) { + pathItemElement.content.push( + this.mapPropertyNodeToMemberElement( + ['document', 'objects', 'PathItem', 'fields', propertyNode.key.value], + propertyNode, + ), + ); + } else if (isOpenApiExtension({}, propertyNode)) { + pathItemElement.content.push( + this.mapPropertyNodeToMemberElement(['document', 'extension'], propertyNode), + ); + } + }); + + this.element = new MemberElement( + this.keyElement, + this.maybeAddSourceMap(objectNode, pathItemElement), + ); + + return BREAK; + }, + }, +}); + +export default PathItemVisitor; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-json/test/fixtures/sample-api.json b/apidom/packages/apidom-parser-adapter-openapi3-1-json/test/fixtures/sample-api.json index 2f16ba8077..221e30aafe 100644 --- a/apidom/packages/apidom-parser-adapter-openapi3-1-json/test/fixtures/sample-api.json +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-json/test/fixtures/sample-api.json @@ -79,6 +79,8 @@ ], "paths": { "/users": { + "summary": "path item summary", + "description": "path item description", "get": { "summary": "Returns a list of users.", "description": "Optional extended description in CommonMark or HTML.",