oas-toolkit 0.1.0 → 0.3.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.
@@ -15,7 +15,7 @@ module.exports = function ({ argv }) {
15
15
  oas.push(yaml.load(fs.readFileSync(f)));
16
16
  }
17
17
 
18
- const combined = merger.apply(null, oas);
18
+ const combined = merger(oas, argv);
19
19
  console.log(yaml.dump(combined));
20
20
  } catch (e) {
21
21
  console.error(`ERROR: ${e.message}`);
package/merger.js CHANGED
@@ -1,8 +1,12 @@
1
1
  const mergician = require("mergician");
2
+ const isEqual = require("lodash.isequal");
3
+ const uniqWith = require("lodash.uniqwith");
2
4
 
3
- function merge(...objects) {
4
- ensureNoComponentColissions(objects);
5
- ensureNoPathColissions(objects);
5
+ function merge(objects, options) {
6
+ ensureNoComponentColissions(objects, options);
7
+ ensureNoPathColissions(objects, options);
8
+ ensureNoTagColissions(objects, options);
9
+ ensureNoSecurityColissions(objects, options);
6
10
 
7
11
  // Do the merge
8
12
  let combinedSpec = {};
@@ -21,6 +25,14 @@ function merge(...objects) {
21
25
  combinedSpec = mergeSection(combinedSpec, appendMerge, objects, section);
22
26
  }
23
27
 
28
+ // Values that should be unique
29
+ const uniqueSections = ["security", "tags"];
30
+ for (let section of uniqueSections) {
31
+ if (combinedSpec[section]) {
32
+ combinedSpec[section] = uniqWith(combinedSpec[section], isEqual);
33
+ }
34
+ }
35
+
24
36
  return combinedSpec;
25
37
  }
26
38
 
@@ -43,7 +55,8 @@ function mergeSection(spec, merger, objects, section) {
43
55
  );
44
56
  }
45
57
 
46
- function ensureNoComponentColissions(objects) {
58
+ function ensureNoComponentColissions(objects, options) {
59
+ options = options || {};
47
60
  const componentList = {};
48
61
  // Fetch the first two levels of components
49
62
  for (const object of objects) {
@@ -59,8 +72,20 @@ function ensureNoComponentColissions(objects) {
59
72
  }
60
73
 
61
74
  for (let component in componentList) {
75
+ if (options.ignorePrefix) {
76
+ if (typeof options.ignorePrefix == "string") {
77
+ options.ignorePrefix = [options.ignorePrefix];
78
+ }
79
+ for (let prefix of options.ignorePrefix) {
80
+ if (component.startsWith(prefix)) {
81
+ delete componentList[component];
82
+ }
83
+ }
84
+ }
85
+
86
+ // Check if there are > 2
62
87
  const value = componentList[component];
63
- if (value.length > 1) {
88
+ if (value && value.length > 1) {
64
89
  throw new Error(
65
90
  `Duplicate component detected: ${component} (${value.join(", ")})`
66
91
  );
@@ -93,7 +118,83 @@ function ensureNoPathColissions(objects) {
93
118
  }
94
119
  }
95
120
 
121
+ function ensureListUniqueness(list, key, objects) {
122
+ let all = [];
123
+ for (let object of objects) {
124
+ all = all.concat(object[list] || []);
125
+ }
126
+
127
+ for (let c of all) {
128
+ const d = all.filter((t) => {
129
+ return t[key] == c[key] && !isEqual(c, t);
130
+ });
131
+
132
+ if (d.length > 0) {
133
+ // Which files does this exist in?
134
+ const sources = [];
135
+ for (let object of objects) {
136
+ if (!object[list]) {
137
+ continue;
138
+ }
139
+
140
+ const match = object[list].filter((t) => t[key] == c[key]);
141
+ if (match.length) {
142
+ sources.push(object.info.title);
143
+ }
144
+ }
145
+
146
+ throw new Error(
147
+ `Conflicting ${list} detected: ${c[key]} (${sources.join(", ")})`
148
+ );
149
+ }
150
+ }
151
+ }
152
+
153
+ function ensureNoTagColissions(objects) {
154
+ ensureListUniqueness("tags", "name", objects);
155
+ }
156
+
157
+ function ensureNoSecurityColissions(objects) {
158
+ let all = [];
159
+ for (let object of objects) {
160
+ all = all.concat(object.security || []);
161
+ }
162
+
163
+ all = all.map((s) => Object.entries(s)[0]);
164
+
165
+ for (let c of all) {
166
+ const d = all.filter((t) => {
167
+ return t[0] == c[0] && !isEqual(c, t);
168
+ });
169
+
170
+ if (d.length > 0) {
171
+ // Which files does this exist in?
172
+ const sources = [];
173
+ for (let object of objects) {
174
+ if (!object.security) {
175
+ continue;
176
+ }
177
+
178
+ const match = object.security.filter((t) => {
179
+ const k = Object.keys(t)[0];
180
+ return k == c[0];
181
+ });
182
+
183
+ if (match.length) {
184
+ sources.push(object.info.title);
185
+ }
186
+ }
187
+
188
+ throw new Error(
189
+ `Conflicting security detected: ${c[0]} (${sources.join(", ")})`
190
+ );
191
+ }
192
+ }
193
+ }
194
+
96
195
  module.exports = Object.assign(merge, {
97
196
  ensureNoComponentColissions,
98
197
  ensureNoPathColissions,
198
+ ensureNoTagColissions,
199
+ ensureNoSecurityColissions,
99
200
  });
package/merger.test.js CHANGED
@@ -1,12 +1,93 @@
1
1
  const {
2
2
  ensureNoComponentColissions,
3
3
  ensureNoPathColissions,
4
+ ensureNoTagColissions,
5
+ ensureNoSecurityColissions,
4
6
  } = require("./merger");
5
7
  const merger = require("./merger");
6
8
 
7
9
  const FooSchema = { type: "string" };
8
10
  const BarSchema = { type: "boolean" };
9
11
 
12
+ describe("#ensureNoTagColissions", () => {
13
+ it("does not throw when there are no colissions", () => {
14
+ expect(
15
+ ensureNoTagColissions([
16
+ { info: { title: "One" }, tags: [{ name: "Demo" }] },
17
+ { info: { title: "Two" }, tags: [{ name: "Demo" }] },
18
+ ])
19
+ ).toBe(undefined);
20
+ });
21
+
22
+ it("throws when there is a field in one tag but not in another", () => {
23
+ expect(() => {
24
+ ensureNoTagColissions([
25
+ { info: { title: "One" }, tags: [{ name: "Demo" }] },
26
+ {
27
+ info: { title: "Two" },
28
+ tags: [{ name: "Demo", description: "FOO" }],
29
+ },
30
+ ]);
31
+ }).toThrow(new Error("Conflicting tags detected: Demo (One, Two)"));
32
+ });
33
+
34
+ it("throws when there is a difference in deeply nested fields", () => {
35
+ expect(() => {
36
+ ensureNoTagColissions([
37
+ {
38
+ info: { title: "One" },
39
+ tags: [
40
+ {
41
+ name: "Demo",
42
+ externalDocs: {
43
+ description: "Hello",
44
+ url: "https://example.com",
45
+ },
46
+ },
47
+ ],
48
+ },
49
+ {
50
+ info: { title: "Two" },
51
+ tags: [
52
+ {
53
+ name: "Demo",
54
+ externalDocs: {
55
+ description: "Hello",
56
+ url: "https://another.example.com",
57
+ },
58
+ },
59
+ ],
60
+ },
61
+ ]);
62
+ }).toThrow(new Error("Conflicting tags detected: Demo (One, Two)"));
63
+ });
64
+ });
65
+
66
+ describe("#ensureNoSecurityColissions", () => {
67
+ it("does not throw when there are no colissions", () => {
68
+ expect(
69
+ ensureNoSecurityColissions([
70
+ { info: { title: "One" }, security: [{ appKey: [] }] },
71
+ { info: { title: "Two" }, security: [{ appKey: [] }] },
72
+ ])
73
+ ).toBe(undefined);
74
+ });
75
+
76
+ it("throws when there is a field in one security but not in another", () => {
77
+ expect(() => {
78
+ ensureNoSecurityColissions([
79
+ { info: { title: "One" }, security: [{ petstore_auth: [] }] },
80
+ {
81
+ info: { title: "Two" },
82
+ security: [{ petstore_auth: ["pets:write"] }],
83
+ },
84
+ ]);
85
+ }).toThrow(
86
+ new Error("Conflicting security detected: petstore_auth (One, Two)")
87
+ );
88
+ });
89
+ });
90
+
10
91
  describe("#ensureNoComponentColissions", () => {
11
92
  it("does not throw when schemas have different prefixes", () => {
12
93
  expect(
@@ -29,6 +110,18 @@ describe("#ensureNoComponentColissions", () => {
29
110
  ).toBe(undefined);
30
111
  });
31
112
 
113
+ it("does not throw when a conflicting schema is in the ignore list", () => {
114
+ expect(
115
+ ensureNoComponentColissions(
116
+ [
117
+ { info: { title: "One" }, components: { schemas: { FooSchema } } },
118
+ { info: { title: "Two" }, components: { schemas: { FooSchema } } },
119
+ ],
120
+ { ignorePrefix: ["components.schemas"] }
121
+ )
122
+ ).toBe(undefined);
123
+ });
124
+
32
125
  it("throws when two components have the same name", () => {
33
126
  expect(() => {
34
127
  ensureNoComponentColissions([
@@ -114,20 +207,20 @@ describe("path collisions", () => {
114
207
 
115
208
  describe("uses the last provided value for:", () => {
116
209
  it("openapi", () => {
117
- expect(merger({ openapi: "3.0.3" }, { openapi: "3.1.0" })).toEqual({
210
+ expect(merger([{ openapi: "3.0.3" }, { openapi: "3.1.0" }])).toEqual({
118
211
  openapi: "3.1.0",
119
212
  });
120
213
  });
121
214
 
122
215
  it("info", () => {
123
216
  expect(
124
- merger({ info: { title: "OAS One" } }, { info: { title: "OAS Two" } })
217
+ merger([{ info: { title: "OAS One" } }, { info: { title: "OAS Two" } }])
125
218
  ).toEqual({ info: { title: "OAS Two" } });
126
219
  });
127
220
 
128
221
  it("servers", () => {
129
222
  expect(
130
- merger(
223
+ merger([
131
224
  {
132
225
  servers: [
133
226
  { url: "https://example.com", description: "My API Description" },
@@ -140,8 +233,8 @@ describe("uses the last provided value for:", () => {
140
233
  description: "Overwritten value",
141
234
  },
142
235
  ],
143
- }
144
- )
236
+ },
237
+ ])
145
238
  ).toEqual({
146
239
  servers: [
147
240
  { url: "https://api.example.com", description: "Overwritten value" },
@@ -153,10 +246,10 @@ describe("uses the last provided value for:", () => {
153
246
  describe("concatenates values for:", () => {
154
247
  it("tags", () => {
155
248
  expect(
156
- merger(
249
+ merger([
157
250
  { tags: [{ name: "One", description: "Description one" }] },
158
- { tags: [{ name: "Two", description: "Description two" }] }
159
- )
251
+ { tags: [{ name: "Two", description: "Description two" }] },
252
+ ])
160
253
  ).toEqual({
161
254
  tags: [
162
255
  { name: "One", description: "Description one" },
@@ -167,7 +260,7 @@ describe("concatenates values for:", () => {
167
260
 
168
261
  it("paths", () => {
169
262
  expect(
170
- merger(
263
+ merger([
171
264
  {
172
265
  info: { title: "One" },
173
266
  paths: { "/users": { get: { operationId: "list-users" } } },
@@ -175,8 +268,8 @@ describe("concatenates values for:", () => {
175
268
  {
176
269
  info: { title: "Two" },
177
270
  paths: { "/users": { post: { operationId: "create-user" } } },
178
- }
179
- )
271
+ },
272
+ ])
180
273
  ).toMatchObject({
181
274
  paths: {
182
275
  "/users": {
@@ -189,7 +282,7 @@ describe("concatenates values for:", () => {
189
282
 
190
283
  it("security", () => {
191
284
  expect(
192
- merger(
285
+ merger([
193
286
  { security: [{ basicAuth: { type: "http", scheme: "basic" } }] },
194
287
  {
195
288
  security: [
@@ -201,8 +294,8 @@ describe("concatenates values for:", () => {
201
294
  },
202
295
  },
203
296
  ],
204
- }
205
- )
297
+ },
298
+ ])
206
299
  ).toEqual({
207
300
  security: [
208
301
  { basicAuth: { type: "http", scheme: "basic" } },
@@ -217,3 +310,28 @@ describe("concatenates values for:", () => {
217
310
  });
218
311
  });
219
312
  });
313
+
314
+ describe("returns unique items for:", () => {
315
+ it("tags", () => {
316
+ expect(
317
+ merger([
318
+ { tags: [{ name: "One", description: "Description one" }] },
319
+ { tags: [{ name: "Two", description: "Description two" }] },
320
+ { tags: [{ name: "One", description: "Description one" }] },
321
+ ])
322
+ ).toEqual({
323
+ tags: [
324
+ { name: "One", description: "Description one" },
325
+ { name: "Two", description: "Description two" },
326
+ ],
327
+ });
328
+ });
329
+
330
+ it("security", () => {
331
+ expect(
332
+ merger([{ security: [{ appKey: [] }] }, { security: [{ appKey: [] }] }])
333
+ ).toEqual({
334
+ security: [{ appKey: [] }],
335
+ });
336
+ });
337
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oas-toolkit",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -20,6 +20,8 @@
20
20
  "debug": "^4.3.4",
21
21
  "js-yaml": "^4.1.0",
22
22
  "jsonpath-plus": "^7.2.0",
23
+ "lodash.isequal": "^4.5.0",
24
+ "lodash.uniqwith": "^4.5.0",
23
25
  "mergician": "^1.1.0",
24
26
  "yargs": "^17.7.1"
25
27
  }