Skip to content

Commit 4eee665

Browse files
authored
feat: add PoC of handling discriminator for allOf case in OpenAPI 3.1 (#4959)
1 parent e8ddbb7 commit 4eee665

File tree

9 files changed

+1026
-13
lines changed

9 files changed

+1026
-13
lines changed

packages/apidom-ast/src/traversal/visitor.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ export const visit = (
455455
nodePredicate = isNode,
456456
nodeCloneFn = cloneNode,
457457
detectCycles = true,
458+
detectCyclesCallback = null,
458459
} = {},
459460
) => {
460461
const visitorKeys = keyMap || {};
@@ -531,6 +532,11 @@ export const visit = (
531532

532533
// cycle detected; skipping over a sub-tree to avoid recursion
533534
if (detectCycles && ancestors.includes(node)) {
535+
if (typeof detectCyclesCallback === 'function') {
536+
// @ts-ignore
537+
detectCyclesCallback(node, key, parent, path, ancestors);
538+
}
539+
534540
path.pop();
535541
continue;
536542
}
@@ -636,6 +642,7 @@ visit[Symbol.for('nodejs.util.promisify.custom')] = async (
636642
nodePredicate = isNode,
637643
nodeCloneFn = cloneNode,
638644
detectCycles = true,
645+
detectCyclesCallback = null,
639646
} = {},
640647
) => {
641648
const visitorKeys = keyMap || {};
@@ -712,6 +719,11 @@ visit[Symbol.for('nodejs.util.promisify.custom')] = async (
712719

713720
// cycle detected; skipping over a sub-tree to avoid recursion
714721
if (detectCycles && ancestors.includes(node)) {
722+
if (typeof detectCyclesCallback === 'function') {
723+
// @ts-ignore
724+
detectCyclesCallback(node, key, parent, path, ancestors);
725+
}
726+
715727
path.pop();
716728
continue;
717729
}

packages/apidom-ns-openapi-3-1/src/refractor/plugins/normalize-discriminator-mapping.ts

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import {
2-
cloneDeep,
2+
ArrayElement,
3+
cloneShallow,
34
Element,
45
isArrayElement,
56
ObjectElement,
67
StringElement,
8+
MemberElement,
79
toValue,
10+
visit,
11+
isMemberElement,
12+
isStringElement,
813
} from '@swagger-api/apidom-core';
914
import { isReferenceLikeElement, isDiscriminatorElement } from '@swagger-api/apidom-ns-openapi-3-0';
1015

@@ -13,15 +18,21 @@ import OpenApi3_1Element from '../../elements/OpenApi3-1.ts';
1318
import NormalizeStorage from './normalize-header-examples/NormalizeStorage.ts';
1419
import { SchemaElement } from '../registration.ts';
1520
import { isSchemaElement } from '../../predicates.ts';
21+
import DiscriminatorElement from '../../elements/Discriminator.ts';
1622

1723
/**
1824
* Normalization of Discriminator.mapping field.
1925
*
2026
* Discriminator.mapping fields are normalized by adding missing mappings from oneOf/anyOf items
2127
* of the parent Schema Object and transforming existing mappings to Schema Objects.
2228
*
29+
* In case of allOf discriminator, the plugin will add missing mappings based on
30+
* allOf items of other Schema Objects.
31+
*
2332
* The normalized mapping is stored in the Schema.discriminator field as `x-normalized-mapping`.
2433
*
34+
* This plugin is designed to be used on dereferenced OpenAPI 3.1 documents.
35+
*
2536
* NOTE: this plugin is idempotent
2637
* @public
2738
*/
@@ -39,12 +50,15 @@ const plugin =
3950
(toolbox: Toolbox) => {
4051
const { ancestorLineageToJSONPointer } = toolbox;
4152
let storage: NormalizeStorage | undefined;
53+
let allOfDiscriminatorMapping: ObjectElement;
4254

4355
return {
4456
visitor: {
4557
OpenApi3_1Element: {
4658
enter(element: OpenApi3_1Element) {
4759
storage = new NormalizeStorage(element, storageField, 'discriminator-mapping');
60+
allOfDiscriminatorMapping =
61+
element.getMetaProperty('allOfDiscriminatorMapping') ?? new ObjectElement();
4862
},
4963
leave() {
5064
storage = undefined;
@@ -80,20 +94,33 @@ const plugin =
8094
return;
8195
}
8296

83-
// skip if neither oneOf nor anyOf is present
84-
if (!isArrayElement(schemaElement.oneOf) && !isArrayElement(schemaElement.anyOf)) {
97+
const parentElement = ancestors[ancestors.length - 1];
98+
const schemaName = schemaElement.getMetaProperty('schemaName');
99+
const allOfMapping = allOfDiscriminatorMapping.getMember(toValue(schemaName));
100+
const hasAllOfMapping =
101+
// @ts-ignore
102+
allOfMapping && !parentElement?.classes?.contains('json-schema-allOf');
103+
104+
// skip if neither oneOf, anyOf nor allOf is present
105+
if (
106+
!isArrayElement(schemaElement.oneOf) &&
107+
!isArrayElement(schemaElement.anyOf) &&
108+
!hasAllOfMapping
109+
) {
85110
return;
86111
}
87112

88113
const mapping = schemaElement.discriminator.get('mapping') ?? new ObjectElement();
89-
const normalizedMapping: ObjectElement = cloneDeep(mapping);
114+
const normalizedMapping = new ObjectElement();
90115
let isNormalized = true;
91116

92117
const items = isArrayElement(schemaElement.oneOf)
93118
? schemaElement.oneOf
94-
: schemaElement.anyOf;
119+
: isArrayElement(schemaElement.anyOf)
120+
? schemaElement.anyOf
121+
: (allOfMapping.value as ArrayElement);
95122

96-
items!.forEach((item) => {
123+
items.forEach((item) => {
97124
if (!isSchemaElement(item)) {
98125
return;
99126
}
@@ -111,7 +138,10 @@ const plugin =
111138
* handle external references and internal references
112139
* that don't point to components/schemas/<SchemaName>
113140
*/
114-
if (metaRefOrigin !== baseURI || (!metaSchemaName && metaRefFields)) {
141+
if (
142+
!hasAllOfMapping &&
143+
(metaRefOrigin !== baseURI || (!metaSchemaName && metaRefFields))
144+
) {
115145
let hasMatchingMapping = false;
116146

117147
mapping.forEach((mappingValue: StringElement, mappingKey: StringElement) => {
@@ -121,7 +151,7 @@ const plugin =
121151
?.get('$refBaseURI');
122152

123153
if (mappingValueSchemaRefBaseURI?.equals(metaRefFields?.$refBaseURI)) {
124-
normalizedMapping.set(toValue(mappingKey), cloneDeep(item));
154+
normalizedMapping.set(toValue(mappingKey), cloneShallow(item));
125155
hasMatchingMapping = true;
126156
}
127157
});
@@ -145,28 +175,74 @@ const plugin =
145175

146176
if (
147177
mappingValueSchemaName?.equals(metaSchemaName) &&
148-
mappingValueSchemaRefBaseURI?.equals(metaRefFields?.$refBaseURI)
178+
(!hasAllOfMapping ||
179+
mappingValueSchemaRefBaseURI?.equals(metaRefFields?.$refBaseURI))
149180
) {
150-
normalizedMapping.set(toValue(mappingKey), cloneDeep(item));
181+
normalizedMapping.set(toValue(mappingKey), cloneShallow(item));
151182
hasMatchingMapping = true;
152183
}
153184
});
154185

155186
// add a new mapping if no matching mapping was found
156187
if (!hasMatchingMapping) {
157-
normalizedMapping.set(metaSchemaName, cloneDeep(item));
188+
normalizedMapping.set(metaSchemaName, cloneShallow(item));
158189
}
159190
}
160191
});
161192

162-
// check if any mapping is not a Schema Object
193+
// check if any mapping is not a Schema Object or if any mapping was not normalized
194+
const mappingKeys = mapping.keys();
195+
const normalizedMappingKeys = normalizedMapping.keys();
163196
isNormalized =
164197
isNormalized &&
165198
normalizedMapping.filter((mappingValue: Element) => !isSchemaElement(mappingValue))
166-
.length === 0;
199+
.length === 0 &&
200+
mappingKeys.every((mappingKey: string) => normalizedMappingKeys.includes(mappingKey));
167201

168202
if (isNormalized) {
169203
schemaElement.discriminator.set('x-normalized-mapping', normalizedMapping);
204+
205+
// dive in and eliminate cycles that might be created by normalization
206+
visit(
207+
schemaElement,
208+
{},
209+
{
210+
// @ts-ignore
211+
detectCyclesCallback: <T extends Element>(
212+
node: T,
213+
nodeKey: string | number,
214+
nodeParent: Element | undefined,
215+
) => {
216+
if (
217+
!nodeParent ||
218+
!isMemberElement(node) ||
219+
!isStringElement(node.key) ||
220+
!node.key.equals('discriminator') ||
221+
!isDiscriminatorElement(node.value)
222+
) {
223+
return;
224+
}
225+
226+
const discriminator = cloneShallow(node.value);
227+
const discriminatorCopy = new DiscriminatorElement();
228+
229+
if (discriminator.get('mapping')) {
230+
discriminatorCopy.mapping = discriminator.get('mapping');
231+
}
232+
233+
if (discriminator.get('propertyName')) {
234+
discriminatorCopy.propertyName = discriminator.get('propertyName');
235+
}
236+
237+
// eslint-disable-next-line no-param-reassign
238+
nodeParent[nodeKey] = new MemberElement(
239+
new StringElement('discriminator'),
240+
discriminatorCopy,
241+
);
242+
},
243+
},
244+
);
245+
170246
storage!.append(schemaJSONPointer);
171247
}
172248
},

0 commit comments

Comments
 (0)