Skip to content

Commit 29a4d51

Browse files
authored
feat(reference): add code infrastructure for bundle feature (#3488)
Refs #692
1 parent e21794c commit 29a4d51

File tree

39 files changed

+573
-69
lines changed

39 files changed

+573
-69
lines changed

packages/apidom-reference/README.md

+244-11
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
`@swagger-api/apidom-reference` package contains advanced algorithms for semantic ApiDOM manipulations.
44
This package is divided into three (3) main components:
55

6-
- **Parse component**
7-
- **Resolve component**
8-
- **Dereference component**
6+
- **[Parse component](#parse-component)**
7+
- **[Resolve component](#resolve-component)**
8+
- **[Dereference component](#dereference-component)**
9+
- **[Bundle component](#bundle-component)**
910

1011
## Installation
1112

@@ -1263,7 +1264,7 @@ External resolution strategies can be added, removed, replaced or reordered. We'
12631264
## Dereference component
12641265

12651266
Dereferencing is a process of transcluding referencing element (internal or external) with a referenced element
1266-
using a specific [dereference strategy](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/dereference/strategies).
1267+
using a specific [dereference strategy](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/dereference/strategies). Simply put, dereferencing is a process of reference removal.
12671268
Dereferencing strategy is determined by asserting on `mediaType` option. [File Resolution](#file-resolution) (file content is read/fetched)
12681269
and [Parse component](#parse-component) (file content is parsed) are used under the hood.
12691270

@@ -1273,11 +1274,11 @@ and [Parse component](#parse-component) (file content is parsed) are used under
12731274
import { dereference } from '@swagger-api/apidom-reference';
12741275

12751276
await dereference('/home/user/oas.json', {
1276-
parse: { mediType: 'application/vnd.oai.openapi+json;version=3.1.0' },
1277-
}); // Promise<Element>
1277+
parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' },
1278+
}); // Promise<ParseResultElement>
12781279
```
12791280

1280-
**Dereferencing a HTTP(S) URL located on an internet:**
1281+
**Dereferencing an HTTP(S) URL located on an internet:**
12811282

12821283
```js
12831284
import { dereference } from '@swagger-api/apidom-reference';
@@ -1291,7 +1292,7 @@ await dereference('https://raw.githubusercontent.com/OAI/OpenAPI-Specification/m
12911292
},
12921293
},
12931294
},
1294-
}); // Promise<ReferenceSet>
1295+
}); // Promise<ParseResultElement>
12951296
```
12961297

12971298
**Dereferencing an ApiDOM fragment:**
@@ -1347,7 +1348,7 @@ const dereferenced = await dereferenceApiDOM(apidom, {
13471348
#### [Dereference strategies](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/dereference/strategies)
13481349

13491350
Dereference strategy determines how a document is internally or externally dereferenced. Depending on document `mediaType` option,
1350-
every strategy differs significantly. `Dereference component` comes with two (2) default dereference strategies.
1351+
every strategy differs significantly. `Dereference component` comes with four (4) default dereference strategies.
13511352

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

@@ -1508,7 +1509,7 @@ must conform to the following interface/shape:
15081509
},
15091510

15101511
// this method actually dereferences the file
1511-
async dereference(file: IFile, options: IReferenceOptions): Promise<Element> {
1512+
async dereference(file: IFile, options: IReferenceOptions): Promise<ParseResultElement> {
15121513
// ...implementation...
15131514
}
15141515
}
@@ -1596,7 +1597,7 @@ Dereference strategies can be added, removed, replaced or reordered. We've alrea
15961597

15971598
##### Increasing speed of dereference
15981599

1599-
Our two default dereference strategies are built on asynchronous sequential traversing of ApiDOM.
1600+
Our default dereference strategies are built on asynchronous sequential traversing of ApiDOM.
16001601
The total time of dereferencing is the sum of `traversing` + sum of `external resolution per referencing element`.
16011602
By having a huge number of external dependencies in your definition file, dereferencing can get quite slow.
16021603
Fortunately there is solution for this by running an `external resolution` first,
@@ -1617,3 +1618,235 @@ const dereferenced = await dereference('/home/user/oas.json', {
16171618
```
16181619

