Skip to content

Commit

Permalink
feat(reference): add AsyncApi 2.0.x YAML reference parser
Browse files Browse the repository at this point in the history
  • Loading branch information
char0n committed Nov 20, 2020
1 parent e3aa9af commit 5eea3ba
Show file tree
Hide file tree
Showing 16 changed files with 351 additions and 19 deletions.
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(asyncapi2_0);

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,8 +1,18 @@
import { pathSatisfies, startsWith, both, curry } from 'ramda';
import { isYamlKeyValuePair } from 'apidom-ast';
import { isYamlMapping, isYamlKeyValuePair } from 'apidom-ast';
// @ts-ignore
import { hasKeys } from 'apidom-parser-adapter-yaml-1-2';

// isAsyncApiExtension :: (Options, YamlKeyValuePair) -> Boolean
// eslint-disable-next-line import/prefer-default-export
export const isAsyncApiExtension = curry((options, node) =>
both(isYamlKeyValuePair, pathSatisfies(startsWith('x-'), ['key', 'content']))(node),
);

// isReferenceObject :: Options -> YamlMapping -> Boolean
export const isReferenceObject = curry((options, node) => {
if (!isYamlMapping(node)) {
return false;
}
return hasKeys(['$ref'], node.properties);
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import ChannelItemDescriptionVisitor from './visitors/async-api-2-0/channel-item
import ChannelBindingsVisitor from './visitors/async-api-2-0/channel-bindings';
import OperationVisitor from './visitors/async-api-2-0/operation';
import ParametersVisitor from './visitors/async-api-2-0/parameters';
import ParameterVisitor from './visitors/async-api-2-0/parameter';
import ParameterDescriptionVisitor from './visitors/async-api-2-0/parameter/DescriptionVisitor';
import ParameterLocationVisitor from './visitors/async-api-2-0/parameter/LocationVisitor';
import ServersVisitor from './visitors/async-api-2-0/servers';
import ServerVisitor from './visitors/async-api-2-0/server';
import ServerUrlVisitor from './visitors/async-api-2-0/server/UrlVisitor';
Expand All @@ -45,6 +48,8 @@ import ServerVariableDescriptionVisitor from './visitors/async-api-2-0/server-va
import ServerVariableExamplesVisitor from './visitors/async-api-2-0/server-variable/ExamplesVisitor';
import ServerBindingsVisitor from './visitors/async-api-2-0/server-bindings';
import SecurityRequirementVisitor from './visitors/async-api-2-0/security-requirement';
import ReferenceVisitor from './visitors/async-api-2-0/reference';
import Reference$RefVisitor from './visitors/async-api-2-0/reference/$RefVisitor';
import { MappingVisitor } from './visitors/generics';

/**
Expand Down Expand Up @@ -180,12 +185,26 @@ const specification = mergeDeepRight(yamlSpecification, {
Parameters: {
$visitor: ParametersVisitor,
},
Parameter: {
$visitor: ParameterVisitor,
fixedFields: {
description: ParameterDescriptionVisitor,
schema: SchemaVisitor,
location: ParameterLocationVisitor,
},
},
Components: {
$visitor: ComponentsVisitor,
fixedFields: {
schemas: SchemasVisitor,
},
},
Reference: {
$visitor: ReferenceVisitor,
fixedFields: {
$ref: Reference$RefVisitor,
},
},
},
extension: SpecificationExtensionVisitor,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import stampit from 'stampit';

import { KindVisitor } from '../../generics';

const DescriptionVisitor = stampit(KindVisitor);

export default DescriptionVisitor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import stampit from 'stampit';

import { KindVisitor } from '../../generics';

const LocationVisitor = stampit(KindVisitor);

export default LocationVisitor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import stampit from 'stampit';
import { always } from 'ramda';

import FixedFieldsJsonObjectVisitor from '../../generics/FixedFieldsYamlMappingVisitor';
import { KindVisitor } from '../../generics';

const ParameterVisitor = stampit(KindVisitor, FixedFieldsJsonObjectVisitor, {
props: {
specPath: always(['document', 'objects', 'Parameter']),
},
init() {
this.element = new this.namespace.elements.Parameter();
},
});

export default ParameterVisitor;
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import stampit from 'stampit';
import { test, always } from 'ramda';
import { test } from 'ramda';
import { isYamlMapping, JsonNode } from 'apidom-ast';
// @ts-ignore
import { isReferenceObject } from 'apidom-parser-adapter-asyncapi-json-2-0';

import PatternedFieldsYamlMappingVisitor from '../../generics/PatternedFieldsYamlMappingVisitor';
import { KindVisitor } from '../../generics';

const ParametersVisitor = stampit(KindVisitor, PatternedFieldsYamlMappingVisitor, {
props: {
// TODO([email protected]): replace generic value spec with concrete objects
specPath: always(['kind']),
specPath: (node: JsonNode) => {
// eslint-disable-next-line no-nested-ternary
return isReferenceObject({}, node)
? ['document', 'objects', 'Reference']
: isYamlMapping(node)
? ['document', 'objects', 'Parameter']
: ['value'];
},
fieldPatternPredicate: test(/^[A-Za-z0-9_\\-]+$/),
},
init() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import stampit from 'stampit';

import { KindVisitor } from '../../generics';

const $RefVisitor = stampit(KindVisitor);

export default $RefVisitor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import stampit from 'stampit';
import { always } from 'ramda';

import FixedFieldsYamlMappingVisitor from '../../generics/FixedFieldsYamlMappingVisitor';
import { KindVisitor } from '../../generics';

const ReferenceVisitor = stampit(KindVisitor, FixedFieldsYamlMappingVisitor, {
props: {
specPath: always(['document', 'objects', 'Reference']),
canSupportSpecificationExtensions: false,
},
init() {
this.element = new this.namespace.elements.Reference();
},
});

export default ReferenceVisitor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import stampit from 'stampit';
import { assocPath, always } from 'ramda';
import { ParseResultElement } from 'apidom';
// @ts-ignore
import { parse, specification } from 'apidom-parser-adapter-asyncapi-yaml-2-0';

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

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

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

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

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

/**
* Path of the Specification object where visitor is located.
*/
specPath: always(['kind']),
},
init(
this: AsyncApiYaml2_0Parser,
{ 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 AsyncApiYaml2_0Parser;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import stampit from 'stampit';
import { always } from 'ramda';
// @ts-ignore
import { DocumentVisitor as AsyncApiYamlDocumentVisitor } from 'apidom-parser-adapter-asyncapi-yaml-2-0';
import { YamlMapping } from 'apidom-ast';

const DocumentVisitor = stampit(AsyncApiYamlDocumentVisitor, {
props: {
specPath: always(['kind']),
},
// @ts-ignore
init({ specPath = this.specPath } = {}) {
this.specPath = specPath;
},
methods: {
mapping(mappingNode: YamlMapping) {
const element = this.nodeToElement(this.specPath(), mappingNode);
this.element.content.push(element);
},
},
});

type SpecPath = () => string[];
type Options = Record<string, unknown>;

export const documentVisitorFactory = (specPath: SpecPath) => (opts: Options) =>
DocumentVisitor({ specPath, ...opts });

export default DocumentVisitor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
userId:
description: Id of the user.
schema:
type: string
Loading

0 comments on commit 5eea3ba

Please sign in to comment.