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.
- package/cli/commands/merge.js +1 -1
- package/merger.js +106 -5
- package/merger.test.js +132 -14
- package/package.json +3 -1
package/cli/commands/merge.js
CHANGED
|
@@ -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
|
|
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(
|
|
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.
|
|
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
|
}
|