Skip to content

Commit

Permalink
feat(reference): finalize reference resolver algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
char0n committed Dec 22, 2020
1 parent 82cb745 commit 61ab29b
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 33 deletions.
12 changes: 3 additions & 9 deletions apidom/packages/apidom-reference/src/ReferenceSet.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import stampit from 'stampit';
import { curry } from 'ramda';
import { propEq } from 'ramda';
import { isNotUndefined } from 'ramda-adjunct';

import { Reference as IReference, ReferenceSet as IReferenceSet } from './types';

const comparator = curry((r1: IReference, r2: IReference): boolean => r1.uri === r2.uri);

const ReferenceSet: stampit.Stamp<IReferenceSet> = stampit({
props: {
rootRef: null,
Expand All @@ -29,8 +27,8 @@ const ReferenceSet: stampit.Stamp<IReferenceSet> = stampit({
return this;
},

has(reference: IReference): boolean {
return isNotUndefined(this.find(comparator(reference)));
has(uri: string): boolean {
return isNotUndefined(this.find(propEq('uri', uri)));
},

find(callback): IReference | undefined {
Expand All @@ -40,10 +38,6 @@ const ReferenceSet: stampit.Stamp<IReferenceSet> = stampit({
*values() {
yield* this.refs;
},

async resolve(): Promise<IReferenceSet> {
return Promise.resolve(this);
},
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,17 @@
import { transduce, map } from 'ramda';
import { Element, filter } from 'apidom';
import { transduce, map, propEq, pathOr, flatten } from 'ramda';
import { Element, ObjectElement, ParseResultElement, filter } from 'apidom';
import { ReferenceElement } from 'apidom-ns-openapi-3-1';

import * as url from '../../../util/url';
import { isExternalReferenceElement } from '../predicates';
import { isExternalReferenceElement, isExternalReferenceLikeElement } from '../predicates';
import ReferenceSet from '../../../ReferenceSet';
import Reference from '../../../Reference';
import { mergeWithDefaults } from '../../../options';
import { ReferenceSet as IReferenceSet } from '../../../types';

/**
* 1.) Compute base URI
* 2.) Find external Reference elements
* 3.) For each Reference element:
* 3.1.) Fetch Reference element content
* 3.2.) Parse Reference element content
* 3.3.) Repeat with 1.)
*
* 1.) Base URI is either provided from outside or determined by CWD.
* 2.) Root References are found by using namespace predicates. Tree structure
* is created and all non-root References are assigned as direct
*/
import {
ReferenceSet as IReferenceSet,
ReferenceOptions as IReferenceOptions,
} from '../../../types';
import parse from '../../../parse';

/**
* If the path is a filesystem path, then convert it to a URL.
Expand All @@ -35,22 +26,72 @@ const sanitizeBaseURI = (baseURI: string): string => {
return url.isFileSystemPath(baseURI) ? url.fromFileSystemPath(baseURI) : baseURI;
};

/**
* Resolves the given JSON Reference, and then crawls the resulting value.
* The promise resolves once all JSON references in the object have been resolved,
* including nested references that are contained in externally-referenced files.
*/
export const resolveReferenceObject = async (
element: ObjectElement | ReferenceElement,
refSet: IReferenceSet,
options: IReferenceOptions,
): Promise<ParseResultElement | ParseResultElement[]> => {
const $ref = element.get('$ref').toValue();
const resolvedURI = url.resolve(options.resolve.baseURI, $ref);
const withoutHash = url.stripHash(resolvedURI);

// return early if we already recognize this reference
if (refSet.has(withoutHash)) {
const reference = refSet.find(propEq('uri', withoutHash));
return pathOr(new ParseResultElement(), ['value'], reference);
}

// parse the file and register with reference set
const parseResult = await parse(withoutHash, options);
const reference = Reference({ uri: withoutHash, depth: 0, refSet, value: parseResult });

refSet.add(reference);

// eslint-disable-next-line @typescript-eslint/no-use-before-define
return flatten(await Promise.all(crawl(parseResult, refSet, options)));
};

/**
* Recursively crawls the given element, and resolves any external Reference Object like element.
* Returns an array of promises. There will be one promise for each Reference like Object.
*/
const crawl = <T extends Element>(
element: T,
refSet: IReferenceSet,
options: IReferenceOptions,
): Promise<ParseResultElement | ParseResultElement[]>[] => {
let promises: Promise<ParseResultElement | ParseResultElement[]>[] = [];
const externalReferenceLikeObjects = filter(isExternalReferenceLikeElement)(element);

for (const externalReferenceLikeObject of externalReferenceLikeObjects) {
const resolved = resolveReferenceObject(externalReferenceLikeObject, refSet, options);
promises = promises.concat(resolved);
}

return promises;
};

/**
* Find and resolve ReferenceElements into ReferenceMap.
*/
const resolve = <T extends Element>(element: T, options = {}): IReferenceSet => {
const mergedOpts = mergeWithDefaults(options);
const baseURI = url.resolve(url.cwd(), sanitizeBaseURI(mergedOpts.resolve.baseURI)); // make it absolut
const externalRefs = filter(isExternalReferenceElement)(element);
const baseURI = url.resolve(url.cwd(), sanitizeBaseURI(mergedOpts.resolve.baseURI)); // make it absolute
const externalReferenceObjects = filter(isExternalReferenceElement)(element);
const transducer = map((ref: ReferenceElement) => url.stripHash(ref.$ref.toValue()));
const iteratorFn = (acc: IReferenceSet, uri: string) =>
acc.add(Reference({ uri, depth: 0, refSet: acc }));
const refSet = ReferenceSet();
const rootReference = Reference({ uri: baseURI, depth: 0, refSet });
const rootReference = Reference({ uri: baseURI, depth: 0, refSet, value: element });
refSet.add(rootReference);

// @ts-ignore
return transduce(transducer, iteratorFn, refSet, externalRefs);
return transduce(transducer, iteratorFn, refSet, externalReferenceObjects);
};

export default resolve;
5 changes: 2 additions & 3 deletions apidom/packages/apidom-reference/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface Parser {
export interface Reference {
uri: string;
depth: number;
value: unknown;
value: ParseResultElement;
refSet: null | ReferenceSet;
errors: Array<Error>;

Expand All @@ -48,10 +48,9 @@ export interface ReferenceSet {
readonly size: number;

add(reference: Reference): ReferenceSet;
has(reference: Reference): boolean;
has(uri: string): boolean;
find(callback: (reference: Reference) => boolean): undefined | Reference;
values(): IterableIterator<Reference>;
resolve(): Promise<ReferenceSet>;
}

export interface ReferenceParserOptions {
Expand Down

0 comments on commit 61ab29b

Please sign in to comment.