16191620
Total time of dereferencing is now the sum of `external resolution traversing` + `dereference traversing` + sum of `max external resolution per file`.
1621+
1622+
1623+
## Bundle component
1624+
1625+
Bundling is a convenient way to package up resources spread across multiple files in a single file
1626+
(**Compound Document**) using a specific [bundle strategy](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/bundle/strategies).
1627+
1628+
The bundling process for creating a Compound Document is defined as taking references (such as "$ref")
1629+
to an external Resource and embedding the referenced Resources within the referring document.
1630+
Bundling SHOULD be done in such a way that all URIs (used for referencing) in the base document
1631+
and any referenced/embedded documents do not require altering.
1632+
1633+
Bundling strategy is determined by asserting on `mediaType` option. [File Resolution](#file-resolution) (file content is read/fetched)
1634+
and [Parse component](#parse-component) (file content is parsed) are used under the hood.
1635+
1636+
**Bundling a file localed on a local filesystem:**
1637+
1638+
```js
1639+
import { bundle } from '@swagger-api/apidom-reference';
1640+
1641+
await bundle('/home/user/oas.json', {
1642+
parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' },
1643+
}); // Promise<ParseResultElement>
1644+
```
1645+
1646+
**Bundling an HTTP(S) URL located on an internet:**
1647+
1648+
```js
1649+
import { bundle } from '@swagger-api/apidom-reference';
1650+
1651+
await bundle('https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.1/webhook-example.json', {
1652+
parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' },
1653+
resolve: {
1654+
resolverOpts: {
1655+
axiosConfig: {
1656+
timeout: 10
1657+
},
1658+
},
1659+
},
1660+
}); // Promise<ParseResultElement>
1661+
```
1662+
1663+
#### [Bundle strategies](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/bundle/strategies)
1664+
1665+
Bundle strategy determines how a document is bundled into a Compound Document. Depending on document `mediaType` option,
1666+
every strategy differs significantly. `Bundle component` comes with single (1) default bundle strategy.
1667+
1668+
##### [openapi-3-1](https://github.com/swagger-api/apidom/tree/main/packages/apidom-reference/src/bundle/strategies/openapi-3-1)
1669+
1670+
Bundle strategy for bundling [OpenApi 3.1.0](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md) definitions.
1671+
1672+
Supported media types:
1673+
1674+
```js
1675+
[
1676+
'application/vnd.oai.openapi;version=3.1.0',
1677+
'application/vnd.oai.openapi+json;version=3.1.0',
1678+
'application/vnd.oai.openapi+yaml;version=3.1.0'
1679+
]
1680+
```
1681+
1682+
##### Bundle strategies execution order
1683+
1684+
It's important to understand that default bundle strategies are run in specific order. The order is determined
1685+
by the `options.bundle.strategies` option.
1686+
Every strategy is pulled from `options.bundle.strategies` option, and it's `canBundle` method is called to determine
1687+
whether the strategy can bundle the URI. If `canBundle` returns `true`, `bundle` method of strategy is called
1688+
and result from bundling is returned. No subsequent strategies are run. If `canBundle` returns
1689+
`false`, next strategy is pulled and this process is repeated until one of the strategy's `canBundle` method
1690+
returns `true` or until entire list of strategies is exhausted (throws error).
1691+
1692+
```js
1693+
[
1694+
OpenApi3_1BundleStrategy(),
1695+
]
1696+
```
1697+
Most specific strategies are listed first, most generic are listed last.
1698+
1699+
It's possible to **change** strategies **order globally** by mutating global `bundle` option:
1700+
1701+
```js
1702+
import { options } from '@swagger-api/apidom-reference';
1703+
import OpenApi3_1BundleStrategy from '@swagger-api/apidom-reference/bundle/strategies/openapi-3-1'
1704+
1705+
options.dereference.strategies = [
1706+
OpenApi3_1DereferenceStrategy(),
1707+
];
1708+
```
1709+
1710+
To **change** the strategies **order** on ad-hoc basis:
1711+
1712+
```js
1713+
import { bundle } from '@swagger-api/apidom-reference';
1714+
import OpenApi3_1BundleStrategy from '@swagger-api/apidom-reference/bundle/strategies/openapi-3-1'
1715+
1716+
await bundle('/home/user/oas.json', {
1717+
parse: {
1718+
mediaType: 'application/vnd.oai.openapi+json;version=3.1.0',
1719+
},
1720+
bundle: {
1721+
strategies: [
1722+
OpenApi3_1BundleStrategy(),
1723+
]
1724+
}
1725+
});
1726+
```
1727+
##### Creating new bundle strategy
1728+
1729+
Bundle component can be extended by additional strategies. Every strategy is an object that
1730+
must conform to the following interface/shape:
1731+
1732+
```typescript
1733+
{
1734+
// uniquely identifies this plugin
1735+
name: string,
1736+
1737+
// this method is called to determine whether the strategy can bundle the file
1738+
canBundle(file: IFile): boolean {
1739+
// ...implementation...
1740+
},
1741+
1742+
// this method actually bundles the file
1743+
async bundle(file: IFile, options: IReferenceOptions): Promise<ParseResultElement> {
1744+
// ...implementation...
1745+
}
1746+
}
1747+
```
1748+
1749+
New strategy is then provided as an option to the `bundle` function:
1750+
1751+
```js
1752+
import { bundle, options } from '@swagger-api/apidom-reference';
1753+
1754+
const myCustomBundleStrategy = {
1755+
name: 'myCustomByndleStrategy',
1756+
canBundle(file) {
1757+
return true;
1758+
},
1759+
async bundle(file, options: IReferenceOptions) {
1760+
// implementation of bundling
1761+
}
1762+
};
1763+
1764+
await bundle('/home/user/oas.json', {
1765+
parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' },
1766+
bundle: {
1767+
strategies: [...options.bundle.strategies, myCustomBundleStrategy],
1768+
}
1769+
});
1770+
```
1771+
1772+
In this particular example we're adding our custom strategy as the last strategy
1773+
to the available default bundle strategy list, so there's a good chance that one of the
1774+
default strategies detects that it can bundle the `/home/user/oas.json` file,
1775+
bundles it and returns a bundled element.
1776+
1777+
If you want to force execution of your strategy, add it as a first one:
1778+
1779+
```js
1780+
import { bundle, options } from '@swagger-api/apidom-reference';
1781+
1782+
const myCustomBundleStrategy = {
1783+
name: 'myCustomBundleStrategy',
1784+
canBundle(file) {
1785+
return true;
1786+
},
1787+
async bundle(file, options: IReferenceOptions) {
1788+
// implementation of bundling
1789+
}
1790+
};
1791+
1792+
1793+
await bundle('/home/user/oas.json', {
1794+
parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' },
1795+
bundle: {
1796+
strategies: [myCustomBundleStrategy, ...options.bundle.strategies],
1797+
}
1798+
});
1799+
```
1800+
1801+
To override the default strategies entirely, set `myCustomBundleStrategy` strategy to be the only one available:
1802+
1803+
```js
1804+
import { bundle } from '@swagger-api/apidom-reference';
1805+
1806+
const myCustomBundleStrategy = {
1807+
name: 'myCustomBundleStrategy',
1808+
canBundle(file) {
1809+
return true;
1810+
},
1811+
async bundle(file, options: IReferenceOptions) {
1812+
// implementation of bundling
1813+
}
1814+
};
1815+
1816+
await bundle('/home/user/oas.json', {
1817+
parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' },
1818+
bundle: {
1819+
strategies: [myCustomBundleStrategy],
1820+
}
1821+
});
1822+
```
1823+
1824+
New strategies can be based on a predefined stamp called [BundleStrategy](https://github.com/swagger-api/apidom/blob/main/packages/apidom-reference/src/bundle/strategies/BundleStrategy.ts).
1825+
1826+
##### Manipulating bundle strategies
1827+
1828+
Bundle strategies can be added, removed, replaced or reordered. We've already covered these techniques in [Manipulating parser plugins section](#manipulating-parser-plugins).
1829+
1830+
##### Increasing speed of bundling
1831+
1832+
Our default bundling strategies are built on asynchronous sequential traversing of ApiDOM.
1833+
The total time of bundling is the sum of `traversing` + sum of `external resolution per referencing element`.
1834+
By having a huge number of external dependencies in your definition file, bundling can get quite slow.
1835+
Fortunately there is solution for this by running an `external resolution` first,
1836+
and passing its result to bundling via an option. External resolution is built on asynchronous parallel traversal (on single file),
1837+
so it's theoretically always faster on huge amount of external dependencies than the bundling.
1838+
1839+
```js
1840+
import { resolve, bundle } from '@swagger-api/apidom-reference';
1841+
1842+
const refSet = await resolve('/home/user/oas.json', {
1843+
parse: { mediType: 'application/vnd.oai.openapi+json;version=3.1.0' },
1844+
});
1845+
1846+
const bundled = await bundle('/home/user/oas.json', {
1847+
parse: { mediaType: 'application/vnd.oai.openapi+json;version=3.1.0' },
1848+
bundle: { refSet },
1849+
});
1850+
```
1851+
1852+
Total time of bundling is now the sum of `external resolution traversing` + `bundle traversing` + sum of `max external resolution per file`.

packages/apidom-reference/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@
174174
"import": "./es/dereference/strategies/openapi-3-1/selectors/uri.mjs",
175175
"require": "./cjs/dereference/strategies/openapi-3-1/selectors/uri.cjs",
176176
"types": "./types/dereference/strategies/openapi-3-1/selectors/uri.d.ts"
177+
},
178+
"./bundle/strategies/openapi-3-1": {
179+
"import": "./es/bundle/strategies/openapi-3-1/index.mjs",
180+
"require": "./cjs/bundle/strategies/openapi-3-1/index.cjs",
181+
"types": "./types/bundle/strategies/openapi-3-1/index.d.ts"
177182
}
178183
},
179184
"imports": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { isEmpty, propEq } from 'ramda';
2+
import { ParseResultElement } from '@swagger-api/apidom-core';
3+
4+
import File from '../util/File';
5+
import * as plugins from '../util/plugins';
6+
import UnmatchedBundleStrategyError from '../errors/UnmatchedBundleStrategyError';
7+
import BundleError from '../errors/BundleError';
8+
import { ReferenceOptions as IReferenceOptions } from '../types';
9+
import parse from '../parse';
10+
import { merge as mergeOptions } from '../options/util';
11+
import * as url from '../util/url';
12+
13+
/**
14+
* Bundle a file with all its external references to a compound document.
15+
*/
16+
const bundle = async (uri: string, options: IReferenceOptions): Promise<ParseResultElement> => {
17+
const { refSet } = options.bundle;
18+
const sanitizedURI = url.sanitize(uri);
19+
const mergedOptions = mergeOptions(options, { resolve: { baseURI: sanitizedURI } });
20+
let parseResult;
21+
22+
// if refSet was provided, use it to avoid unnecessary parsing
23+
if (refSet !== null && refSet.has(sanitizedURI)) {
24+
// @ts-ignore
25+
({ value: parseResult } = refSet.find(propEq(sanitizedURI, 'uri')));
26+
} else {
27+
parseResult = await parse(uri, mergedOptions);
28+
}
29+
30+
const file = File({
31+
uri: mergedOptions.resolve.baseURI,
32+
parseResult,
33+
mediaType: mergedOptions.parse.mediaType,
34+
});
35+
36+
const bundleStrategies = await plugins.filter('canBundle', file, mergedOptions.bundle.strategies);
37+
38+
// we couldn't find any bundle strategy for this File
39+
if (isEmpty(bundleStrategies)) {
40+
throw new UnmatchedBundleStrategyError(file.uri);
41+
}
42+
43+
try {
44+
const { result } = await plugins.run('bundle', [file, mergedOptions], bundleStrategies);
45+
return result;
46+
} catch (error: any) {
47+
throw new BundleError(`Error while bundling file "${file.uri}"`, { cause: error });
48+
}
49+
};
50+
51+
export default bundle;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import stampit from 'stampit';
2+
import { NotImplementedError } from '@swagger-api/apidom-error';
3+
4+
import { BundleStrategy as IBundleStrategy } from '../../types';
5+
6+
const BundleStrategy: stampit.Stamp<IBundleStrategy> = stampit({
7+
props: {
8+
name: null,
9+
},
10+
methods: {
11+
canBundle() {
12+
return false;
13+
},
14+
15+
async bundle(): Promise<never> {
16+
throw new NotImplementedError(
17+
'bundle method in BundleStrategy stamp is not yet implemented.',
18+
);
19+
},
20+
},
21+
});
22+
23+
export default BundleStrategy;

0 commit comments

Comments
 (0)