Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(reference): add resolver strategy that handles abstract RefElement #3893

Merged
merged 4 commits into from
Mar 5, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
75 changes: 74 additions & 1 deletion packages/apidom-reference/README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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));
});
});
Original file line number Diff line number Diff line change
@@ -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"}}}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"a": "b"
}