Skip to content

Commit

Permalink
feat(reference): introduce POC of dereference algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
char0n committed Jan 15, 2021
1 parent 3f179a0 commit 2b3b69f
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 13 deletions.
2 changes: 2 additions & 0 deletions apidom/packages/@types/minim.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ declare module 'minim' {
setMetaProperty(name: string, value: any): void;

freeze(): void;

clone(): Element;
}

interface Type<T> extends Element {
Expand Down
18 changes: 10 additions & 8 deletions apidom/packages/apidom-ns-openapi-3-1/src/elements/Parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,6 @@ class Parameter extends ObjectElement {
this.set('in', val);
}

get description(): StringElement {
return this.get('description');
}

set description(description: StringElement) {
this.set('description', description);
}

get required(): BooleanElement {
return this.get('required');
}
Expand Down Expand Up @@ -113,4 +105,14 @@ class Parameter extends ObjectElement {
}
}

Object.defineProperty(Parameter.prototype, 'description', {
get(): StringElement {
return this.get('description');
},
set(description: StringElement) {
this.set('description', description);
},
enumerable: true,
});

export default Parameter;
2 changes: 2 additions & 0 deletions apidom/packages/apidom-ns-openapi-3-1/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export {
isReferenceElement,
} from './predicates';

export { visit, getNodeType, keyMapDefault as keyMap, BREAK } from './traversal/visitor';

export { default as ComponentsElement } from './elements/Components';
export { default as ContactElement } from './elements/Contact';
export { default as InfoElement } from './elements/Info';
Expand Down
34 changes: 34 additions & 0 deletions apidom/packages/apidom-ns-openapi-3-1/src/traversal/visitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { propOr } from 'ramda';
import {
Element,
visit as astVisit,
keyMap as keyMapBase,
getNodeType as getNodeTypeBase,
} from 'apidom';
import { isReferenceElement } from '../predicates';

export { BREAK } from 'apidom';

export const getNodeType = <T extends Element>(element: T): string | undefined => {
return isReferenceElement(element) ? 'reference' : getNodeTypeBase(element);
};

export const keyMapDefault = {
...keyMapBase,
reference: ['content'],
};

// @ts-ignore
export const visit = (root: Element, visitor, { keyMap = keyMapDefault, ...rest } = {}): void => {
// if visitor is associated with the keymap, we prefer this visitor keymap
const effectiveKeyMap = propOr(keyMap, 'keyMap', visitor);

// @ts-ignore
return astVisit(root, visitor, {
// @ts-ignore
keyMap: effectiveKeyMap,
// @ts-ignore
nodeTypeGetter: getNodeType,
...rest,
});
};
80 changes: 80 additions & 0 deletions apidom/packages/apidom-reference/test/dereference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import fs from 'fs';
import util from 'util';
import path from 'path';
import stampit from 'stampit';
import { hasIn } from 'ramda';
import { isNotUndefined } from 'ramda-adjunct';
import { transclude, toValue } from 'apidom';
import { visit, isReferenceElement } from 'apidom-ns-openapi-3-1';
// @ts-ignore
import { parse } from 'apidom-parser-adapter-openapi-json-3-1';
import { evaluate, uriToPointer } from '../src/selectors/json-pointer';

const DereferenceVisitor = stampit({
props: {
element: null,
indirections: [],
},
init({ element, indirections = [] }) {
this.element = element;
this.indirections = indirections;
},
methods: {
reference(element) {
this.indirections.push(element);

const jsonPointer = uriToPointer(element.$ref.toValue());
let fragment = evaluate(jsonPointer, this.element);

// detect direct or circular reference
if (this.indirections.includes(fragment)) {
throw new Error('Recursive JSON Pointer detected');
}

// follow the reference
if (isReferenceElement(fragment)) {
const innerReference = fragment;
const visitor = DereferenceVisitor({
element: this.element,
indirections: [...this.indirections, innerReference],
});
visit(innerReference, visitor);

fragment = evaluate(jsonPointer, this.element);
}

// override description and summary (outer has higher priority then inner)
const hasDescription = isNotUndefined(element.description);
const hasSummary = isNotUndefined(element.summary);
if (hasDescription || hasSummary) {
fragment = fragment.clone();

if (hasDescription && hasIn('description', fragment)) {
// @ts-ignore
fragment.description = element.description;
}
if (hasSummary && hasIn('summary', fragment)) {
// @ts-ignore
fragment.summary = element.summary;
}
}

this.element = transclude(element, fragment, this.element);
this.indirections.pop();
},
},
});

