Skip to content

Commit 944380d

Browse files
committed
fix: deep array merge
deep-extend does not support array merge. There was special code added to merge top-level arrays, but that was a shallow merge. Use deepmerge instead of deep-extend to merge arrays also. Default merge settings seem to work well - all tests pass. Add a test case based on swagger-api/swagger-ui#7618 Fixes: 2f5bb86 ("Fix and test for swagger-ui #3328: swagger-api/swagger-ui#3328. Added manual logic to merge arrays after calling deepExtend within `mergeDeep` function")
1 parent 5a7548e commit 944380d

File tree

3 files changed

+251
-39
lines changed

3 files changed

+251
-39
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112
"btoa": "^1.2.1",
113113
"cookie": "~0.4.1",
114114
"cross-fetch": "^3.1.4",
115-
"deep-extend": "~0.6.0",
115+
"deepmerge": "~4.2.2",
116116
"fast-json-patch": "^3.0.0-1",
117117
"form-data-encoder": "^1.4.3",
118118
"formdata-node": "^4.0.0",

src/specmap/lib/index.js

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as jsonPatch from 'fast-json-patch';
2-
import deepExtend from 'deep-extend';
3-
import cloneDeep from 'lodash/cloneDeep';
2+
import deepmerge from 'deepmerge'
43

