diff --git a/apidom/packages/apidom-ast/src/nodes/yaml/YamlDocument.ts b/apidom/packages/apidom-ast/src/nodes/yaml/YamlDocument.ts index 08c2d96ff4..0ce2186699 100644 --- a/apidom/packages/apidom-ast/src/nodes/yaml/YamlDocument.ts +++ b/apidom/packages/apidom-ast/src/nodes/yaml/YamlDocument.ts @@ -1,15 +1,22 @@ import stampit from 'stampit'; +import { head } from 'ramda'; import Node from '../../Node'; interface YamlDocument extends Node { type: 'document'; + readonly child: unknown; } const YamlDocument: stampit.Stamp = stampit(Node, { statics: { type: 'document', }, + // @ts-ignore + get child(): unknown { + // @ts-ignore + return head(this.children); + }, }); export default YamlDocument; diff --git a/apidom/packages/apidom-ast/src/nodes/yaml/YamlKeyValuePair.ts b/apidom/packages/apidom-ast/src/nodes/yaml/YamlKeyValuePair.ts index 761df7d683..05ee71e599 100644 --- a/apidom/packages/apidom-ast/src/nodes/yaml/YamlKeyValuePair.ts +++ b/apidom/packages/apidom-ast/src/nodes/yaml/YamlKeyValuePair.ts @@ -1,16 +1,32 @@ import stampit from 'stampit'; +import { filter, anyPass, pipe, nth } from 'ramda'; import Node from '../../Node'; import YamlStyleModel from './YamlStyle'; +import { isScalar, isMapping, isSequence } from './predicates'; interface YamlKeyValuePair extends Node, YamlStyleModel { type: 'keyValuePair'; + readonly key: unknown; + readonly value: unknown; } const YamlKeyValuePair: stampit.Stamp = stampit(Node, YamlStyleModel, { statics: { type: 'keyValuePair', }, + methods: { + // @ts-ignore + get key(): unknown { + // @ts-ignore + return pipe(filter(anyPass([isScalar, isMapping, isSequence])), nth(0))(this.children); + }, + // @ts-ignore + get value(): unknown { + // @ts-ignore + return pipe(filter(anyPass([isScalar, isMapping, isSequence])), nth(1))(this.children); + }, + }, }); export default YamlKeyValuePair; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/DocumentVisitor.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/DocumentVisitor.ts index 6d9133b984..5df05edc5d 100644 --- a/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/DocumentVisitor.ts +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-json/src/parser/visitors/DocumentVisitor.ts @@ -1,33 +1,29 @@ import stampit from 'stampit'; -import { isJsonObject } from 'apidom-ast'; +import { isJsonObject, Literal, Error, JsonDocument } from 'apidom-ast'; -import { visit } from '.'; import SpecificationVisitor from './SpecificationVisitor'; const DocumentVisitor = stampit(SpecificationVisitor, { methods: { - literal(literalNode) { + literal(literalNode: Literal) { if (literalNode.isMissing) { - const errorVisitor = this.retrieveVisitorInstance(['error']); - visit(literalNode, errorVisitor); - this.element.content.push(errorVisitor.element); + const element = this.nodeToElement(['error'], literalNode); + this.element.content.push(element); } }, - document(documentNode) { + document(documentNode: JsonDocument) { const specPath = isJsonObject(documentNode.child) ? ['document', 'objects', 'OpenApi'] : ['value']; - const visitor = this.retrieveVisitorInstance(specPath); - visit(documentNode.child, visitor); - this.element.content.push(visitor.element); + const element = this.nodeToElement(specPath, documentNode); + this.element.content.push(element); }, - error(errorNode) { - const errorVisitor = this.retrieveVisitorInstance(['error']); - visit(errorNode, errorVisitor); - this.element.content.push(errorVisitor.element); + error(errorNode: Error) { + const element = this.nodeToElement(['error'], errorNode); + this.element.content.push(element); }, }, }); diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/index.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/index.ts index a6a1009487..8d0385c8c6 100644 --- a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/index.ts +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/index.ts @@ -1,11 +1,62 @@ -import { createNamespace } from 'apidom'; +import $RefParser from '@apidevtools/json-schema-ref-parser'; +import { createNamespace, ParseResultElement } from 'apidom'; +import { + Error, + YamlStream, + YamlDocument, + YamlMapping, + YamlSequence, + YamlKeyValuePair, + transformTreeSitterYamlCST, +} from 'apidom-ast'; import openapi3_1 from 'apidom-ns-openapi3-1'; +import specification from './specification'; +import { visit } from './visitors'; + export const namespace = createNamespace(openapi3_1); -const parse = async (source: string, { parser = null } = {}): Promise => { +const parse = async ( + source: string, + { sourceMap = false, specObj = specification, parser = null } = {}, +): Promise => { + const resolvedSpecObj = await $RefParser.dereference(specObj); + // @ts-ignore + const parseResultElement = new namespace.elements.ParseResult(); + // @ts-ignore + const documentVisitor = resolvedSpecObj.visitors.stream.$visitor(); + // @ts-ignore - return parser.parse(source); + const cst = parser.parse(source); + const ast = transformTreeSitterYamlCST(cst); + + const keyMap = { + // @ts-ignore + [YamlStream.type]: ['children'], + // @ts-ignore + [YamlDocument.type]: ['child'], + // @ts-ignore + [YamlMapping.type]: ['children'], + // @ts-ignore + [YamlSequence.type]: ['children'], + // @ts-ignore + [YamlKeyValuePair.type]: ['children'], + // @ts-ignore + [Error.type]: ['children'], + }; + + visit(ast.rootNode, documentVisitor, { + keyMap, + // @ts-ignore + state: { + namespace, + specObj: resolvedSpecObj, + sourceMap, + element: parseResultElement, + }, + }); + + return parseResultElement; }; export default parse; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/source-map.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/source-map.ts new file mode 100644 index 0000000000..910a0f5171 --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/source-map.ts @@ -0,0 +1,16 @@ +import { namespace } from 'apidom'; + +/* eslint-disable import/prefer-default-export */ + +// @ts-ignore +export const addSourceMap = (node, element) => { + // @ts-ignore + const sourceMap = new namespace.elements.SourceMap(); + + sourceMap.position = node.position; + sourceMap.astNode = node; + + element.meta.set('sourceMap', sourceMap); + + return element; +}; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/specification.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/specification.ts new file mode 100644 index 0000000000..4a69bd33ed --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/specification.ts @@ -0,0 +1,26 @@ +import StreamVisitor from './visitors/StreamVisitor'; +import DocumentVisitor from './visitors/DocumentVisitor'; + +import ErrorVisitor from './visitors/ErrorVisitor'; + +/** + * Specification object allows us to have complete control over visitors + * when traversing the AST. + * Specification also allows us to create new parser adapters from + * existing ones by manipulating it. + * + * Note: Specification object allows to use relative JSON pointers. + */ +const specification = { + visitors: { + error: ErrorVisitor, + stream: { + $visitor: StreamVisitor, + }, + document: { + $visitor: DocumentVisitor, + }, + }, +}; + +export default specification; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/DocumentVisitor.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/DocumentVisitor.ts new file mode 100644 index 0000000000..e265f17e2f --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/DocumentVisitor.ts @@ -0,0 +1,11 @@ +import stampit from 'stampit'; + +import SpecificationVisitor from './SpecificationVisitor'; + +const DocumentVisitor = stampit(SpecificationVisitor, { + init() { + this.element = new this.namespace.elements.Object(); + }, +}); + +export default DocumentVisitor; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/ErrorVisitor.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/ErrorVisitor.ts new file mode 100644 index 0000000000..8b81fecc46 --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/ErrorVisitor.ts @@ -0,0 +1,35 @@ +import stampit from 'stampit'; +import { BREAK } from '.'; +import SpecificationVisitor from './SpecificationVisitor'; + +const ErrorVisitor = stampit(SpecificationVisitor, { + methods: { + literal(literalNode) { + if (literalNode.isMissing) { + const message = `(Missing ${literalNode.value})`; + this.element = new this.namespace.elements.Annotation(message); + + this.maybeAddSourceMap(literalNode, this.element); + + return BREAK; + } + + return undefined; + }, + + error(errorNode) { + const message = errorNode.isUnexpected + ? `(Unexpected ${errorNode.value})` + : `(Error ${errorNode.value})`; + + this.element = new this.namespace.elements.Annotation(message); + this.element.classes.push('error'); + + this.maybeAddSourceMap(errorNode, this.element); + + return BREAK; + }, + }, +}); + +export default ErrorVisitor; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/SpecificationVisitor.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/SpecificationVisitor.ts new file mode 100644 index 0000000000..fce9840d16 --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/SpecificationVisitor.ts @@ -0,0 +1,46 @@ +import stampit from 'stampit'; +import { pathSatisfies, path, pick, pipe, keys } from 'ramda'; +import { isFunction } from 'ramda-adjunct'; +import Visitor from './Visitor'; +import { visit } from './index'; + +/** + * This is a base Type for every visitor that does + * internal look-ups to retrieve other child visitors. + */ +const SpecificationVisitor = stampit(Visitor, { + props: { + specObj: null, + }, + // @ts-ignore + init({ specObj = this.specObj }) { + this.specObj = specObj; + }, + methods: { + retrieveFixedFields(specPath) { + return pipe(path(['visitors', ...specPath, 'fixedFields']), keys)(this.specObj); + }, + + retrieveVisitor(specPath) { + if (pathSatisfies(isFunction, ['visitors', ...specPath], this.specObj)) { + return path(['visitors', ...specPath], this.specObj); + } + + return path(['visitors', ...specPath, '$visitor'], this.specObj); + }, + + retrieveVisitorInstance(specPath, options = {}) { + const passingOpts = pick(['namespace', 'sourceMap', 'specObj'], this); + + return this.retrieveVisitor(specPath)({ ...passingOpts, ...options }); + }, + + nodeToElement(specPath: string[], node) { + const visitor = this.retrieveVisitorInstance(specPath); + visit(node, visitor); + return visitor.element; + }, + }, +}); + +export default SpecificationVisitor; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/StreamVisitor.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/StreamVisitor.ts new file mode 100644 index 0000000000..734925a7f8 --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/StreamVisitor.ts @@ -0,0 +1,44 @@ +import stampit from 'stampit'; +import { Literal, Error, YamlDocument } from 'apidom-ast'; + +import SpecificationVisitor from './SpecificationVisitor'; + +const StreamVisitor = stampit(SpecificationVisitor, { + props: { + processedDocumentCount: 0, + }, + methods: { + literal(literalNode: Literal) { + if (literalNode.isMissing) { + const element = this.nodeToElement(['error'], literalNode); + this.element.content.push(element); + } + }, + + document(documentNode: YamlDocument) { + if (this.processedDocumentCount === 1) { + const message = + 'Only first document within YAML stream will be used. Rest of them will be discarded.'; + const annotationElement = new this.namespace.elements.Annotation(message); + annotationElement.classes.push('warning'); + this.element.content.push(annotationElement); + } + + if (this.processedDocumentCount >= 1) { + return false; + } + + const element = this.nodeToElement(['document'], documentNode); + this.element.content.push(element); + this.processedDocumentCount += 1; + return undefined; + }, + + error(errorNode: Error) { + const element = this.nodeToElement(['error'], errorNode); + this.element.content.push(element); + }, + }, +}); + +export default StreamVisitor; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/Visitor.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/Visitor.ts new file mode 100644 index 0000000000..d66900a13b --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/Visitor.ts @@ -0,0 +1,26 @@ +import stampit from 'stampit'; +import { addSourceMap } from '../source-map'; + +const Visitor = stampit({ + props: { + element: null, + namespace: null, + sourceMap: false, + }, + // @ts-ignore + init({ namespace = this.namespace, sourceMap = this.sourceMap } = {}) { + this.namespace = namespace; + this.sourceMap = sourceMap; + }, + methods: { + maybeAddSourceMap(node, element) { + if (!this.sourceMap) { + return element; + } + + return addSourceMap(node, element); + }, + }, +}); + +export default Visitor; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/index.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/index.ts new file mode 100644 index 0000000000..c7a448ccd7 --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/src/parser/visitors/index.ts @@ -0,0 +1,34 @@ +import { + YamlStream, + YamlDocument, + YamlMapping, + YamlSequence, + YamlKeyValuePair, + Error, + visit as astVisit, +} from 'apidom-ast'; + +export { BREAK } from 'apidom-ast'; + +/* eslint-disable import/prefer-default-export */ + +const keyMapDefault = { + // @ts-ignore + [YamlStream.type]: ['children'], + // @ts-ignore + [YamlDocument.type]: ['child'], + // @ts-ignore + [YamlMapping.type]: ['children'], + // @ts-ignore + [YamlSequence.type]: ['children'], + // @ts-ignore + [YamlKeyValuePair.type]: ['key', 'value'], + // @ts-ignore + [Error.type]: ['children'], +}; + +// @ts-ignore +export const visit = (root, visitor, { keyMap = keyMapDefault, ...rest } = {}) => { + // @ts-ignore + return astVisit(root, visitor, { ...rest, keyMap }); +}; diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/test/fixtures/sample-api.yaml b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/test/fixtures/sample-api.yaml new file mode 100644 index 0000000000..74cd83ead7 --- /dev/null +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/test/fixtures/sample-api.yaml @@ -0,0 +1,24 @@ +--- +openapi: 3.1.0 +x-top-level: value +info: + title: Sample API + unknownFixedField: value + description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) + or HTML. + summary: example summary + termsOfService: Terms of service + version: 0.1.9 + x-version: 0.1.9-beta + license: + name: Apache License 2.0 + x-fullName: Apache License 2.0 + identifier: Apache License 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + contact: + name: Vladimir Gorej + x-username: char0n + url: https://www.linkedin.com/in/vladimirgorej/ + email: vladimir.gorej@gmail.com +... +prop: value diff --git a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/test/index.ts b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/test/index.ts index e69de29bb2..ab5bb79e14 100644 --- a/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/test/index.ts +++ b/apidom/packages/apidom-parser-adapter-openapi3-1-yaml/test/index.ts @@ -0,0 +1,19 @@ +import fs from 'fs'; +import path from 'path'; +import * as apiDOM from 'apidom'; + +import * as adapter from '../src/adapter-node'; + +const spec = fs.readFileSync(path.join(__dirname, 'fixtures', 'sample-api.yaml')).toString(); +// const namespace = apiDOM.createNamespace(openapi3); + +describe('apidom-parser-adapter-openapi3-1-yaml', function () { + it('test', async function () { + console.log(adapter.detect(spec)); + console.log(adapter.mediaTypes); + + const parseResult = await adapter.parse(spec, { sourceMap: true }); + console.log(JSON.stringify(apiDOM.toValue(parseResult), null, 2)); + // console.log (JSON.stringify(apiDOM.toJSON(namespace, parseResult), null, null)); + }); +}); diff --git a/apidom/packages/apidom/src/elements/Annotation.ts b/apidom/packages/apidom/src/elements/Annotation.ts index 4dc368cbec..ba52369a11 100644 --- a/apidom/packages/apidom/src/elements/Annotation.ts +++ b/apidom/packages/apidom/src/elements/Annotation.ts @@ -1,6 +1,8 @@ import { Attributes, Meta, StringElement } from 'minim'; class Annotation extends StringElement { + // classes: warning | error + constructor(content: Array, meta: Meta, attributes: Attributes) { super(content, meta, attributes); this.element = 'annotation';