describe('dereference', function () {
specify('should dereference', async function () {
const fixturePath = path.join(__dirname, 'fixtures', 'dereference', 'reference-objects.json');
const source = fs.readFileSync(fixturePath).toString();
const parseResult = await parse(source);
const { api } = parseResult;

const visitor = DereferenceVisitor({ element: api });
visit(api, visitor);

console.log(util.inspect(toValue(api), true, null, true));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"openapi": "3.1.0",
"components": {
"parameters": {
"userId": {
"$ref": "#/components/parameters/indirection",
"description": "override"
},
"indirection": {
"$ref": "#/components/parameters/userIdRef",
"summary": "indirect summary"
},
"userIdRef": {
"name": "userId",
"in": "query",
"description": "ID of the user",
"required": true
},
"ref": {
"$ref": "#/components/parameters/userIdRef",
"description": "another ref"
}
}
}
}
1 change: 1 addition & 0 deletions apidom/packages/apidom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export {
export { default as createPredicate } from './predicates/helpers';

export { filter, reject, find, findAtOffset, some, traverse } from './traversal';
export { visit, BREAK, getNodeType, keyMapDefault as keyMap } from './traversal/visitor';
export { transclude, default as Transcluder } from './transcluder';

export const createNamespace = (namespacePlugin?: NamespacePlugin): ApiDOMNamespace => {
Expand Down
4 changes: 4 additions & 0 deletions apidom/packages/apidom/src/transcluder/Transcluder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ const computeEdges = (element: Element, edges = new WeakMap()): WeakMap<Element,
// @ts-ignore
edges.set(element.key, element);
// @ts-ignore
computeEdges(element.key, edges);
// @ts-ignore
edges.set(element.value, element);
// @ts-ignore
computeEdges(element.value, edges);
} else {
element.children.forEach((childElement: Element): void => {
edges.set(childElement, element);
Expand Down
4 changes: 2 additions & 2 deletions apidom/packages/apidom/src/traversal/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export const CallbackVisitor = stampit(PredicateVisitor, {
enter(element: Element): undefined {
if (this.predicate(element)) {
this.callback(element);
return this.return;
return this.returnOnTrue;
}
return undefined;
return this.returnOnFalse;
},
},
});
Expand Down
6 changes: 3 additions & 3 deletions apidom/packages/apidom/src/traversal/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
export { BREAK } from 'apidom-ast';

// getNodeType :: Node -> String
const getNodeType = <T extends Element>(element: T): string | undefined => {
export const getNodeType = <T extends Element>(element: T): string | undefined => {
/*
* We're translating every possible higher element type to primitive minim type here.
* This allows us keep key mapping to minimum.
Expand All @@ -44,7 +44,7 @@ const getNodeType = <T extends Element>(element: T): string | undefined => {
// isNode :: Node -> Boolean
const isNode = curryN(1, pipe(getNodeType, isString));

const keyMapDefault = {
export const keyMapDefault = {
object: ['content'],
array: ['content'],
member: ['key', 'value'],
Expand Down Expand Up @@ -88,11 +88,11 @@ export const visit = (root: Element, visitor, { keyMap = keyMapDefault, ...rest

// @ts-ignore
return astVisit(root, visitor, {
...rest,
// @ts-ignore
keyMap: effectiveKeyMap,
// @ts-ignore
nodeTypeGetter: getNodeType,
nodePredicate: isNode,
...rest,
});
};
2 changes: 2 additions & 0 deletions apidom/packages/apidom/test/traversal/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('traversal', function () {
let callback;

beforeEach(function () {
// @ts-ignore
objElement = new namespace.elements.Object({ a: 'b', c: 'd' });
callback = sinon.spy();
});
Expand Down Expand Up @@ -95,6 +96,7 @@ describe('traversal', function () {
let callback;

beforeEach(function () {
// @ts-ignore
arrayElement = new namespace.elements.Array(['a', 'b']);
callback = sinon.spy();
});
Expand Down

0 comments on commit 2b3b69f

Please sign in to comment.