11import {
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' ;
914import { isReferenceLikeElement , isDiscriminatorElement } from '@swagger-api/apidom-ns-openapi-3-0' ;
1015
@@ -13,15 +18,21 @@ import OpenApi3_1Element from '../../elements/OpenApi3-1.ts';
1318import NormalizeStorage from './normalize-header-examples/NormalizeStorage.ts' ;
1419import { SchemaElement } from '../registration.ts' ;
1520import { 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