oas-toolkit 0.8.0 → 0.10.0
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 +18 -0
- package/canonical-server.js +47 -0
- package/canonical-server.test.js +77 -0
- package/cli/bin.js +35 -4
- package/cli/commands/canonical-server.js +14 -0
- package/cli/commands/remove-with-annotation.js +22 -0
- package/package.json +1 -1
- package/remove-with-annotation.js +43 -0
- package/remove-with-annotation.test.js +118 -0
package/README.md
CHANGED
|
@@ -34,3 +34,21 @@ Merge the following lists/objects recursively. If you encounter a list, concaten
|
|
|
34
34
|
- tags
|
|
35
35
|
- components
|
|
36
36
|
- paths
|
|
37
|
+
|
|
38
|
+
## Remove by Annotation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
oas-toolkit remove-with-annotation --annotation x-visibility.internal=true /tmp/openapi.yaml
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Remove paths or operations from an OpenAPI spec based on an annotation. The example provided will remove any paths or operations with the following annotation:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"x-visibility": {
|
|
49
|
+
"internal": true
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
You can pass `--remove-unused` to run `remove-unused-components` and `remove-tags` after running this command.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const isEqual = require("lodash.isequal");
|
|
2
|
+
const uniqWith = require("lodash.uniqwith");
|
|
3
|
+
const url = require("url");
|
|
4
|
+
function run(oas) {
|
|
5
|
+
oas = JSON.parse(JSON.stringify(oas)); // Prevent modification of original object
|
|
6
|
+
|
|
7
|
+
// Extract the base path from servers
|
|
8
|
+
const basePaths = uniqWith(
|
|
9
|
+
oas.servers.map((server) => {
|
|
10
|
+
const path = url.parse(server.url).pathname;
|
|
11
|
+
if (path.slice(-1) === "/") {
|
|
12
|
+
return path.slice(0, -1);
|
|
13
|
+
}
|
|
14
|
+
return path;
|
|
15
|
+
}),
|
|
16
|
+
isEqual
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (basePaths.length > 1) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Base paths are different in the servers block. Found: ${basePaths.join(
|
|
22
|
+
", "
|
|
23
|
+
)}`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (let path in oas.paths) {
|
|
28
|
+
let newPath = basePaths[0] + path;
|
|
29
|
+
oas.paths[newPath] = oas.paths[path];
|
|
30
|
+
delete oas.paths[path];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Remove paths from servers
|
|
34
|
+
oas.servers = oas.servers.map((server) => {
|
|
35
|
+
const u = url.parse(server.url);
|
|
36
|
+
return {
|
|
37
|
+
...server,
|
|
38
|
+
url: `${u.protocol}//${u.hostname}/`,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return oas;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
run,
|
|
47
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const c = require("./canonical-server");
|
|
2
|
+
|
|
3
|
+
const oas = {
|
|
4
|
+
servers: [
|
|
5
|
+
{
|
|
6
|
+
url: "https://api.example.com/v1",
|
|
7
|
+
},
|
|
8
|
+
],
|
|
9
|
+
paths: {
|
|
10
|
+
"/foo/hello": {},
|
|
11
|
+
"/foo/world": {},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function getOas(urls) {
|
|
16
|
+
return {
|
|
17
|
+
servers: urls.map((url) => {
|
|
18
|
+
return {
|
|
19
|
+
url,
|
|
20
|
+
};
|
|
21
|
+
}),
|
|
22
|
+
paths: oas.paths,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("#run", () => {
|
|
27
|
+
it("prefixes all routes", () => {
|
|
28
|
+
const o = getOas(["https://api.example.com/v1"]);
|
|
29
|
+
expect(c.run(o).paths).toEqual({
|
|
30
|
+
"/v1/foo/hello": {},
|
|
31
|
+
"/v1/foo/world": {},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("handles multiple servers", () => {
|
|
36
|
+
const o = getOas([
|
|
37
|
+
"https://api.example.com/v1",
|
|
38
|
+
"https://stagingapi.example.com/v1",
|
|
39
|
+
]);
|
|
40
|
+
expect(c.run(o).paths).toEqual({
|
|
41
|
+
"/v1/foo/hello": {},
|
|
42
|
+
"/v1/foo/world": {},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("handles trailing slashes", () => {
|
|
47
|
+
const o = getOas([
|
|
48
|
+
"https://api.example.com/v1/",
|
|
49
|
+
"https://stagingapi.example.com/v1/",
|
|
50
|
+
]);
|
|
51
|
+
expect(c.run(o).paths).toEqual({
|
|
52
|
+
"/v1/foo/hello": {},
|
|
53
|
+
"/v1/foo/world": {},
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("throws if the paths are different in a single OAS", () => {
|
|
58
|
+
const o = getOas([
|
|
59
|
+
"https://api.example.com/v1",
|
|
60
|
+
"https://api.example.com/v2",
|
|
61
|
+
]);
|
|
62
|
+
expect(() => c.run(o).paths).toThrow(
|
|
63
|
+
"Base paths are different in the servers block. Found: /v1, /v2"
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("rewrites the servers", () => {
|
|
68
|
+
const o = getOas([
|
|
69
|
+
"https://api.example.com/v1",
|
|
70
|
+
"https://stagingapi.example.com/v1",
|
|
71
|
+
]);
|
|
72
|
+
expect(c.run(o).servers).toEqual([
|
|
73
|
+
{ url: "https://api.example.com/" },
|
|
74
|
+
{ url: "https://stagingapi.example.com/" },
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
});
|
package/cli/bin.js
CHANGED
|
@@ -5,25 +5,44 @@ const { hideBin } = require("yargs/helpers");
|
|
|
5
5
|
|
|
6
6
|
yargs(hideBin(process.argv))
|
|
7
7
|
.command(
|
|
8
|
-
"merge <openapi
|
|
8
|
+
"merge <openapi> <...more.yaml>",
|
|
9
9
|
"merge the provided OpenAPI files",
|
|
10
10
|
require("./commands/merge")
|
|
11
11
|
)
|
|
12
12
|
.command(
|
|
13
|
-
"check-conflicts <openapi
|
|
13
|
+
"check-conflicts <openapi> <...more.yaml>",
|
|
14
14
|
"check for conflicting components, paths, tags, and security schemes",
|
|
15
15
|
require("./commands/check-conflicts")
|
|
16
16
|
)
|
|
17
17
|
.command(
|
|
18
|
-
"remove-unused-components <openapi
|
|
18
|
+
"remove-unused-components <openapi>",
|
|
19
19
|
"remove unused components from the provided OpenAPI file",
|
|
20
20
|
require("./commands/remove-unused-components")
|
|
21
21
|
)
|
|
22
22
|
.command(
|
|
23
|
-
"remove-unused-tags <openapi
|
|
23
|
+
"remove-unused-tags <openapi>",
|
|
24
24
|
"remove unused tags from the provided OpenAPI file",
|
|
25
25
|
require("./commands/remove-unused-tags")
|
|
26
26
|
)
|
|
27
|
+
.command(
|
|
28
|
+
"remove-with-annotation <openapi>",
|
|
29
|
+
"remove all paths/select paths/operations with a specific annotation from the provided OpenAPI file",
|
|
30
|
+
(yargs) => {
|
|
31
|
+
yargs.option("annotation", {
|
|
32
|
+
demandOption: true,
|
|
33
|
+
});
|
|
34
|
+
yargs.option("remove-unused", {
|
|
35
|
+
demandOption: false,
|
|
36
|
+
type: "boolean",
|
|
37
|
+
});
|
|
38
|
+
yargs.positional("openapi", {
|
|
39
|
+
require: true,
|
|
40
|
+
describe: "the OpenAPI file to rewrite",
|
|
41
|
+
type: "string",
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
require("./commands/remove-with-annotation")
|
|
45
|
+
)
|
|
27
46
|
.command(
|
|
28
47
|
"rewrite-path <openapi>",
|
|
29
48
|
"rewrite paths in the provided OpenAPI file",
|
|
@@ -42,4 +61,16 @@ yargs(hideBin(process.argv))
|
|
|
42
61
|
},
|
|
43
62
|
require("./commands/rewrite-path")
|
|
44
63
|
)
|
|
64
|
+
.command(
|
|
65
|
+
"canonical-server <openapi>",
|
|
66
|
+
"move the path from the servers block in to /paths",
|
|
67
|
+
(yargs) => {
|
|
68
|
+
yargs.positional("openapi", {
|
|
69
|
+
require: true,
|
|
70
|
+
describe: "the OpenAPI file to rewrite",
|
|
71
|
+
type: "string",
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
require("./commands/canonical-server")
|
|
75
|
+
)
|
|
45
76
|
.parse();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const yaml = require("js-yaml");
|
|
3
|
+
|
|
4
|
+
module.exports = async function (argv) {
|
|
5
|
+
try {
|
|
6
|
+
const p = require("../../canonical-server");
|
|
7
|
+
let oas = yaml.load(fs.readFileSync(argv.openapi));
|
|
8
|
+
oas = p.run(oas);
|
|
9
|
+
console.log(yaml.dump(oas));
|
|
10
|
+
} catch (e) {
|
|
11
|
+
console.error(`ERROR: ${e.message}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const yaml = require("js-yaml");
|
|
3
|
+
const { removeUnusedComponents } = require("../../components");
|
|
4
|
+
const { removeUnusedTags } = require("../../tags");
|
|
5
|
+
|
|
6
|
+
module.exports = async function (argv) {
|
|
7
|
+
try {
|
|
8
|
+
const p = require("../../remove-with-annotation");
|
|
9
|
+
let oas = yaml.load(fs.readFileSync(argv.openapi));
|
|
10
|
+
oas = p.remove(oas, argv.annotation);
|
|
11
|
+
|
|
12
|
+
if (argv["remove-unused"]) {
|
|
13
|
+
oas = removeUnusedComponents(oas);
|
|
14
|
+
oas = removeUnusedTags(oas);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log(yaml.dump(oas));
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.error(`ERROR: ${e.message}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const get = require("lodash.get");
|
|
2
|
+
function remove(oas, annotation) {
|
|
3
|
+
oas = JSON.parse(JSON.stringify(oas)); // Prevent modification of original object
|
|
4
|
+
let [key, value] = annotation.split("=");
|
|
5
|
+
|
|
6
|
+
// Coerce booleans in value
|
|
7
|
+
if (value === "true") {
|
|
8
|
+
value = true;
|
|
9
|
+
}
|
|
10
|
+
if (value === "false") {
|
|
11
|
+
value = false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Remove at a global level
|
|
15
|
+
if (get(oas, key) === value) {
|
|
16
|
+
delete oas.paths;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Remove at an operation level
|
|
20
|
+
for (let operation in oas.paths) {
|
|
21
|
+
if (get(oas.paths[operation], key) === value) {
|
|
22
|
+
delete oas.paths[operation];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Remove at a path level
|
|
26
|
+
for (let path in oas.paths[operation]) {
|
|
27
|
+
if (get(oas.paths[operation][path], key) === value) {
|
|
28
|
+
delete oas.paths[operation][path];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If the operation is now empty, remove it
|
|
32
|
+
if (Object.keys(oas.paths[operation]).length === 0) {
|
|
33
|
+
delete oas.paths[operation];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return oas;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
remove,
|
|
43
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const p = require("./remove-with-annotation");
|
|
2
|
+
|
|
3
|
+
const hello = {
|
|
4
|
+
post: {
|
|
5
|
+
operationId: "create-hello",
|
|
6
|
+
requestBody: {
|
|
7
|
+
$ref: "#/components/requestBodies/CreateHello",
|
|
8
|
+
},
|
|
9
|
+
responses: {},
|
|
10
|
+
},
|
|
11
|
+
get: {
|
|
12
|
+
operationId: "get-hello",
|
|
13
|
+
responses: {},
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const world = {
|
|
18
|
+
get: {
|
|
19
|
+
operationId: "create-world",
|
|
20
|
+
requestBody: {
|
|
21
|
+
$ref: "#/components/requestBodies/CreateWorld",
|
|
22
|
+
},
|
|
23
|
+
responses: {},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const oas = {
|
|
28
|
+
paths: {
|
|
29
|
+
"/v1/foo/hello": hello,
|
|
30
|
+
"/v1/foo/world": world,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("#remove-with-annotation", () => {
|
|
35
|
+
describe("global", () => {
|
|
36
|
+
it("removes a scalar global item", () => {
|
|
37
|
+
const testOas = {
|
|
38
|
+
"x-visibility": "internal",
|
|
39
|
+
...oas,
|
|
40
|
+
};
|
|
41
|
+
expect(p.remove(testOas, "x-visibility=internal")).toEqual({
|
|
42
|
+
"x-visibility": "internal",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("removes a boolean global item", () => {
|
|
47
|
+
const testOas = {
|
|
48
|
+
"x-hidden": true,
|
|
49
|
+
...oas,
|
|
50
|
+
};
|
|
51
|
+
expect(p.remove(testOas, "x-hidden=true")).toEqual({
|
|
52
|
+
"x-hidden": true,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("removes a nested global item", () => {
|
|
57
|
+
const testOas = {
|
|
58
|
+
"x-visibility": {
|
|
59
|
+
publish: "INTERNAL",
|
|
60
|
+
},
|
|
61
|
+
...oas,
|
|
62
|
+
};
|
|
63
|
+
expect(p.remove(testOas, "x-visibility.publish=INTERNAL")).toEqual({
|
|
64
|
+
"x-visibility": {
|
|
65
|
+
publish: "INTERNAL",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("operation", () => {
|
|
72
|
+
it("removes a nested operation item", () => {
|
|
73
|
+
const testOas = {
|
|
74
|
+
paths: {
|
|
75
|
+
"/v1/foo/hello": {
|
|
76
|
+
"x-visibility": {
|
|
77
|
+
publish: "INTERNAL",
|
|
78
|
+
},
|
|
79
|
+
...hello,
|
|
80
|
+
},
|
|
81
|
+
"/v1/foo/world": world,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
expect(p.remove(testOas, "x-visibility.publish=INTERNAL")).toEqual({
|
|
85
|
+
paths: {
|
|
86
|
+
"/v1/foo/world": world,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("path", () => {
|
|
93
|
+
it("removes a nested path item", () => {
|
|
94
|
+
const testOas = {
|
|
95
|
+
paths: {
|
|
96
|
+
"/v1/foo/hello": {
|
|
97
|
+
get: {
|
|
98
|
+
"x-visibility": {
|
|
99
|
+
publish: "INTERNAL",
|
|
100
|
+
},
|
|
101
|
+
...hello.get,
|
|
102
|
+
},
|
|
103
|
+
post: hello.post,
|
|
104
|
+
},
|
|
105
|
+
"/v1/foo/world": world,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
expect(p.remove(testOas, "x-visibility.publish=INTERNAL")).toEqual({
|
|
109
|
+
paths: {
|
|
110
|
+
"/v1/foo/hello": {
|
|
111
|
+
post: hello.post,
|
|
112
|
+
},
|
|
113
|
+
"/v1/foo/world": world,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|