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 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.yaml> <...more.yaml>",
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.yaml> <...more.yaml>",
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.yaml>",
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.yaml>",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oas-toolkit",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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
+ });