54
export default {
65
add,
@@ -40,42 +39,8 @@ function applyPatch(obj, patch, opts) {
4039
jsonPatch.applyPatch(obj, [replace(patch.path, newValue)]);
4140
} else if (patch.op === 'mergeDeep') {
4241
const currentValue = getInByJsonPath(obj, patch.path);
43-
44-
// Iterate the properties of the patch
45-
// eslint-disable-next-line no-restricted-syntax, guard-for-in
46-
for (const prop in patch.value) {
47-
const propVal = patch.value[prop];
48-
const isArray = Array.isArray(propVal);
49-
if (isArray) {
50-
// deepExtend doesn't merge arrays, so we will do it manually
51-
const existing = currentValue[prop] || [];
52-
currentValue[prop] = existing.concat(propVal);
53-
} else if (isObject(propVal) && !isArray) {
54-
// If it's an object, iterate it's keys and merge
55-
// if there are conflicting keys, merge deep, otherwise shallow merge
56-
let currentObj = { ...currentValue[prop] };
57-
// eslint-disable-next-line no-restricted-syntax
58-
for (const key in propVal) {
59-
if (Object.prototype.hasOwnProperty.call(currentObj, key)) {
60-
// if there is a single conflicting key, just deepExtend the entire value
61-
// and break from the loop (since all future keys are also merged)
62-
// We do this because we can't deepExtend two primitives
63-
// (currentObj[key] & propVal[key] may be primitives).
64-
//
65-
// we also deeply assign here, since we aren't in control of
66-
// how deepExtend affects existing nested objects
67-
currentObj = deepExtend(cloneDeep(currentObj), propVal);
68-
break;
69-
} else {
70-
Object.assign(currentObj, { [key]: propVal[key] });
71-
}
72-
}
73-
currentValue[prop] = currentObj;
74-
} else {
75-
// It's a primitive, just replace existing
76-
currentValue[prop] = propVal;
77-
}
78-
}
42+
const newValue = deepmerge(currentValue, patch.value);
43+
obj = jsonPatch.applyPatch(obj, [replace(patch.path, newValue)]).newDocument;
7944
} else if (patch.op === 'add' && patch.path === '' && isObject(patch.value)) {
8045
// { op: 'add', path: '', value: { a: 1, b: 2 }}
8146
// has no effect: json patch refuses to do anything.

test/bugs/ui-7618.js

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
// https://github.com/swagger-api/swagger-ui/issues/7618
2+
3+
import resolveSubtree from '../../src/subtree-resolver';
4+
5+
const spec = {
6+
"openapi": "3.0.3",
7+
"info": {
8+
"version": "1.0.0",
9+
"title": "test",
10+
"description": "test",
11+
"contact": {
12+
"name": "test",
13+
"email": "[email protected]"
14+
}
15+
},
16+
"servers": [
17+
{
18+
"url": "https://localhost/api"
19+
}
20+
],
21+
"tags": [
22+
{
23+
"name": "Test",
24+
"description": "Test"
25+
}
26+
],
27+
"paths": {
28+
"/one": {
29+
"get": {
30+
"description": "test",
31+
"operationId": "one",
32+
"tags": [
33+
"Test"
34+
],
35+
"parameters": [
36+
{
37+
"name": "test",
38+
"in": "header",
39+
"required": true,
40+
"schema": {
41+
"type": "string"
42+
}
43+
}
44+
],
45+
"responses": {
46+
"default": {
47+
"description": "Response",
48+
"content": {
49+
"text/plain": {
50+
"schema": {
51+
"type": "integer",
52+
"enum": [
53+
401,
54+
404,
55+
500
56+
]
57+
}
58+
}
59+
}
60+
}
61+
}
62+
}
63+
}
64+
},
65+
"components": {
66+
"schemas": {
67+
"parent": {
68+
"type": "object",
69+
"required": [
70+
"required1"
71+
],
72+
"properties": {
73+
"required1": {
74+
"type": "string"
75+
},
76+
"nested1": {
77+
"type": "object",
78+
"required": [
79+
"nestedrequired1"
80+
],
81+
"properties": {
82+
"nestedrequired1": {
83+
"type": "string"
84+
}
85+
}
86+
}
87+
}
88+
},
89+
"child": {
90+
"allOf": [
91+
{
92+
"$ref": "#/components/schemas/parent"
93+
},
94+
{
95+
"type": "object",
96+
"required": [
97+
"required2"
98+
],
99+
"properties": {
100+
"required2": {
101+
"type": "string"
102+
},
103+
"nested1": {
104+
"type": "object",
105+
"required": [
106+
"nestedrequired2"
107+
],
108+
"properties": {
109+
"nestedrequired2": {
110+
"type": "string"
111+
}
112+
}
113+
}
114+
}
115+
}
116+
]
117+
}
118+
}
119+
}
120+
};
121+
122+
test('should resolve test case from UI-7618 correctly', async () => {
123+
const res = await resolveSubtree(spec, []);
124+
125+
expect(res).toEqual({
126+
"spec": {
127+
"openapi": "3.0.3",
128+
"info": {
129+
"version": "1.0.0",
130+
"title": "test",
131+
"description": "test",
132+
"contact": {
133+
"name": "test",
134+
"email": "[email protected]"
135+
}
136+
},
137+
"servers": [
138+
{
139+
"url": "https://localhost/api"
140+
}
141+
],
142+
"tags": [
143+
{
144+
"name": "Test",
145+
"description": "Test"
146+
}
147+
],
148+
"paths": {
149+
"/one": {
150+
"get": {
151+
"description": "test",
152+
"operationId": "one",
153+
"tags": [
154+
"Test"
155+
],
156+
"parameters": [
157+
{
158+
"name": "test",
159+
"in": "header",
160+
"required": true,
161+
"schema": {
162+
"type": "string"
163+
}
164+
}
165+
],
166+
"responses": {
167+
"default": {
168+
"description": "Response",
169+
"content": {
170+
"text/plain": {
171+
"schema": {
172+
"type": "integer",
173+
"enum": [
174+
401,
175+
404,
176+
500
177+
]
178+
}
179+
}
180+
}
181+
}
182+
},
183+
"__originalOperationId": "one"
184+
}
185+
}
186+
},
187+
"components": {
188+
"schemas": {
189+
"parent": {
190+
"type": "object",
191+
"required": [
192+
"required1"
193+
],
194+
"properties": {
195+
"required1": {
196+
"type": "string"
197+
},
198+
"nested1": {
199+
"type": "object",
200+
"required": [
201+
"nestedrequired1"
202+
],
203+
"properties": {
204+
"nestedrequired1": {
205+
"type": "string"
206+
}
207+
}
208+
}
209+
}
210+
},
211+
"child": {
212+
"type": "object",
213+
"required": [
214+
"required1",
215+
"required2"
216+
],
217+
"properties": {
218+
"required1": {
219+
"type": "string"
220+
},
221+
"nested1": {
222+
"type": "object",
223+
"required": [
224+
"nestedrequired1",
225+
"nestedrequired2"
226+
],
227+
"properties": {
228+
"nestedrequired1": {
229+
"type": "string"
230+
},
231+
"nestedrequired2": {
232+
"type": "string"
233+
}
234+
}
235+
},
236+
"required2": {
237+
"type": "string"
238+
}
239+
}
240+
}
241+
}
242+
},
243+
"$$normalized": true
244+
},
245+
"errors": []
246+
});
247+
});

0 commit comments

Comments
 (0)