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
2 changes: 1 addition & 1 deletion apidom/packages/apidom-ns-asyncapi-2-0/src/predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import ServerVariableElement from './elements/ServerVariable';

export const isAsyncApi2_0Element = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq, hasClass }) => {
const isElementTypeAsyncApi2_0 = isElementType('asyncApi2-0');
const isElementTypeAsyncApi2_0 = isElementType('asyncApi2_0');
const primitiveEqObject = primitiveEq('object');
const hasClassApi = hasClass('api');

Expand Down
2 changes: 1 addition & 1 deletion apidom/packages/apidom-ns-asyncapi-2-0/test/predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('predicates', function () {

specify('should support duck-typing', function () {
const asyncApi2_0ElementDuck = {
_storedElement: 'asyncApi2-0',
_storedElement: 'asyncApi2_0',
_content: [],
classes: new ArrayElement(['api']),
primitive() {
Expand Down
2 changes: 1 addition & 1 deletion apidom/packages/apidom-ns-openapi-3-1/src/predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const isOpenapiElement = createPredicate(

export const isOpenApi3_1Element = createPredicate(
({ hasBasicElementProps, isElementType, primitiveEq, hasClass }) => {
const isElementTypeOpenApi3_1 = isElementType('openApi3-1');
const isElementTypeOpenApi3_1 = isElementType('openApi3_1');
const primitiveEqObject = primitiveEq('object');
const hasClassApi = hasClass('api');

Expand Down
2 changes: 1 addition & 1 deletion apidom/packages/apidom-ns-openapi-3-1/test/predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('predicates', function () {

specify('should support duck-typing', function () {
const openApi3_1ElementDuck = {
_storedElement: 'openApi3-1',
_storedElement: 'openApi3_1',
classes: new ArrayElement(['api']),
_content: [],
primitive() {
Expand Down
3 changes: 2 additions & 1 deletion apidom/packages/apidom-reference/src/options/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FileResolver from '../resolve/resolvers/FileResolver';
import HttpResolverAxios from '../resolve/resolvers/HttpResolverAxios';
import OpenApi3_1ResolveStrategy from '../resolve/strategies/openapi-3-1';
import AsyncApi2_0ResolveStrategy from '../resolve/strategies/asyncapi-2-0';
import OpenApiJson3_1Parser from '../parse/parsers/apidom-reference-parser-openapi-json-3-1';
import OpenApiYaml3_1Parser from '../parse/parsers/apidom-reference-parser-openapi-yaml-3-1';
import AsyncApiJson2_0Parser from '../parse/parsers/apidom-reference-parser-asyncapi-json-2-0';
Expand Down Expand Up @@ -56,7 +57,7 @@ const defaultOptions: IReferenceOptions = {
* You can add additional resolver strategies of your own, replace an existing one with
* your own implementation, or remove any resolve strategy by removing it from the list.
*/
strategies: [OpenApi3_1ResolveStrategy()],
strategies: [OpenApi3_1ResolveStrategy(), AsyncApi2_0ResolveStrategy()],
/**
* Determines whether external references will be resolved.
* If this option is disabled, then none of above resolvers will be called.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import stampit from 'stampit';
import { createNamespace, visit } from 'apidom';
import asyncApi2_0Namespace, {
getNodeType,
isAsyncApi2_0Element,
keyMap,
} from 'apidom-ns-asyncapi-2-0';

import ResolveStrategy from '../ResolveStrategy';
import {
File as IFile,
ReferenceOptions as IReferenceOptions,
ResolveStrategy as IResolveStrategy,
} from '../../../types';
import ReferenceSet from '../../../ReferenceSet';
import Reference from '../../../Reference';
import AsyncApi2_0ResolveVisitor from './visitor';

// @ts-ignore
const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];

const AsyncApi2_0ResolveStrategy: stampit.Stamp<IResolveStrategy> = stampit(ResolveStrategy, {
methods: {
canResolve(file: IFile) {
// assert by media type
if (file.mediaType !== 'text/plain') {
return [
'application/vnd.aai.asyncapi;version=2.0.0',
'application/vnd.aai.asyncapi+json;version=2.0.0',
'application/vnd.aai.asyncapi+yaml;version=2.0.0',
].includes(file.mediaType);
}

// assert by inspecting ApiDOM
return isAsyncApi2_0Element(file.parseResult?.api);
},

async resolve(file: IFile, options: IReferenceOptions) {
const namespace = createNamespace(asyncApi2_0Namespace);
const reference = Reference({ uri: file.uri, value: file.parseResult });
const visitor = AsyncApi2_0ResolveVisitor({ reference, namespace, options });
const refSet = ReferenceSet();
refSet.add(reference);

await visitAsync(refSet.rootRef.value, visitor, {
keyMap,
nodeTypeGetter: getNodeType,
});
await visitor.crawl();

return refSet;
},
},
});

export default AsyncApi2_0ResolveStrategy;
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import stampit from 'stampit';
import { propEq, values, has, pipe } from 'ramda';
import { allP } from 'ramda-adjunct';
import { isPrimitiveElement, visit } from 'apidom';
import {
getNodeType,
isReferenceElement,
isReferenceLikeElement,
keyMap,
ReferenceElement,
isReferenceElementExternal,
} from 'apidom-ns-asyncapi-2-0';

import { Reference as IReference } from '../../../types';
import { MaximumDereferenceDepthError, MaximumResolverDepthError } from '../../../util/errors';
import * as url from '../../../util/url';
import parse from '../../../parse';
import Reference from '../../../Reference';
import { evaluate, uriToPointer } from '../../../selectors/json-pointer';

// @ts-ignore
const visitAsync = visit[Symbol.for('nodejs.util.promisify.custom')];

const AsyncApi2_0ResolveVisitor = stampit({
props: {
indirections: [],
namespace: null,
reference: null,
crawledElements: null,
crawlingMap: null,
options: null,
},
init({ reference, namespace, indirections = [], options }) {
this.indirections = indirections;
this.namespace = namespace;
this.reference = reference;
this.crawledElements = [];
this.crawlingMap = {};
this.options = options;
},
methods: {
toBaseURI(uri: string): string {
const uriWithoutHash = url.stripHash(uri);
const sanitizedURI = url.isFileSystemPath(uriWithoutHash)
? url.fromFileSystemPath(uriWithoutHash)
: uriWithoutHash;

return url.resolve(this.reference.uri, sanitizedURI);
},

async toReference(uri: string): Promise<IReference> {
// detect maximum depth of resolution
if (this.reference.depth >= this.options.resolve.maxDepth) {
throw new MaximumResolverDepthError(
`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`,
);
}

const baseURI = this.toBaseURI(uri);
const { refSet } = this.reference;

// we've already processed this Reference in past
if (refSet.has(baseURI)) {
return refSet.find(propEq('uri', baseURI));
}

const parseResult = await parse(baseURI, this.options);

// register new Reference with ReferenceSet
const reference = Reference({
uri: baseURI,
value: parseResult,
depth: this.reference.depth + 1,
});

refSet.add(reference);

return reference;
},

ReferenceElement(referenceElement: ReferenceElement) {
// ignore resolving external Reference Objects
if (!this.options.resolve.external && isReferenceElementExternal(referenceElement)) {
return false;
}

const uri = referenceElement.$ref.toValue();
const baseURI = this.toBaseURI(uri);

if (!has(baseURI, this.crawlingMap)) {
this.crawlingMap[baseURI] = this.toReference(uri);
}
this.crawledElements.push(referenceElement);

return undefined;
},

async crawlReferenceElement(referenceElement: ReferenceElement) {
// @ts-ignore
const reference = await this.toReference(referenceElement.$ref.toValue());

this.indirections.push(referenceElement);

const jsonPointer = uriToPointer(referenceElement.$ref.toValue());

// possibly non-semantic fragment
let fragment = evaluate(jsonPointer, reference.value.result);

// applying semantics to a fragment
if (isPrimitiveElement(fragment)) {
const referencedElementType = referenceElement.meta.get('referenced-element').toValue();

if (isReferenceLikeElement(fragment)) {
// handling indirect references
fragment = ReferenceElement.refract(fragment);
fragment.setMetaProperty('referenced-element', referencedElementType);
} else {
// handling direct references
const ElementClass = this.namespace.getElementClass(referencedElementType);
fragment = ElementClass.refract(fragment);
}
}

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

// detect maximum depth of dereferencing
if (this.indirections.length > this.options.dereference.maxDepth) {
throw new MaximumDereferenceDepthError(
`Maximum dereference depth of "${this.options.dereference.maxDepth}" has been exceeded in file "${this.reference.uri}"`,
);
}

// dive deep into the fragment
const visitor = AsyncApi2_0ResolveVisitor({
reference,
namespace: this.namespace,
indirections: [...this.indirections],
options: this.options,
});
await visitAsync(fragment, visitor, { keyMap, nodeTypeGetter: getNodeType });
await visitor.crawl();

this.indirections.pop();
},

async crawl() {
/**
* Synchronize all parallel resolutions in this place.
* After synchronization happened we can be sure that refSet
* contains resolved Reference objects.
*/
await pipe(values, allP)(this.crawlingMap);
this.crawlingMap = null;

for (const element of this.crawledElements) {
if (isReferenceElement(element)) {
await this.crawlReferenceElement(element); // eslint-disable-line no-await-in-loop
}
}
},
},
});

export default AsyncApi2_0ResolveVisitor;
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const OpenApi3_1ResolveVisitor = stampit({
namespace: null,
reference: null,
crawledElements: null,
crawlingMap: {},
crawlingMap: null,
options: null,
},
init({ reference, namespace, indirections = [], options }) {
Expand Down
16 changes: 1 addition & 15 deletions apidom/packages/apidom-reference/test/dereference/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { assert } from 'chai';
import path from 'path';
import { toValue } from 'apidom';

import { dereference, dereferenceApiDOM, parse, resolve } from '../../src';
import { dereference, dereferenceApiDOM, parse } from '../../src';
import { loadJsonFile } from '../helpers';

describe('dereference', function () {
Expand Down Expand Up @@ -39,18 +39,4 @@ describe('dereference', function () {
});
});
});

context('given refSet is provided as an option', function () {
specify('should dereference without external resolution', async function () {
const fixturePath = path.join(__dirname, 'fixtures', 'refset-as-option');
const uri = path.join(fixturePath, 'root.json');
const refSet = await resolve(uri, {
parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' },
});
const actual = await dereference(uri, { dereference: { refSet } });
const expected = loadJsonFile(path.join(fixturePath, 'dereferenced.json'));

assert.deepEqual(toValue(actual), expected);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[
{
"asyncapi": "2.0.0",
"components": {
"parameters": {
"externalRef": {
"description": "Id of the user.",
"schema": {
"type": "string"
}
}
}
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"externalParameter": {
"description": "Id of the user.",
"schema": {
"type": "string"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"asyncapi": "2.0.0",
"components": {
"parameters": {
"externalRef": {
"$ref": "./ex1.json#/indirection"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { toValue } from 'apidom';
import { isParameterElement } from 'apidom-ns-asyncapi-2-0';

import { loadJsonFile } from '../../../../helpers';
import { dereference } from '../../../../../src';
import { dereference, resolve } from '../../../../../src';
import { evaluate } from '../../../../../src/selectors/json-pointer';
import {
DereferenceError,
Expand Down Expand Up @@ -266,6 +266,20 @@ describe('dereference', function () {
}
});
});

context('given refSet is provided as an option', function () {
specify('should dereference without external resolution', async function () {
const fixturePath = path.join(__dirname, 'fixtures', 'refset-as-option');
const uri = path.join(fixturePath, 'root.json');
const refSet = await resolve(uri, {
parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' },
});
const actual = await dereference(uri, { dereference: { refSet } });
const expected = loadJsonFile(path.join(fixturePath, 'dereferenced.json'));

assert.deepEqual(toValue(actual), expected);
});
});
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"indirection": {
"$ref": "./ex2.json#/indirection"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"indirection": {
"$ref": "./ex3.json#/externalParameter"
}
}
Loading