Skip to content

Commit

Permalink
feat(reference): add OpenApi 3.1.x YAML reference parser
Browse files Browse the repository at this point in the history
  • Loading branch information
char0n committed Nov 19, 2020
1 parent dda56c8 commit f7301fc
Show file tree
Hide file tree
Showing 17 changed files with 371 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const SpecificationVisitor = stampit(Visitor, {
this.specObj = specObj;
},
methods: {
retrievePassingOptions() {
return pick(['namespace', 'sourceMap', 'specObj'], this);
},

retrieveFixedFields(specPath) {
return pipe(path(['visitors', ...specPath, 'fixedFields']), keys)(this.specObj);
},
Expand All @@ -31,14 +35,14 @@ const SpecificationVisitor = stampit(Visitor, {
},

retrieveVisitorInstance(specPath, options = {}) {
const passingOpts = pick(['namespace', 'sourceMap', 'specObj'], this);
const passingOpts = this.retrievePassingOptions();

return this.retrieveVisitor(specPath)({ ...passingOpts, ...options });
},

nodeToElement(specPath: string[], node) {
nodeToElement(specPath: string[], node, options = {}) {
const visitor = this.retrieveVisitorInstance(specPath);
visit(node, visitor);
visit(node, visitor, options);
return visitor.element;
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { invokeArgs } from 'ramda-adjunct';
import $RefParser from '@apidevtools/json-schema-ref-parser';
import { createNamespace, ParseResultElement } from 'apidom';
import {
Expand All @@ -10,7 +11,7 @@ import {
} from 'apidom-ast';
import openapi3_1 from 'apidom-ns-openapi-3-1';
// @ts-ignore
import { visit, SpecificationVisitor } from 'apidom-parser-adapter-json';
import { visit } from 'apidom-parser-adapter-json';

import specification from './specification';

Expand All @@ -21,21 +22,22 @@ const parse = async (
{
sourceMap = false,
specObj = specification,
rootVisitorSpecPath = ['document'],
rootVisitorSpecPath = ['visitors', 'document', '$visitor'],
parser = null,
} = {},
): Promise<ParseResultElement> => {
const resolvedSpecObj = await $RefParser.dereference(specObj);
// @ts-ignore
const parseResultElement = new namespace.elements.ParseResult();
const rootVisitor = SpecificationVisitor({ specObj: resolvedSpecObj }).retrieveVisitorInstance(
rootVisitorSpecPath,
);

// @ts-ignore
const cst = parser.parse(source);
const ast = transformTreeSitterJsonCST(cst);

const state = {
namespace,
specObj: resolvedSpecObj,
sourceMap,
element: parseResultElement,
};
const keyMap = {
// @ts-ignore
[JsonDocument.type]: ['children'],
Expand All @@ -48,17 +50,9 @@ const parse = async (
// @ts-ignore
[Error.type]: ['children'],
};
const rootVisitor = invokeArgs(rootVisitorSpecPath, [], resolvedSpecObj);

visit(ast.rootNode, rootVisitor, {
keyMap,
// @ts-ignore
state: {
namespace,
specObj: resolvedSpecObj,
sourceMap,
element: parseResultElement,
},
});
visit(ast.rootNode, rootVisitor, { keyMap, state });

return parseResultElement;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import stampit from 'stampit';
import { isJsonObject, JsonDocument } from 'apidom-ast';
import { JsonDocument } from 'apidom-ast';
// @ts-ignore
import { DocumentVisitor as JsonDocumentVisitor } from 'apidom-parser-adapter-json';

const DocumentVisitor = stampit(JsonDocumentVisitor, {
methods: {
document(documentNode: JsonDocument) {
const specPath = isJsonObject(documentNode.child)
? ['document', 'objects', 'OpenApi']
: ['value'];
const element = this.nodeToElement(specPath, documentNode);

const element = this.nodeToElement(['document', 'objects', 'OpenApi'], documentNode);
this.element.content.push(element);
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,33 @@ import stampit from 'stampit';
import { StringElement } from 'minim';
import { always } from 'ramda';
import { isOperationElement, OperationElement } from 'apidom-ns-openapi-3-1';
import { JsonObject } from 'apidom-ast';

import FixedFieldsJsonObjectVisitor from '../../generics/FixedFieldsJsonObjectVisitor';
import { ValueVisitor } from '../../generics';

const PathItemVisitor = stampit(ValueVisitor, FixedFieldsJsonObjectVisitor, {
props: {
specPath: always(['document', 'objects', 'PathItem']),
},
init() {
const PathItemVisitor = stampit(ValueVisitor, FixedFieldsJsonObjectVisitor).init(
function PathItemVisitor() {
this.element = new this.namespace.elements.PathItem();
},
methods: {
object(objectNode) {
// @ts-ignore
const result = FixedFieldsJsonObjectVisitor.compose.methods.object.call(this, objectNode);

// decorate Operation elements with HTTP method
this.element
.filter(isOperationElement)
.forEach((operationElement: OperationElement, httpMethodElementCI: StringElement) => {
const httpMethod = httpMethodElementCI.toValue().toUpperCase();
const httpMethodElementCS = new this.namespace.elements.String(httpMethod);
operationElement.setMetaProperty('httpMethod', httpMethodElementCS);
});
this.specPath = always(['document', 'objects', 'PathItem']);

return result;
},
this.object = {
enter(objectNode: JsonObject) {
// @ts-ignore
return FixedFieldsJsonObjectVisitor.compose.methods.object.call(this, objectNode);
},
leave() {
// decorate Operation elements with HTTP method
this.element
.filter(isOperationElement)
.forEach((operationElement: OperationElement, httpMethodElementCI: StringElement) => {
const httpMethod = httpMethodElementCI.toValue().toUpperCase();
const httpMethodElementCS = new this.namespace.elements.String(httpMethod);
operationElement.setMetaProperty('httpMethod', httpMethodElementCS);
});
},
};
},
});
);

export default PathItemVisitor;
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export { default as parse, namespace } from './parser/index-browser';
export { detect, mediaTypes } from './adapter';

export { default as specification } from './parser/specification';

export { default as DocumentVisitor } from './parser/visitors/DocumentVisitor';
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export { default as parse, namespace } from './parser/index-node';
export { mediaTypes, detect } from './adapter';

export { default as specification } from './parser/specification';

export { default as DocumentVisitor } from './parser/visitors/DocumentVisitor';
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { invokeArgs } from 'ramda-adjunct';
import $RefParser from '@apidevtools/json-schema-ref-parser';
import { createNamespace, ParseResultElement } from 'apidom';
import { transformTreeSitterYamlCST } from 'apidom-ast';
Expand All @@ -11,27 +12,28 @@ export const namespace = createNamespace(openapi3_1);

const parse = async (
source: string,
{ sourceMap = false, specObj = specification, parser = null } = {},
{
sourceMap = false,
specObj = specification,
rootVisitorSpecPath = ['visitors', 'stream', '$visitor'],
parser = null,
} = {},
): Promise<ParseResultElement> => {
const resolvedSpecObj = await $RefParser.dereference(specObj);
// @ts-ignore
const parseResultElement = new namespace.elements.ParseResult();
// @ts-ignore
const streamVisitor = resolvedSpecObj.visitors.stream.$visitor();

// @ts-ignore
const cst = parser.parse(source);
const ast = transformTreeSitterYamlCST(cst);
const state = {
namespace,
specObj: resolvedSpecObj,
sourceMap,
element: parseResultElement,
};
const rootVisitor = invokeArgs(rootVisitorSpecPath, [], resolvedSpecObj);

visit(ast.rootNode, streamVisitor, {
// @ts-ignore
state: {
namespace,
specObj: resolvedSpecObj,
sourceMap,
element: parseResultElement,
},
});
visit(ast.rootNode, rootVisitor, { state });

return parseResultElement;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import stampit from 'stampit';
import { StringElement } from 'minim';
import { always } from 'ramda';
import { YamlMapping } from 'apidom-ast';
import { isOperationElement, OperationElement } from 'apidom-ns-openapi-3-1';

import FixedFieldsYamlMappingVisitor from '../../generics/FixedFieldsYamlMappingVisitor';
Expand All @@ -12,7 +13,12 @@ const PathItemVisitor = stampit(KindVisitor, FixedFieldsYamlMappingVisitor).init
this.specPath = always(['document', 'objects', 'PathItem']);

this.mapping = {
enter(mappingNode: YamlMapping) {
// @ts-ignore
return FixedFieldsYamlMappingVisitor.compose.methods.mapping.call(this, mappingNode);
},
leave() {
// decorate Operation elements with HTTP method
this.element
.filter(isOperationElement)
.forEach((operationElement: OperationElement, httpMethodElementCI: StringElement) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import StreamVisitor from './visitors/StreamVisitor';
import DocumentVisitor from './visitors/DocumentVisitor';
import ErrorVisitor from './visitors/ErrorVisitor';
import CommentVisitor from './visitors/CommentVisitor';
import { ScalarVisitor, MappingVisitor, SequenceVisitor, KindVisitor } from './visitors/generics';

/**
Expand All @@ -18,6 +19,7 @@ const specification = {
sequence: SequenceVisitor,
kind: KindVisitor,
error: ErrorVisitor,
comment: CommentVisitor,
stream: {
$visitor: StreamVisitor,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import stampit from 'stampit';
import { YamlComment } from 'apidom-ast';

import { BREAK } from './index';
import SpecificationVisitor from './SpecificationVisitor';

const CommentVisitor = stampit(SpecificationVisitor, {
init() {
this.element = new this.namespace.elements.Comment();
},
methods: {
comment(commentNode: YamlComment) {
this.element.content = commentNode.content;
this.maybeAddSourceMap(commentNode, this.element);

return BREAK;
},
},
});

export default CommentVisitor;
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const DocumentVisitor = stampit(SpecificationVisitor, {
},

comment(commentNode: YamlComment) {
const commentElement = new this.namespace.elements.Comment(commentNode.content);
const commentElement = this.nodeToElement(['comment'], commentNode);
this.element.content.push(commentElement);
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ const StreamVisitor = stampit(SpecificationVisitor, {
return false;
}

const commentElement = new this.namespace.elements.Comment(commentNode.content);
const commentElement = this.nodeToElement(['comment'], commentNode);
this.element.content.push(commentElement);

return undefined;
},

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import stampit from 'stampit';
import { assocPath, always } from 'ramda';
import { ParseResultElement } from 'apidom';

// @ts-ignore
import { parse, specification } from 'apidom-parser-adapter-openapi-yaml-3-1';

import File from '../../util/File';
import { ParserError } from '../../util/errors';
import { documentVisitorFactory } from './visitors/DocumentVisitor';

interface OpenApiYaml3_1Parser {
allowEmpty: boolean;
sourceMap: boolean;
specPath: string;

canParse(file: File): boolean;
parse(file: File): Promise<ParseResultElement>;
}

const OpenApiYaml3_1Parser: stampit.Stamp<OpenApiYaml3_1Parser> = stampit({
props: {
/**
* Whether to allow "empty" files. This includes zero-byte files.
*/
allowEmpty: true,

/**
* Whether to generate source map during parsing.
*/
sourceMap: true,

/**
* Path of the Specification object where visitor is located.
*/
specPath: always(['kind']),
},
init(
this: OpenApiYaml3_1Parser,
{ allowEmpty = this.allowEmpty, sourceMap = this.sourceMap, specPath = this.specPath } = {},
) {
this.allowEmpty = allowEmpty;
this.sourceMap = sourceMap;
this.specPath = specPath;
},
methods: {
canParse(file: File): boolean {
return ['.yaml', '.yml'].includes(file.extension);
},
async parse(file: File): Promise<ParseResultElement> {
const specObj = assocPath(
['visitors', 'document', '$visitor'],
documentVisitorFactory(this.specPath),
specification,
);

try {
return await parse(file.data, { sourceMap: this.sourceMap, specObj });
} catch (e) {
throw new ParserError(`Error parsing "${file.url}"`, e);
}
},
},
});

export default OpenApiYaml3_1Parser;
Loading

0 comments on commit f7301fc

Please sign in to comment.