1
1
import {
2
- cloneDeep ,
2
+ ArrayElement ,
3
+ cloneShallow ,
3
4
Element ,
4
5
isArrayElement ,
5
6
ObjectElement ,
6
7
StringElement ,
8
+ MemberElement ,
7
9
toValue ,
10
+ visit ,
11
+ isMemberElement ,
12
+ isStringElement ,
8
13
} from '@swagger-api/apidom-core' ;
9
14
import { isReferenceLikeElement , isDiscriminatorElement } from '@swagger-api/apidom-ns-openapi-3-0' ;
10
15
@@ -13,15 +18,21 @@ import OpenApi3_1Element from '../../elements/OpenApi3-1.ts';
13
18
import NormalizeStorage from './normalize-header-examples/NormalizeStorage.ts' ;
14
19
import { SchemaElement } from '../registration.ts' ;
15
20
import { isSchemaElement } from '../../predicates.ts' ;
21
+ import DiscriminatorElement from '../../elements/Discriminator.ts' ;
16
22
17
23
/**
18
24
* Normalization of Discriminator.mapping field.
19
25
*
20
26
* Discriminator.mapping fields are normalized by adding missing mappings from oneOf/anyOf items
21
27
* of the parent Schema Object and transforming existing mappings to Schema Objects.
22
28
*
29
+ * In case of allOf discriminator, the plugin will add missing mappings based on
30
+ * allOf items of other Schema Objects.
31
+ *
23
32
* The normalized mapping is stored in the Schema.discriminator field as `x-normalized-mapping`.
24
33
*
34
+ * This plugin is designed to be used on dereferenced OpenAPI 3.1 documents.
35
+ *
25
36
* NOTE: this plugin is idempotent
26
37
* @public
27
38
*/
@@ -39,12 +50,15 @@ const plugin =
39
50
( toolbox : Toolbox ) => {
40
51
const { ancestorLineageToJSONPointer } = toolbox ;
41
52
let storage : NormalizeStorage | undefined ;
53
+ let allOfDiscriminatorMapping : ObjectElement ;
42
54
43
55
return {
44
56
visitor : {
45
57
OpenApi3_1Element : {
46
58
enter ( element : OpenApi3_1Element ) {
47
59
storage = new NormalizeStorage ( element , storageField , 'discriminator-mapping' ) ;
60
+ allOfDiscriminatorMapping =
61
+ element . getMetaProperty ( 'allOfDiscriminatorMapping' ) ?? new ObjectElement ( ) ;
48
62
} ,
49
63
leave ( ) {
50
64
storage = undefined ;
@@ -80,20 +94,33 @@ const plugin =
80
94
return ;
81
95
}
82
96
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
+ ) {
85
110
return ;
86
111
}
87
112
88
113
const mapping = schemaElement . discriminator . get ( 'mapping' ) ?? new ObjectElement ( ) ;
89
- const normalizedMapping : ObjectElement = cloneDeep ( mapping ) ;
114
+ const normalizedMapping = new ObjectElement ( ) ;
90
115
let isNormalized = true ;
91
116
92
117
const items = isArrayElement ( schemaElement . oneOf )
93
118
? schemaElement . oneOf
94
- : schemaElement . anyOf ;
119
+ : isArrayElement ( schemaElement . anyOf )
120
+ ? schemaElement . anyOf
121
+ : ( allOfMapping . value as ArrayElement ) ;
95
122
96
- items ! . forEach ( ( item ) => {
123
+ items . forEach ( ( item ) => {
97
124
if ( ! isSchemaElement ( item ) ) {
98
125
return ;
99
126
}
@@ -111,7 +138,10 @@ const plugin =
111
138
* handle external references and internal references
112
139
* that don't point to components/schemas/<SchemaName>
113
140
*/
114
- if ( metaRefOrigin !== baseURI || ( ! metaSchemaName && metaRefFields ) ) {
141
+ if (
142
+ ! hasAllOfMapping &&
143
+ ( metaRefOrigin !== baseURI || ( ! metaSchemaName && metaRefFields ) )
144
+ ) {
115
145
let hasMatchingMapping = false ;
116
146
117
147
mapping . forEach ( ( mappingValue : StringElement , mappingKey : StringElement ) => {
@@ -121,7 +151,7 @@ const plugin =
121
151
?. get ( '$refBaseURI' ) ;
122
152
123
153
if ( mappingValueSchemaRefBaseURI ?. equals ( metaRefFields ?. $refBaseURI ) ) {
124
- normalizedMapping . set ( toValue ( mappingKey ) , cloneDeep ( item ) ) ;
154
+ normalizedMapping . set ( toValue ( mappingKey ) , cloneShallow ( item ) ) ;
125
155
hasMatchingMapping = true ;
126
156
}
127
157
} ) ;
@@ -145,28 +175,74 @@ const plugin =
145
175
146
176
if (
147
177
mappingValueSchemaName ?. equals ( metaSchemaName ) &&
148
- mappingValueSchemaRefBaseURI ?. equals ( metaRefFields ?. $refBaseURI )
178
+ ( ! hasAllOfMapping ||
179
+ mappingValueSchemaRefBaseURI ?. equals ( metaRefFields ?. $refBaseURI ) )
149
180
) {
150
- normalizedMapping . set ( toValue ( mappingKey ) , cloneDeep ( item ) ) ;
181
+ normalizedMapping . set ( toValue ( mappingKey ) , cloneShallow ( item ) ) ;
151
182
hasMatchingMapping = true ;
152
183
}
153
184
} ) ;
154
185
155
186
// add a new mapping if no matching mapping was found
156
187
if ( ! hasMatchingMapping ) {
157
- normalizedMapping . set ( metaSchemaName , cloneDeep ( item ) ) ;
188
+ normalizedMapping . set ( metaSchemaName , cloneShallow ( item ) ) ;
158
189
}
159
190
}
160
191
} ) ;
161
192
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 ( ) ;
163
196
isNormalized =
164
197
isNormalized &&
165
198
normalizedMapping . filter ( ( mappingValue : Element ) => ! isSchemaElement ( mappingValue ) )
166
- . length === 0 ;
199
+ . length === 0 &&
200
+ mappingKeys . every ( ( mappingKey : string ) => normalizedMappingKeys . includes ( mappingKey ) ) ;
167
201
168
202
if ( isNormalized ) {
169
203
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
+
170
246
storage ! . append ( schemaJSONPointer ) ;
171
247
}
172
248
} ,
0 commit comments