@team-supercharge/oasg 11.0.0 → 12.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -58
- package/bin/merger.js +3 -1
- package/bin/oasg +46 -6
- package/bin/openapi-target.js +4 -108
- package/bin/process-source.js +138 -0
- package/config.schema.yml +10 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,11 +70,7 @@ For detailed configuration instructions please refer to the [Configuration](#con
|
|
|
70
70
|
|
|
71
71
|
## Configure Linter
|
|
72
72
|
|
|
73
|
-
Create a `.spectral.
|
|
74
|
-
|
|
75
|
-
```yaml
|
|
76
|
-
extends: '@team-supercharge/oasg/ruleset'
|
|
77
|
-
```
|
|
73
|
+
Create a `.spectral.js` file in the repo's root directory with the following minimal content:
|
|
78
74
|
|
|
79
75
|
```js
|
|
80
76
|
import oasgRuleset from '@team-supercharge/oasg/ruleset';
|
|
@@ -84,9 +80,9 @@ export default {
|
|
|
84
80
|
};
|
|
85
81
|
```
|
|
86
82
|
|
|
87
|
-
The file is used for configuring [Spectral](https://stoplight.io/open-source/spectral/) on a per-project basis and will enable usage of
|
|
83
|
+
The file is used for configuring [Spectral](https://stoplight.io/open-source/spectral/) on a per-project basis and will enable usage of the base _OASg_ ruleset and enables to add project-specific rules as well. For detailed instructions on how to use custom rules refer to the [Linter rules](#linter-rules) section below.
|
|
88
84
|
|
|
89
|
-
|
|
85
|
+
> ⚠️ The YAML ruleset format is highly discouraged to be used, as it won't work seamlessly [when being used with Docker](#use-with-docker).
|
|
90
86
|
|
|
91
87
|
## Use with Docker
|
|
92
88
|
|
|
@@ -160,17 +156,43 @@ $ npx oasg publish [target]
|
|
|
160
156
|
|
|
161
157
|
_OASg_ makes use of the awesome [Spectral](https://stoplight.io/open-source/spectral/) project to provide linting capabilities and comes with a set of more strict default rules we call the _OASg Ruleset_ which is defined in the file [ruleset/ruleset.js](ruleset/ruleset.js) and exported as `@team-supercharge/oasg/ruleset` for the outside world.
|
|
162
158
|
|
|
163
|
-
The project's `.spectral.
|
|
159
|
+
The project's `.spectral.js` file in the API repository should be always configured to extend the default _OASg Ruleset_ using the methods described in the [Configure linter](#configure-linter) section.
|
|
164
160
|
|
|
165
161
|
Rules defined in the ruleset can be split into two categories:
|
|
166
162
|
|
|
167
163
|
* **default rules** - rules that are enabled by default, can be disabled with the `rule-name: off` syntax
|
|
168
164
|
* **opt-in rules** - rules that need to be explicitly enabled with the `rule-name: true` syntax
|
|
169
165
|
|
|
170
|
-
You can further customize the ruleset in `.spectral.
|
|
166
|
+
You can further customize the ruleset in `.spectral.js` according to the [documentation](https://meta.stoplight.io/docs/spectral/docs/guides/4-custom-rulesets.md) using the `rules:` property.
|
|
171
167
|
|
|
172
168
|
An example configuration can be found here:
|
|
173
169
|
|
|
170
|
+
```js
|
|
171
|
+
import oasgRuleset from '@team-supercharge/oasg/ruleset';
|
|
172
|
+
|
|
173
|
+
import { truthy } from '@stoplight/spectral-functions';
|
|
174
|
+
|
|
175
|
+
export default {
|
|
176
|
+
extends: oasgRuleset,
|
|
177
|
+
rules: {
|
|
178
|
+
'oasg-object-names-pascal-case': false, // disables a default rule - don't do this in general :)
|
|
179
|
+
'oasg-object-names-api-model-suffix': true, // enables a non-default rule
|
|
180
|
+
|
|
181
|
+
'oasg-path-casing-kebab': false, // switches a default rule to another one
|
|
182
|
+
'oasg-path-casing-pascal': true,
|
|
183
|
+
|
|
184
|
+
'my-rule-name': { // adds a fully custom rule which applies to only your project
|
|
185
|
+
description: 'Tags must have a description.',
|
|
186
|
+
given: '$.tags[*]',
|
|
187
|
+
severity: 'error',
|
|
188
|
+
then: {
|
|
189
|
+
field: 'description',
|
|
190
|
+
function: truthy
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
|
|
174
196
|
```yaml
|
|
175
197
|
extends: '@team-supercharge/oasg/ruleset'
|
|
176
198
|
|
|
@@ -285,8 +307,51 @@ Common source parameters
|
|
|
285
307
|
|-|-|-|-|
|
|
286
308
|
| id | Unique identifier | Y | - |
|
|
287
309
|
| type | Source type: `simple` / `merged` | N | `simple` |
|
|
310
|
+
| bundle | Bundle specification into a single file | N | `true` |
|
|
311
|
+
| sortSchemas | Sort `components.schemas` alphabetically | N | `true` |
|
|
312
|
+
| decorators | Array of files for decorator functions | N | `[]` |
|
|
313
|
+
| cleanup | Cleans specification from unused paths, tags and schemas | N | `true` |
|
|
288
314
|
| overrides | Override properties of the OpenApi file | N | - |
|
|
289
315
|
|
|
316
|
+
The source specification is by default bundled into a single file (resolving external dependencies) using the [@redocly/cli package's](https://www.npmjs.com/package/@redocly/cli) `bundle` command.
|
|
317
|
+
|
|
318
|
+
> ⚠️ It is higly recommended to keep `bundle` on if you plan to use the `openapi` target type, as external refs won't be part of the target artifact.
|
|
319
|
+
|
|
320
|
+
Decorator files must export a `decorate(document)` function which transforms the parsed OpenAPI document and returns it at the end of the function.
|
|
321
|
+
|
|
322
|
+
Full example with decorators:
|
|
323
|
+
|
|
324
|
+
```json
|
|
325
|
+
{
|
|
326
|
+
"id": "platform-api",
|
|
327
|
+
"type": "merged",
|
|
328
|
+
"inputs": [
|
|
329
|
+
"api/*.openapi.yaml"
|
|
330
|
+
],
|
|
331
|
+
"bundle": false,
|
|
332
|
+
"sortSchemas": false,
|
|
333
|
+
"decorators": ["decorators/one", "decorators/two"],
|
|
334
|
+
"cleanup": false
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
// decorators/one.js
|
|
340
|
+
|
|
341
|
+
function decorate(document) {
|
|
342
|
+
document.info.title = 'My Custom Decorated Title';
|
|
343
|
+
|
|
344
|
+
return document;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
exports.decorate = decorate;
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
After the decorators have run, the specification is by default cleaned up (can be turned off by setting the `"cleanup": false` option):
|
|
351
|
+
- `paths` with no methods in them are removed
|
|
352
|
+
- `tags` with no endpoints using them are removed
|
|
353
|
+
- `components` that are unused are removed (only if `bundle` option is also enabled)
|
|
354
|
+
|
|
290
355
|
### Source Types
|
|
291
356
|
|
|
292
357
|
#### Simple
|
|
@@ -688,74 +753,51 @@ describe('Auth', function () {
|
|
|
688
753
|
|Parameter| Description| Required | Default |
|
|
689
754
|
|-|-|-|-|
|
|
690
755
|
| fileName | Name of the generated file | N | `openapi.yaml` |
|
|
691
|
-
| bundle | Bundle specification into a single file | N | `true` |
|
|
692
|
-
| sortSchemas | Sort `components.schemas` alphabetically | N | `true` |
|
|
693
|
-
| decorators | Array of files for decorator functions | N | `[]` |
|
|
694
|
-
| cleanup | Cleans specification from unused paths, tags and schemas | N | `true` |
|
|
695
|
-
|
|
696
|
-
The target specification is by default bundled into a single file (resolving external dependencies) using the [@redocly/cli package's](https://www.npmjs.com/package/@redocly/cli) `bundle` command.
|
|
697
|
-
|
|
698
|
-
> ⚠️ It is higly recommended to keep `bundle` on, as external refs won't be part of the target artifact.
|
|
699
|
-
|
|
700
|
-
Decorator files must export a `decorate(document)` function which transforms the parsed OpenAPI document and returns it at the end of the function.
|
|
701
|
-
|
|
702
|
-
Full example with decorators:
|
|
703
756
|
|
|
704
|
-
|
|
705
|
-
{
|
|
706
|
-
"id": "api-docs",
|
|
707
|
-
"type": "openapi",
|
|
708
|
-
"source": "source-simple",
|
|
709
|
-
"fileName": "my-openapi-file.yaml",
|
|
710
|
-
"bundle": false,
|
|
711
|
-
"sortSchemas": false,
|
|
712
|
-
"decorators": ["decorators/one", "decorators/two"],
|
|
713
|
-
"cleanup": false
|
|
714
|
-
}
|
|
715
|
-
```
|
|
716
|
-
|
|
717
|
-
```js
|
|
718
|
-
// decorators/one.js
|
|
757
|
+
---
|
|
719
758
|
|
|
720
|
-
|
|
721
|
-
document.info.title = 'My Custom Decorated Title';
|
|
759
|
+
# Migration Guide
|
|
722
760
|
|
|
723
|
-
|
|
724
|
-
}
|
|
761
|
+
This section covers the breaking changes and their migrations across major version upgrades.
|
|
725
762
|
|
|
726
|
-
|
|
727
|
-
```
|
|
763
|
+
## From `11.x.x` to `12.0.0`
|
|
728
764
|
|
|
729
|
-
|
|
730
|
-
- `paths` with no methods in them are removed
|
|
731
|
-
- `tags` with no endpoints using them are removed
|
|
732
|
-
- `components` that are unused are removed (only if `bundle` option is also enabled)
|
|
765
|
+
The following options from the `openapi` target type has been moved to the [Source](#source) configuration.
|
|
733
766
|
|
|
734
|
-
|
|
767
|
+
* `bundle`
|
|
768
|
+
* `sortSchemas`
|
|
769
|
+
* `decorators`
|
|
770
|
+
* `cleanup`
|
|
735
771
|
|
|
736
|
-
|
|
772
|
+
Please update your `config.json` accordingly: move these properties - if they exist - to the respective `source` configuration.
|
|
737
773
|
|
|
738
|
-
|
|
774
|
+
> ⚠️ As the `bundle`, `sortSchemas` and `cleanup` flags are **enabled by default**, even without using custom decorators some normalization steps are applied to the source specification before generating the targets. If this causes any problems in your project (although highly unlikely it will) consider **disable** these flags.
|
|
739
775
|
|
|
740
776
|
## From `10.x.x` to `11.0.0`
|
|
741
777
|
|
|
742
778
|
### Linting
|
|
743
779
|
|
|
744
|
-
|
|
780
|
+
The linter configuration previously stored in `.spectral.yaml` needs to be updated to the JS format:
|
|
781
|
+
|
|
782
|
+
1. rename the file to `.spectral.js` (don't forget to update `spectral.rulesetFile` in `.vscode/settings.json` if used!)
|
|
783
|
+
2. follow the [instructions](#configure-linter) for extending the base _OASg_ ruleset in JS
|
|
784
|
+
3. migrate project-specific custom [rules](#custom-rules) and [functions](#custom-functions) to JS format
|
|
785
|
+
|
|
786
|
+
The typical minimal configuration from this in YAML
|
|
745
787
|
|
|
746
788
|
```yaml
|
|
747
|
-
# old one
|
|
748
789
|
extends: '@team-supercharge/oasg/rules/default.yaml'
|
|
749
|
-
|
|
750
|
-
# new one
|
|
751
|
-
extends: '@team-supercharge/oasg/ruleset'
|
|
752
790
|
```
|
|
753
791
|
|
|
754
|
-
|
|
792
|
+
must become this in JS (if no custom rules or functions are used):
|
|
755
793
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
794
|
+
```js
|
|
795
|
+
import oasgRuleset from '@team-supercharge/oasg/ruleset';
|
|
796
|
+
|
|
797
|
+
export default {
|
|
798
|
+
extends: oasgRuleset
|
|
799
|
+
}
|
|
800
|
+
```
|
|
759
801
|
|
|
760
802
|
## From `9.x.x` to `10.0.0`
|
|
761
803
|
|
package/bin/merger.js
CHANGED
|
@@ -26,6 +26,8 @@ async function merge(source) {
|
|
|
26
26
|
|
|
27
27
|
dump(document, outFile);
|
|
28
28
|
|
|
29
|
+
console.log(`merged input files =>\n ${inputFiles.join('\n ')}\ninto\n ${outFile}\n`);
|
|
30
|
+
|
|
29
31
|
return outFile;
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -41,7 +43,7 @@ async function getInputFilesWithGlob(inputs) {
|
|
|
41
43
|
const inputFiles = [];
|
|
42
44
|
for (const input of inputs) {
|
|
43
45
|
const files = await glob(input);
|
|
44
|
-
inputFiles.push(...files);
|
|
46
|
+
inputFiles.push(...files.sort());
|
|
45
47
|
}
|
|
46
48
|
const uniqueInputFiles = inputFiles.filter((value, index, self) => self.indexOf(value) === index);
|
|
47
49
|
return uniqueInputFiles;
|
package/bin/oasg
CHANGED
|
@@ -18,6 +18,7 @@ const { exec } = require(`${__dirname}/exec.js`);
|
|
|
18
18
|
const { merge } = require(`${__dirname}/merger.js`);
|
|
19
19
|
const { applyOverrides } = require(`${__dirname}/overrider.js`);
|
|
20
20
|
const { openApiTarget } = require(`${__dirname}/openapi-target.js`);
|
|
21
|
+
const { processSource } = require(`${__dirname}/process-source.js`);
|
|
21
22
|
const { globSync } = require('glob');
|
|
22
23
|
|
|
23
24
|
const projectPackageJson = JSON.parse(fs.readFileSync('package.json'));
|
|
@@ -215,6 +216,25 @@ async function parseConfig() {
|
|
|
215
216
|
config.sources = [DEFAULT_SOURCE];
|
|
216
217
|
}
|
|
217
218
|
|
|
219
|
+
// set default source values
|
|
220
|
+
config.sources.forEach(s => {
|
|
221
|
+
if (s.bundle === undefined) {
|
|
222
|
+
s.bundle = true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (s.sortSchemas === undefined) {
|
|
226
|
+
s.sortSchemas = true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (s.decorators === undefined) {
|
|
230
|
+
s.decorators = [];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (s.cleanup === undefined) {
|
|
234
|
+
s.cleanup = true;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
218
238
|
// set default source to targets with undefined
|
|
219
239
|
config.targets.forEach(t => {
|
|
220
240
|
if (!t.source) {
|
|
@@ -313,11 +333,19 @@ async function parseConfig() {
|
|
|
313
333
|
return config;
|
|
314
334
|
}
|
|
315
335
|
|
|
316
|
-
async function buildSources() {
|
|
336
|
+
async function buildSources(sourceIds) {
|
|
317
337
|
const sources = {};
|
|
318
338
|
|
|
319
339
|
for (var i = 0; i < config.sources.length; i++) {
|
|
320
340
|
const source = config.sources[i];
|
|
341
|
+
|
|
342
|
+
// only build sources that are defined
|
|
343
|
+
if (!sourceIds.includes(source.id)) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log(`\n=====\n id:\t\t${source.id}\n type:\t\t${source.type}\n bundle:\t${source.bundle}\n sortSchemas:\t${source.sortSchemas}\n decorators: \t${JSON.stringify(source.decorators)}\n cleanup:\t${source.cleanup}\n---\n`);
|
|
348
|
+
|
|
321
349
|
let file;
|
|
322
350
|
|
|
323
351
|
switch (source.type) {
|
|
@@ -330,6 +358,7 @@ async function buildSources() {
|
|
|
330
358
|
break;
|
|
331
359
|
}
|
|
332
360
|
|
|
361
|
+
file = processSource(source, file, VERSION);
|
|
333
362
|
file = await applyOverrides(source, file);
|
|
334
363
|
|
|
335
364
|
sources[source.id] = file;
|
|
@@ -368,7 +397,7 @@ async function serve(argv) {
|
|
|
368
397
|
checkSourceId(sourceId);
|
|
369
398
|
|
|
370
399
|
// handle input
|
|
371
|
-
const sources = await buildSources();
|
|
400
|
+
const sources = await buildSources([sourceId]);
|
|
372
401
|
const input = sources[sourceId];
|
|
373
402
|
const document = YAML.load(input);
|
|
374
403
|
|
|
@@ -415,7 +444,7 @@ async function proxy(argv) {
|
|
|
415
444
|
checkSourceId(sourceId);
|
|
416
445
|
|
|
417
446
|
// handle input
|
|
418
|
-
const sources = await buildSources();
|
|
447
|
+
const sources = await buildSources([sourceId]);
|
|
419
448
|
const input = sources[sourceId];
|
|
420
449
|
const document = YAML.load(input);
|
|
421
450
|
|
|
@@ -446,7 +475,7 @@ async function generate(argv) {
|
|
|
446
475
|
VERSION = determineArtifactVersion(argv);
|
|
447
476
|
const preRelease = determinePreRelease(argv);
|
|
448
477
|
|
|
449
|
-
const sources = await buildSources();
|
|
478
|
+
const sources = await buildSources(sourceIdsFromTargetIds(targetIds));
|
|
450
479
|
|
|
451
480
|
console.log(`generate targets: ${targetIds}`);
|
|
452
481
|
|
|
@@ -455,7 +484,7 @@ async function generate(argv) {
|
|
|
455
484
|
|
|
456
485
|
// handle docs target
|
|
457
486
|
if (target.type === 'openapi') {
|
|
458
|
-
openApiTarget(target, sources[target.source]
|
|
487
|
+
openApiTarget(target, sources[target.source]);
|
|
459
488
|
return;
|
|
460
489
|
}
|
|
461
490
|
|
|
@@ -478,7 +507,7 @@ async function generate(argv) {
|
|
|
478
507
|
// publish
|
|
479
508
|
async function publish(argv) {
|
|
480
509
|
const targetIds = determineTargetIds(argv);
|
|
481
|
-
const sources = await buildSources();
|
|
510
|
+
const sources = await buildSources(sourceIdsFromTargetIds(targetIds));
|
|
482
511
|
const preRelease = determinePreRelease(argv);
|
|
483
512
|
|
|
484
513
|
console.log(`publish targets: ${targetIds}`);
|
|
@@ -630,6 +659,17 @@ function determineTargetIds(argv) {
|
|
|
630
659
|
return targetIds;
|
|
631
660
|
}
|
|
632
661
|
|
|
662
|
+
function sourceIdsFromTargetIds(targetIds) {
|
|
663
|
+
const sourceIds = [];
|
|
664
|
+
targetIds.forEach(targetId => {
|
|
665
|
+
const target = config.targets.find(t => t.id === targetId);
|
|
666
|
+
if (target.source) {
|
|
667
|
+
sourceIds.push(target.source);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
return sourceIds;
|
|
671
|
+
}
|
|
672
|
+
|
|
633
673
|
function validTargetIds() {
|
|
634
674
|
return config.targets.map(t => t.id);
|
|
635
675
|
}
|
package/bin/openapi-target.js
CHANGED
|
@@ -2,128 +2,24 @@ const fs = require('fs');
|
|
|
2
2
|
const SwaggerParser = require("@apidevtools/swagger-parser");
|
|
3
3
|
|
|
4
4
|
const { dump } = require(`${__dirname}/dump.js`);
|
|
5
|
-
const { exec } = require(`${__dirname}/exec.js`);
|
|
6
5
|
|
|
7
|
-
function
|
|
8
|
-
exec(`npx redocly bundle ${source} --keep-url-references${remove ? ' --remove-unused-components' : ''} --output ${target}`, 'pipe');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async function openApiTarget(target, source, version) {
|
|
6
|
+
async function openApiTarget(target, source) {
|
|
12
7
|
const outDir = `out/${target.id}`;
|
|
13
8
|
let outFile = target.fileName || `openapi.yaml`;
|
|
14
9
|
outFile = `${outDir}/${outFile}`;
|
|
15
10
|
|
|
16
11
|
if (fs.existsSync(outDir)) {
|
|
17
|
-
fs.
|
|
12
|
+
fs.rmSync(outDir, { recursive: true })
|
|
18
13
|
}
|
|
19
14
|
fs.mkdirSync(outDir, { recursive: true });
|
|
20
15
|
|
|
21
|
-
// bundle spec to a single file by default
|
|
22
|
-
const bundle = target.bundle === undefined || target.bundle;
|
|
23
|
-
if (bundle) {
|
|
24
|
-
bundleSpec(source, outFile, false);
|
|
25
|
-
console.log(`bundled specification`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
16
|
// parse document
|
|
29
|
-
let document = await SwaggerParser.parse(
|
|
30
|
-
|
|
31
|
-
// set version of target document
|
|
32
|
-
document.info.version = version;
|
|
33
|
-
|
|
34
|
-
// sort schemas alphabeticaly
|
|
35
|
-
const sortSchemas = target.sortSchemas === undefined || target.sortSchemas;
|
|
36
|
-
if (sortSchemas) {
|
|
37
|
-
document.components.schemas = Object.keys(document.components.schemas)
|
|
38
|
-
.sort()
|
|
39
|
-
.reduce((obj, key) => { obj[key] = document.components.schemas[key]; return obj; }, {});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// apply custom decorators
|
|
43
|
-
const decorators = target.decorators || [];
|
|
44
|
-
decorators.forEach(d => {
|
|
45
|
-
const decoratorFile = `${d}.js`;
|
|
46
|
-
const decoratorPath = `${process.cwd()}/${decoratorFile}`;
|
|
47
|
-
|
|
48
|
-
if (!fs.existsSync(decoratorPath)) {
|
|
49
|
-
console.error(`skipped decorator '${decoratorFile}' - file does not exist`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const decorator = require(decoratorPath);
|
|
54
|
-
|
|
55
|
-
if (!decorator.decorate) {
|
|
56
|
-
console.error(`skipped decorator '${decoratorFile}' - must export a 'decorate(document)' function`);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
document = decorator.decorate(document);
|
|
17
|
+
let document = await SwaggerParser.parse(source);
|
|
61
18
|
|
|
62
|
-
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// clean up unused things
|
|
66
|
-
const cleanup = target.cleanup === undefined || target.cleanup;
|
|
67
|
-
if (cleanup) {
|
|
68
|
-
const removedPaths = [];
|
|
69
|
-
const removedTags = [];
|
|
70
|
-
const usedTags = [];
|
|
71
|
-
|
|
72
|
-
// remove empty paths & gather tags
|
|
73
|
-
const paths = document.paths;
|
|
74
|
-
for (const pathKey in paths) {
|
|
75
|
-
const path = document.paths[pathKey];
|
|
76
|
-
|
|
77
|
-
if (Object.keys(path).length === 0) {
|
|
78
|
-
removedPaths.push(pathKey);
|
|
79
|
-
delete paths[pathKey];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
for (const methodKey in path) {
|
|
83
|
-
const operation = path[methodKey];
|
|
84
|
-
|
|
85
|
-
operation.tags.forEach(tag => {
|
|
86
|
-
if (!usedTags.includes(tag)) {
|
|
87
|
-
usedTags.push(tag);
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (removedPaths.length !== 0) {
|
|
94
|
-
console.log(`cleaned up unused paths:\n ${removedPaths.join('\n ')}`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// remouve unused tags
|
|
98
|
-
const tags = [...document.tags];
|
|
99
|
-
tags.forEach(tag => {
|
|
100
|
-
if (!usedTags.includes(tag.name)) {
|
|
101
|
-
removedTags.push(tag.name);
|
|
102
|
-
document.tags.splice(document.tags.indexOf(tag), 1);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
if (removedTags.length !== 0) {
|
|
107
|
-
console.log(`cleaned up unused tags:\n ${removedTags.join('\n ')}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
19
|
+
// target-specific functions, e.g. write as JSON
|
|
110
20
|
|
|
111
21
|
// write file
|
|
112
22
|
dump(document, outFile);
|
|
113
|
-
|
|
114
|
-
// re-bundle to remove unused components
|
|
115
|
-
if (cleanup && bundle) {
|
|
116
|
-
console.log(`re-bundling specification to remove unused components`);
|
|
117
|
-
let currentSize;
|
|
118
|
-
let bundledSize;
|
|
119
|
-
do {
|
|
120
|
-
currentSize = fs.statSync(outFile).size;
|
|
121
|
-
bundleSpec(outFile, outFile, true);
|
|
122
|
-
console.log('.');
|
|
123
|
-
bundledSize = fs.statSync(outFile).size;
|
|
124
|
-
}
|
|
125
|
-
while (bundledSize !== currentSize)
|
|
126
|
-
}
|
|
127
23
|
}
|
|
128
24
|
|
|
129
25
|
exports.openApiTarget = openApiTarget;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const SwaggerParser = require("@apidevtools/swagger-parser");
|
|
4
|
+
|
|
5
|
+
const { dump } = require(`${__dirname}/dump.js`);
|
|
6
|
+
const { exec } = require(`${__dirname}/exec.js`);
|
|
7
|
+
|
|
8
|
+
function bundleSpec(source, target, remove) {
|
|
9
|
+
exec(`npx redocly bundle ${source} --keep-url-references${remove ? ' --remove-unused-components' : ''} --output ${target}`, 'pipe');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function processSource(source, sourceFile, version) {
|
|
13
|
+
const outDir = `./out/.tmp`;
|
|
14
|
+
let outFile;
|
|
15
|
+
|
|
16
|
+
if (!sourceFile.startsWith(outDir)) {
|
|
17
|
+
if (!fs.existsSync(outDir)) {
|
|
18
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
outFile = `${outDir}/${source.id}-${crypto.randomBytes(4).readUInt32LE(0)}.yaml`;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
outFile = sourceFile;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// bundle spec to a single file by default
|
|
28
|
+
if (source.bundle) {
|
|
29
|
+
bundleSpec(sourceFile, outFile, false);
|
|
30
|
+
console.log(`bundled specification`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// parse document
|
|
34
|
+
let document = await SwaggerParser.parse(source.bundle ? outFile : sourceFile);
|
|
35
|
+
|
|
36
|
+
// set version of target document
|
|
37
|
+
document.info.version = version;
|
|
38
|
+
|
|
39
|
+
// sort schemas alphabeticaly
|
|
40
|
+
if (source.sortSchemas && document.components && document.components.schemas) {
|
|
41
|
+
document.components.schemas = Object.keys(document.components.schemas)
|
|
42
|
+
.sort()
|
|
43
|
+
.reduce((obj, key) => { obj[key] = document.components.schemas[key]; return obj; }, {});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// apply custom decorators
|
|
47
|
+
const decorators = source.decorators || [];
|
|
48
|
+
decorators.forEach(d => {
|
|
49
|
+
const decoratorFile = `${d}.js`;
|
|
50
|
+
const decoratorPath = `${process.cwd()}/${decoratorFile}`;
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(decoratorPath)) {
|
|
53
|
+
console.error(`skipped decorator '${decoratorFile}' - file does not exist`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const decorator = require(decoratorPath);
|
|
58
|
+
|
|
59
|
+
if (!decorator.decorate) {
|
|
60
|
+
console.error(`skipped decorator '${decoratorFile}' - must export a 'decorate(document)' function`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
document = decorator.decorate(document);
|
|
65
|
+
|
|
66
|
+
console.log(`applied decorator '${decoratorFile}'`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// clean up unused things
|
|
70
|
+
if (source.cleanup) {
|
|
71
|
+
const removedPaths = [];
|
|
72
|
+
const removedTags = [];
|
|
73
|
+
const usedTags = [];
|
|
74
|
+
|
|
75
|
+
// remove empty paths & gather tags
|
|
76
|
+
const paths = document.paths || {};
|
|
77
|
+
for (const pathKey in paths) {
|
|
78
|
+
const path = document.paths[pathKey];
|
|
79
|
+
|
|
80
|
+
if (Object.keys(path).length === 0) {
|
|
81
|
+
removedPaths.push(pathKey);
|
|
82
|
+
delete paths[pathKey];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const methodKey in path) {
|
|
86
|
+
const maybeOperation = path[methodKey];
|
|
87
|
+
|
|
88
|
+
if (!maybeOperation.tags) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
maybeOperation.tags.forEach(tag => {
|
|
93
|
+
if (!usedTags.includes(tag)) {
|
|
94
|
+
usedTags.push(tag);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (removedPaths.length !== 0) {
|
|
101
|
+
console.log(`cleaned up unused paths:\n ${removedPaths.join('\n ')}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// remouve unused tags
|
|
105
|
+
const tags = [...document.tags];
|
|
106
|
+
tags.forEach(tag => {
|
|
107
|
+
if (!usedTags.includes(tag.name)) {
|
|
108
|
+
removedTags.push(tag.name);
|
|
109
|
+
document.tags.splice(document.tags.indexOf(tag), 1);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (removedTags.length !== 0) {
|
|
114
|
+
console.log(`cleaned up unused tags:\n ${removedTags.join('\n ')}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// write file
|
|
119
|
+
dump(document, outFile);
|
|
120
|
+
|
|
121
|
+
// re-bundle to remove unused components
|
|
122
|
+
if (source.cleanup && source.bundle) {
|
|
123
|
+
console.log(`re-bundling specification to remove unused components`);
|
|
124
|
+
let currentSize;
|
|
125
|
+
let bundledSize;
|
|
126
|
+
do {
|
|
127
|
+
currentSize = fs.statSync(outFile).size;
|
|
128
|
+
bundleSpec(outFile, outFile, true);
|
|
129
|
+
console.log('.');
|
|
130
|
+
bundledSize = fs.statSync(outFile).size;
|
|
131
|
+
}
|
|
132
|
+
while (bundledSize !== currentSize)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return outFile;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
exports.processSource = processSource;
|
package/config.schema.yml
CHANGED
|
@@ -37,6 +37,16 @@ sources:
|
|
|
37
37
|
pattern: '^[a-zA-Z0-9]+([-_][a-zA-Z0-9]+)*$' # only kebab-case or snake_case IDs
|
|
38
38
|
type:
|
|
39
39
|
type: string
|
|
40
|
+
bundle:
|
|
41
|
+
type: boolean
|
|
42
|
+
sortSchemas:
|
|
43
|
+
type: boolean
|
|
44
|
+
decorators:
|
|
45
|
+
type: array
|
|
46
|
+
items:
|
|
47
|
+
type: string
|
|
48
|
+
cleanup:
|
|
49
|
+
type: boolean
|
|
40
50
|
overrides:
|
|
41
51
|
$ref: '#/definitions/SourceOverrides'
|
|
42
52
|
required:
|
|
@@ -320,16 +330,6 @@ targets:
|
|
|
320
330
|
pattern: "^openapi$"
|
|
321
331
|
fileName:
|
|
322
332
|
type: string
|
|
323
|
-
bundle:
|
|
324
|
-
type: boolean
|
|
325
|
-
sortSchemas:
|
|
326
|
-
type: boolean
|
|
327
|
-
decorators:
|
|
328
|
-
type: array
|
|
329
|
-
items:
|
|
330
|
-
type: string
|
|
331
|
-
cleanup:
|
|
332
|
-
type: boolean
|
|
333
333
|
|
|
334
334
|
definitions:
|
|
335
335
|
# default
|
package/package.json
CHANGED