Skip to content

Commit

Permalink
feat(reference): add resolver strategy that handles abstract RefEleme…
Browse files Browse the repository at this point in the history
…nt (#3893)

Refs #3888
  • Loading branch information
char0n committed Mar 5, 2024
1 parent 6b97051 commit 9db6d3e
Show file tree
Hide file tree
Showing 31 changed files with 568 additions and 132 deletions.
75 changes: 74 additions & 1 deletion packages/apidom-reference/README.md
Expand Up @@ -892,6 +892,35 @@ Both of above examples will be using [HttpResolverAxios](https://github.com/swag
(as we're trying to resolve HTTP(s) URL) and the `timeout` of resolution will increase from **default 3 seconds**
to 10 seconds.

##### Resolver strategy plugin options

Some resolver strategy plugins accept additional options. It's possible to **change** strategy plugin
**options globally** by mutating global `resolve` options:

```js
import { options, resolve } from '@swagger-api/apidom-reference';

options.resolve.strategyOpts = {
apidom: { clone: true },
};

await resolve('https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.1/webhook-example.json');
```

To **change** the resolver strategy plugins **options** on ad-hoc basis:

```js
import { resolve } from '@swagger-api/apidom-reference';

await resolve('https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.1/webhook-example.json', {
resolve: {
strategyOpts: {
apidom: { clone: true },
},
},
});
```

##### Creating new resolver plugin

Resolve component can be extended by additional resolver plugins. Every resolver plugin is an object that
Expand Down Expand Up @@ -1065,6 +1094,19 @@ Every Reference object represents single external dependency.
External resolution strategy determines how a document is externally resolved. Depending on document `mediaType`
every strategy differs significantly. Resolve component comes with two (2) default external resolution strategies.

##### [apidom](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/resolve/strategies/apidom)

External resolution strategy for understanding and resolving remote elements referenced with [Ref Element](https://apielements.org/en/latest/element-definitions.html?highlight=referencing#ref-element).

Supported media types:

```js
[
'application/vnd.apidom',
'application/vnd.apidom+json'
]
```

##### [asyncapi-2](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/resolve/strategies/asyncapi-2)

External resolution strategy for understanding and resolving external dependencies of [AsyncApi 2.x.y](https://github.com/asyncapi/spec/blob/master/spec/asyncapi.md) definitions.
Expand Down Expand Up @@ -1416,7 +1458,8 @@ Supported media types:

```js
[
'application/vnd.apidom'
'application/vnd.apidom',
'application/vnd.apidom+json',
]
```

Expand Down Expand Up @@ -1569,6 +1612,36 @@ await dereference('/home/user/oas.json', {
}
});
```

##### Dereference strategy plugin options

Some dereference strategy plugins accept additional options. It's possible to **change** strategy plugin
**options globally** by mutating global `dereference` options:

```js
import { options, dereference } from '@swagger-api/apidom-reference';

options.dereference.strategyOpts = {
apidom: { clone: true },
};

await dereference('https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.1/webhook-example.json');
```

To **change** the dereference strategy plugins **options** on ad-hoc basis:

```js
import { dereference } from '@swagger-api/apidom-reference';

await dereference('https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.1/webhook-example.json', {
dereference: {
strategyOpts: {
apidom: { clone: true },
},
},
});
```

##### Creating new dereference strategy

Dereference component can be extended by additional strategies. Every strategy is an object that
Expand Down
5 changes: 5 additions & 0 deletions packages/apidom-reference/package.json
Expand Up @@ -53,6 +53,11 @@
"require": "./cjs/resolve/resolvers/http-axios/index.cjs",
"types": "./types/resolve/resolvers/http-axios/index.d.ts"
},
"./resolve/strategies/apidom": {
"import": "./es/resolve/strategies/apidom/index.mjs",
"require": "./cjs/resolve/strategies/apidom/index.cjs",
"types": "./types/resolve/strategies/apidom/index.d.ts"
},
"./resolve/strategies/asyncapi-2": {
"import": "./es/resolve/strategies/asyncapi-2/index.mjs",
"require": "./cjs/resolve/strategies/asyncapi-2/index.cjs",
Expand Down
6 changes: 5 additions & 1 deletion packages/apidom-reference/src/bundle/index.ts
Expand Up @@ -33,7 +33,11 @@ const bundle = async (uri: string, options: IReferenceOptions): Promise<ParseRes
mediaType: mergedOptions.parse.mediaType,
});

const bundleStrategies = await plugins.filter('canBundle', file, mergedOptions.bundle.strategies);
const bundleStrategies = await plugins.filter(
'canBundle',
[file, mergedOptions],
mergedOptions.bundle.strategies,
);

// we couldn't find any bundle strategy for this File
if (isEmpty(bundleStrategies)) {
Expand Down
2 changes: 2 additions & 0 deletions packages/apidom-reference/src/configuration/saturated.ts
Expand Up @@ -4,6 +4,7 @@ import OpenApi2ResolveStrategy from '../resolve/strategies/openapi-2';
import OpenApi3_0ResolveStrategy from '../resolve/strategies/openapi-3-0';
import OpenApi3_1ResolveStrategy from '../resolve/strategies/openapi-3-1';
import AsyncApi2ResolveStrategy from '../resolve/strategies/asyncapi-2';
import ApiDOMResolveStrategy from '../resolve/strategies/apidom';
import ApiDesignSystemsJsonParser from '../parse/parsers/api-design-systems-json';
import ApiDesignSystemsYamlParser from '../parse/parsers/api-design-systems-yaml';
import OpenApiJson2Parser from '../parse/parsers/openapi-json-2';
Expand Down Expand Up @@ -57,6 +58,7 @@ options.resolve.strategies = [
OpenApi3_0ResolveStrategy(),
OpenApi3_1ResolveStrategy(),
AsyncApi2ResolveStrategy(),
ApiDOMResolveStrategy(),
];

options.dereference.strategies = [
Expand Down
2 changes: 1 addition & 1 deletion packages/apidom-reference/src/dereference/index.ts
Expand Up @@ -42,7 +42,7 @@ export const dereferenceApiDOM = async <T extends Element>(

const dereferenceStrategies = await plugins.filter(
'canDereference',
file,
[file, options],
options.dereference.strategies,
);

Expand Down
Expand Up @@ -43,8 +43,8 @@ const ApiDOMDereferenceStrategy: stampit.Stamp<IDereferenceStrategy> = stampit(

// clone reference set due the dereferencing process being mutable
if (
typeof options.dereference.dereferenceOpts.apidom?.clone === 'undefined' ||
options.dereference.dereferenceOpts.apidom?.clone
typeof options.dereference.strategyOpts.apidom?.clone === 'undefined' ||
options.dereference.strategyOpts.apidom?.clone
) {
const refsCopy = [...refSet.refs].map((ref) => {
return Reference({ ...ref, value: cloneDeep(ref.value) });
Expand Down
Expand Up @@ -86,7 +86,7 @@ const ApiDOMDereferenceVisitor = stampit({

async RefElement(refElement: RefElement, key: any, parent: any, path: any, ancestors: any[]) {
const refURI = toValue(refElement);
const refNormalizedURI = url.isURI(refURI) ? refURI : `#${refURI}`;
const refNormalizedURI = refURI.includes('#') ? refURI : `#${refURI}`;
const retrievalURI = this.toBaseURI(refNormalizedURI);
const isExternal = url.stripHash(this.reference.uri) !== retrievalURI;

Expand Down
12 changes: 8 additions & 4 deletions packages/apidom-reference/src/options/index.ts
Expand Up @@ -45,6 +45,10 @@ const defaultOptions: IReferenceOptions = {
* your own implementation, or remove any resolve strategy by removing it from the list.
*/
strategies: [],
/**
* These options are available in resolver strategy `canResolve` and `resolve` methods.
*/
strategyOpts: {},
/**
* Determines whether external references will be resolved.
* If this option is disabled, then none of above resolvers will be called.
Expand Down Expand Up @@ -73,16 +77,16 @@ const defaultOptions: IReferenceOptions = {
* your own implementation, or remove any dereference strategy by removing it from the list.
*/
strategies: [],
/**
* These options are available in dereference strategy `canDereference` and `dereference` methods.
*/
strategyOpts: {},
/**
* This option accepts an instance of pre-computed ReferenceSet.
* If provided it will speed up the dereferencing significantly as the external
* resolution doesn't need to happen anymore.
*/
refSet: null,
/**
* These options are merged with derecrence strategy plugin instance before the plugin is run.
*/
dereferenceOpts: {},
/**
* Determines the maximum depth of dereferencing.
* By default, there is no limit.
Expand Down
4 changes: 2 additions & 2 deletions packages/apidom-reference/src/parse/index.ts
Expand Up @@ -18,15 +18,15 @@ const parseFile = async (file: IFile, options: IReferenceOptions): Promise<Parse
return Object.assign(clonedParser, options.parse.parserOpts);
});

const parsers: IParser[] = await plugins.filter('canParse', file, optsBoundParsers);
const parsers: IParser[] = await plugins.filter('canParse', [file, options], optsBoundParsers);

// we couldn't find any parser for this File
if (isEmpty(parsers)) {
throw new UnmatchedResolverError(file.uri);
}

try {
const { plugin, result } = await plugins.run('parse', [file], parsers);
const { plugin, result } = await plugins.run('parse', [file, options], parsers);

// empty files handling
if (!plugin.allowEmpty && result.isEmpty) {
Expand Down
6 changes: 5 additions & 1 deletion packages/apidom-reference/src/resolve/index.ts
Expand Up @@ -40,7 +40,11 @@ export const resolveApiDOM = async <T extends Element>(
mediaType: options.parse.mediaType,
});

const resolveStrategies = await plugins.filter('canResolve', file, options.resolve.strategies);
const resolveStrategies = await plugins.filter(
'canResolve',
[file, options],
options.resolve.strategies,
);

// we couldn't find any resolver for this File
if (isEmpty(resolveStrategies)) {
Expand Down
44 changes: 44 additions & 0 deletions packages/apidom-reference/src/resolve/strategies/apidom/index.ts
@@ -0,0 +1,44 @@
import stampit from 'stampit';
import { isElement, visit, cloneDeep } from '@swagger-api/apidom-core';

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 ApiDOMResolveVisitor from './visitor';

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

const ApiDOMResolveStrategy: stampit.Stamp<IResolveStrategy> = stampit(ResolveStrategy, {
init() {
this.name = 'apidom';
},
methods: {
canResolve(file: IFile) {
return (
file.mediaType.startsWith('application/vnd.apidom') && isElement(file.parseResult?.result)
);
},

async resolve(file: IFile, options: IReferenceOptions) {
const referenceValue = options.resolve.strategyOpts.apidom?.clone
? cloneDeep(file.parseResult)
: file.parseResult;
const reference = Reference({ uri: file.uri, value: referenceValue });
const visitor = ApiDOMResolveVisitor({ reference, options });
const refSet = ReferenceSet();
refSet.add(reference);

await visitAsync(refSet.rootRef.value, visitor);

return refSet;
},
},
});

export default ApiDOMResolveStrategy;
@@ -0,0 +1,7 @@
import stampit from 'stampit';

import ApiDOMDereferenceVisitor from '../../../dereference/strategies/apidom/visitor';

const ApiDOMResolveVisitor = stampit(ApiDOMDereferenceVisitor);

export default ApiDOMResolveVisitor;
6 changes: 5 additions & 1 deletion packages/apidom-reference/src/resolve/util.ts
Expand Up @@ -19,7 +19,11 @@ export const readFile = async (file: IFile, options: IReferenceOptions): Promise
return Object.assign(clonedResolver, options.resolve.resolverOpts);
});

const resolvers: IResolver[] = await plugins.filter('canRead', file, optsBoundResolvers);
const resolvers: IResolver[] = await plugins.filter(
'canRead',
[file, options],
optsBoundResolvers,
);

// we couldn't find any resolver for this File
if (isEmpty(resolvers)) {
Expand Down
3 changes: 2 additions & 1 deletion packages/apidom-reference/src/types.ts
Expand Up @@ -91,14 +91,15 @@ export interface ReferenceResolveOptions {
resolvers: Array<Resolver>;
resolverOpts: Record<string, any>;
strategies: Array<ResolveStrategy>;
strategyOpts: Record<string, any>;
external: boolean;
maxDepth: number;
}

export interface ReferenceDereferenceOptions {
strategies: Array<DereferenceStrategy>;
strategyOpts: Record<string, any>;
refSet: null | ReferenceSet;
dereferenceOpts: Record<string, any>;
maxDepth: number;
}

Expand Down
5 changes: 2 additions & 3 deletions packages/apidom-reference/src/util/plugins.ts
@@ -1,17 +1,16 @@
import { invokeArgs } from 'ramda-adjunct';

import { File as IFile } from '../types';
import PluginError from '../errors/PluginError';

/**
* Filters the given plugins, returning only the ones return `true` for the given method.
*/
export const filter = async (
method: string,
file: IFile,
parameters: any[],
plugins: Array<any>,
): Promise<Array<any>> => {
const pluginResults = await Promise.all(plugins.map(invokeArgs([method], [file])));
const pluginResults = await Promise.all(plugins.map(invokeArgs([method], parameters)));
return plugins.filter((plugin, index) => pluginResults[index]);
};

Expand Down
12 changes: 5 additions & 7 deletions packages/apidom-reference/test/bundle/bundle.ts
Expand Up @@ -16,13 +16,11 @@ describe('bundle', function () {
);
const rootFilePath = path.join(fixturePath, 'root.json');

context('bundle', function () {
specify('should bundle a file', async function () {
const bundled = await bundle(rootFilePath, {
parse: { mediaType: mediaTypes.latest('json') },
});

assert.isTrue(isParseResultElement(bundled));
specify('should bundle a file', async function () {
const bundled = await bundle(rootFilePath, {
parse: { mediaType: mediaTypes.latest('json') },
});

assert.isTrue(isParseResultElement(bundled));
});
});
@@ -0,0 +1 @@
{"element":"object","content":[{"element":"member","content":{"key":{"element":"string","content":"element"},"value":{"element":"string","meta":{"id":{"element":"string","content":"unique-id"}},"content":"b"}}},{"element":"member","content":{"key":{"element":"string","content":"ref"},"value":{"element":"ref","attributes":{"path":{"element":"string","content":"element"}},"content":"unique-id"}}}]}
@@ -0,0 +1,3 @@
{
"a": "b"
}

0 comments on commit 9db6d3e

Please sign in to comment.