Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apidom/packages/apidom-ast/src/nodes/yaml/YamlDocument.ts
Original file line number Diff line number Diff line change
@@ -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<YamlDocument> = stampit(Node, {
statics: {
type: 'document',
},
// @ts-ignore
get child(): unknown {
// @ts-ignore
return head(this.children);
},
});

export default YamlDocument;
16 changes: 16 additions & 0 deletions apidom/packages/apidom-ast/src/nodes/yaml/YamlKeyValuePair.ts
Original file line number Diff line number Diff line change
@@ -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<YamlKeyValuePair> = 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;
Original file line number Diff line number Diff line change
@@ -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);
},
},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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<any> => {
const parse = async (
source: string,
{ sourceMap = false, specObj = specification, parser = null } = {},
): Promise<ParseResultElement> => {
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;
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Loading