Skip to content

Commit 6caddd7

Browse files
authored
fix(resolver): avoid duplicating enum values when nested in allOf (#3939)
1 parent 5ee8ec1 commit 6caddd7

File tree

6 files changed

+144
-13
lines changed

6 files changed

+144
-13
lines changed

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,11 @@
7474
"dependencies": {
7575
"@babel/runtime-corejs3": "^7.22.15",
7676
"@scarf/scarf": "=1.4.0",
77-
"@swagger-api/apidom-core": ">=1.0.0-beta.39 <1.0.0-rc.0",
78-
"@swagger-api/apidom-error": ">=1.0.0-beta.39 <1.0.0-rc.0",
79-
"@swagger-api/apidom-json-pointer": ">=1.0.0-beta.39 <1.0.0-rc.0",
80-
"@swagger-api/apidom-ns-openapi-3-1": ">=1.0.0-beta.39 <1.0.0-rc.0",
81-
"@swagger-api/apidom-reference": ">=1.0.0-beta.39 <1.0.0-rc.0",
77+
"@swagger-api/apidom-core": ">=1.0.0-beta.40 <1.0.0-rc.0",
78+
"@swagger-api/apidom-error": ">=1.0.0-beta.40 <1.0.0-rc.0",
79+
"@swagger-api/apidom-json-pointer": ">=1.0.0-beta.40 <1.0.0-rc.0",
80+
"@swagger-api/apidom-ns-openapi-3-1": ">=1.0.0-beta.40 <1.0.0-rc.0",
81+
"@swagger-api/apidom-reference": ">=1.0.0-beta.40 <1.0.0-rc.0",
8282
"@swaggerexpert/cookie": "^2.0.2",
8383
"deepmerge": "~4.3.0",
8484
"fast-json-patch": "^3.0.0-1",

src/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/visitors/all-of.js

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { isArrayElement, deepmerge } from '@swagger-api/apidom-core';
1+
import { uniqWith } from 'ramda';
2+
import {
3+
isArrayElement,
4+
isObjectElement,
5+
deepmerge,
6+
toValue,
7+
cloneShallow,
8+
includesClasses,
9+
} from '@swagger-api/apidom-core';
210
import { isSchemaElement } from '@swagger-api/apidom-ns-openapi-3-1';
311

412
import toPath from '../utils/to-path.js';
@@ -37,7 +45,39 @@ class AllOfVisitor {
3745
while (schemaElement.hasKey('allOf')) {
3846
const { allOf } = schemaElement;
3947
schemaElement.remove('allOf');
40-
const allOfMerged = deepmerge.all([...allOf.content, schemaElement]);
48+
const allOfMerged = deepmerge.all([...allOf.content, schemaElement], {
49+
customMerge: (keyElement) => {
50+
if (toValue(keyElement) === 'enum') {
51+
return (targetElement, sourceElement) => {
52+
if (
53+
includesClasses(['json-schema-enum'], targetElement) &&
54+
includesClasses(['json-schema-enum'], sourceElement)
55+
) {
56+
const areElementsEqual = (a, b) => {
57+
if (
58+
isArrayElement(a) ||
59+
isArrayElement(b) ||
60+
isObjectElement(a) ||
61+
isObjectElement(b)
62+
) {
63+
return false;
64+
}
65+
return a.equals(toValue(b));
66+
};
67+
const clone = cloneShallow(targetElement);
68+
clone.content = uniqWith(areElementsEqual)([
69+
...targetElement.content,
70+
...sourceElement.content,
71+
]);
72+
73+
return clone;
74+
}
75+
return deepmerge(targetElement, sourceElement);
76+
};
77+
}
78+
return deepmerge;
79+
},
80+
});
4181

4282
/**
4383
* If there was not an original $$ref value, make sure to remove

src/resolver/specmap/lib/index.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,19 @@ function applyPatch(obj, patch, opts) {
3939
jsonPatch.applyPatch(obj, [replace(patch.path, newValue)]);
4040
} else if (patch.op === 'mergeDeep') {
4141
const currentValue = getInByJsonPath(obj, patch.path);
42-
const newValue = deepmerge(currentValue, patch.value);
42+
const newValue = deepmerge(currentValue, patch.value, {
43+
customMerge: (key) => {
44+
if (key === 'enum') {
45+
return (targetElement, sourceElement) => {
46+
if (Array.isArray(targetElement) && Array.isArray(sourceElement)) {
47+
return [...new Set([...targetElement, ...sourceElement])];
48+
}
49+
return deepmerge(targetElement, sourceElement);
50+
};
51+
}
52+
return undefined;
53+
},
54+
});
4355
obj = jsonPatch.applyPatch(obj, [replace(patch.path, newValue)]).newDocument;
4456
} else if (patch.op === 'add' && patch.path === '' && isObject(patch.value)) {
4557
// { op: 'add', path: '', value: { a: 1, b: 2 }}

test/resolver/apidom/reference/dereference/strategies/openapi-3-1-swagger-client/schema-object/all-of.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,51 @@ describe('dereference', () => {
10181018
},
10191019
});
10201020
});
1021+
1022+
test('should not duplicate primitive `enum` values nested in `allOf`', async () => {
1023+
const spec = OpenApi3_1Element.refract({
1024+
openapi: '3.1.0',
1025+
components: {
1026+
schemas: {
1027+
one: {
1028+
allOf: [
1029+
{
1030+
properties: {
1031+
foo: { enum: [1, 2, 3, 4] },
1032+
bar: { enum: [{ enum: [1, 2, 3] }, { enum: [1, 2, 3, 4] }] },
1033+
},
1034+
},
1035+
{
1036+
properties: {
1037+
foo: { enum: [1, 2, 3, 5, { enum: [1, 2, 3] }] },
1038+
bar: { enum: [{ enum: [1, 2, 3, 4] }] },
1039+
},
1040+
},
1041+
],
1042+
},
1043+
},
1044+
},
1045+
});
1046+
const dereferenced = await dereferenceApiDOM(spec, {
1047+
parse: { mediaType: mediaTypes.latest('json') },
1048+
});
1049+
1050+
expect(toValue(dereferenced)).toEqual({
1051+
openapi: '3.1.0',
1052+
components: {
1053+
schemas: {
1054+
one: {
1055+
properties: {
1056+
foo: { enum: [1, 2, 3, 4, 5, { enum: [1, 2, 3] }] },
1057+
bar: {
1058+
enum: [{ enum: [1, 2, 3] }, { enum: [1, 2, 3, 4] }, { enum: [1, 2, 3, 4] }],
1059+
},
1060+
},
1061+
},
1062+
},
1063+
},
1064+
});
1065+
});
10211066
});
10221067
});
10231068
});

test/resolver/strategies/generic/index.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,40 @@ describe('resolver', () => {
890890
});
891891
});
892892

893+
test('should not duplicate primitive `enum` values nested in `allOf`', () => {
894+
// Given
895+
const spec = {
896+
allOf: [
897+
{
898+
properties: {
899+
foo: { enum: [1, 2, 3, 4] },
900+
bar: { enum: [{ enum: [1, 2, 3] }, { enum: [1, 2, 3, 4] }] },
901+
},
902+
},
903+
{
904+
properties: {
905+
foo: { enum: [1, 2, 3, 5] },
906+
bar: { enum: [{ enum: [1, 2, 3, 4] }] },
907+
},
908+
},
909+
],
910+
};
911+
912+
// When
913+
return Swagger.resolve({ spec }).then(handleResponse);
914+
915+
// Then
916+
function handleResponse(obj) {
917+
expect(obj.errors).toEqual([]);
918+
expect(obj.spec).toEqual({
919+
properties: {
920+
foo: { enum: [1, 2, 3, 4, 5] },
921+
bar: { enum: [{ enum: [1, 2, 3] }, { enum: [1, 2, 3, 4] }, { enum: [1, 2, 3, 4] }] },
922+
},
923+
});
924+
}
925+
});
926+
893927
test('should not throw errors on resvered-keywords in freely-named-fields', () => {
894928
// Given
895929
const ReservedKeywordSpec = jsYaml.load(

0 commit comments

Comments
 (0)