1
+ from typing import Dict , List , Optional , Tuple , Union
2
+
3
+ import pytest
4
+ from attr import evolve
5
+
1
6
import openapi_python_client .schema as oai
7
+ from openapi_python_client .config import Config
2
8
from openapi_python_client .parser .errors import ParseError , PropertyError
3
9
from openapi_python_client .parser .properties import Schemas , UnionProperty
10
+ from openapi_python_client .parser .properties .model_property import ModelProperty
11
+ from openapi_python_client .parser .properties .property import Property
4
12
from openapi_python_client .parser .properties .protocol import Value
13
+ from openapi_python_client .parser .properties .schemas import Class
5
14
from openapi_python_client .schema import DataType , ParameterLocation
15
+ from tests .test_parser .test_properties .properties_test_helpers import assert_prop_error
6
16
7
17
8
18
def test_property_from_data_union (union_property_factory , date_time_property_factory , string_property_factory , config ):
@@ -33,6 +43,206 @@ def test_property_from_data_union(union_property_factory, date_time_property_fac
33
43
assert s == Schemas ()
34
44
35
45
46
+ def _make_basic_model (
47
+ name : str ,
48
+ props : Dict [str , oai .Schema ],
49
+ required_prop : Optional [str ],
50
+ schemas : Schemas ,
51
+ config : Config ,
52
+ ) -> Tuple [ModelProperty , Schemas ]:
53
+ model , schemas = ModelProperty .build (
54
+ data = oai .Schema .model_construct (
55
+ required = [required_prop ] if required_prop else [],
56
+ title = name ,
57
+ properties = props ,
58
+ ),
59
+ name = name or "some_generated_name" ,
60
+ schemas = schemas ,
61
+ required = False ,
62
+ parent_name = "" ,
63
+ config = config ,
64
+ roots = {"root" },
65
+ process_properties = True ,
66
+ )
67
+ assert isinstance (model , ModelProperty )
68
+ if name :
69
+ schemas = evolve (
70
+ schemas , classes_by_reference = {** schemas .classes_by_reference , f"/components/schemas/{ name } " : model }
71
+ )
72
+ return model , schemas
73
+
74
+
75
+ def _assert_valid_discriminator (
76
+ p : Union [Property , PropertyError ],
77
+ expected_discriminators : List [Tuple [str , Dict [str , Class ]]],
78
+ ) -> None :
79
+ assert isinstance (p , UnionProperty )
80
+ assert p .discriminators
81
+ assert [(d [0 ], {key : model .class_info for key , model in d [1 ].items ()}) for d in expected_discriminators ] == [
82
+ (d .property_name , {key : model .class_info for key , model in d .value_to_model_map .items ()})
83
+ for d in p .discriminators
84
+ ]
85
+
86
+
87
+ def test_discriminator_with_explicit_mapping (config ):
88
+ from openapi_python_client .parser .properties import Schemas , property_from_data
89
+
90
+ schemas = Schemas ()
91
+ props = {"type" : oai .Schema .model_construct (type = "string" )}
92
+ model1 , schemas = _make_basic_model ("Model1" , props , "type" , schemas , config )
93
+ model2 , schemas = _make_basic_model ("Model2" , props , "type" , schemas , config )
94
+ data = oai .Schema .model_construct (
95
+ oneOf = [
96
+ oai .Reference (ref = "#/components/schemas/Model1" ),
97
+ oai .Reference (ref = "#/components/schemas/Model2" ),
98
+ ],
99
+ discriminator = oai .Discriminator .model_construct (
100
+ propertyName = "type" ,
101
+ mapping = {
102
+ # mappings can use either a fully-qualified schema reference or just the schema name
103
+ "type1" : "#/components/schemas/Model1" ,
104
+ "type2" : "Model2" ,
105
+ },
106
+ ),
107
+ )
108
+
109
+ p , schemas = property_from_data (
110
+ name = "MyUnion" , required = False , data = data , schemas = schemas , parent_name = "parent" , config = config
111
+ )
112
+ _assert_valid_discriminator (p , [("type" , {"type1" : model1 , "type2" : model2 })])
113
+
114
+
115
+ def test_discriminator_with_implicit_mapping (config ):
116
+ from openapi_python_client .parser .properties import Schemas , property_from_data
117
+
118
+ schemas = Schemas ()
119
+ props = {"type" : oai .Schema .model_construct (type = "string" )}
120
+ model1 , schemas = _make_basic_model ("Model1" , props , "type" , schemas , config )
121
+ model2 , schemas = _make_basic_model ("Model2" , props , "type" , schemas , config )
122
+ data = oai .Schema .model_construct (
123
+ oneOf = [
124
+ oai .Reference (ref = "#/components/schemas/Model1" ),
125
+ oai .Reference (ref = "#/components/schemas/Model2" ),
126
+ ],
127
+ discriminator = oai .Discriminator .model_construct (
128
+ propertyName = "type" ,
129
+ ),
130
+ )
131
+
132
+ p , schemas = property_from_data (
133
+ name = "MyUnion" , required = False , data = data , schemas = schemas , parent_name = "parent" , config = config
134
+ )
135
+ _assert_valid_discriminator (p , [("type" , {"Model1" : model1 , "Model2" : model2 })])
136
+
137
+
138
+ def test_discriminator_with_partial_explicit_mapping (config ):
139
+ from openapi_python_client .parser .properties import Schemas , property_from_data
140
+
141
+ schemas = Schemas ()
142
+ props = {"type" : oai .Schema .model_construct (type = "string" )}
143
+ model1 , schemas = _make_basic_model ("Model1" , props , "type" , schemas , config )
144
+ model2 , schemas = _make_basic_model ("Model2" , props , "type" , schemas , config )
145
+ data = oai .Schema .model_construct (
146
+ oneOf = [
147
+ oai .Reference (ref = "#/components/schemas/Model1" ),
148
+ oai .Reference (ref = "#/components/schemas/Model2" ),
149
+ ],
150
+ discriminator = oai .Discriminator .model_construct (
151
+ propertyName = "type" ,
152
+ mapping = {
153
+ "type1" : "#/components/schemas/Model1" ,
154
+ # no value specified for Model2, so it defaults to just "Model2"
155
+ },
156
+ ),
157
+ )
158
+
159
+ p , schemas = property_from_data (
160
+ name = "MyUnion" , required = False , data = data , schemas = schemas , parent_name = "parent" , config = config
161
+ )
162
+ _assert_valid_discriminator (p , [("type" , {"type1" : model1 , "Model2" : model2 })])
163
+
164
+
165
+ def test_discriminators_in_nested_unions_same_property (config ):
166
+ from openapi_python_client .parser .properties import Schemas , property_from_data
167
+
168
+ schemas = Schemas ()
169
+ props = {"type" : oai .Schema .model_construct (type = "string" )}
170
+ model1 , schemas = _make_basic_model ("Model1" , props , "type" , schemas , config )
171
+ model2 , schemas = _make_basic_model ("Model2" , props , "type" , schemas , config )
172
+ model3 , schemas = _make_basic_model ("Model3" , props , "type" , schemas , config )
173
+ model4 , schemas = _make_basic_model ("Model4" , props , "type" , schemas , config )
174
+ data = oai .Schema .model_construct (
175
+ oneOf = [
176
+ oai .Schema .model_construct (
177
+ oneOf = [
178
+ oai .Reference (ref = "#/components/schemas/Model1" ),
179
+ oai .Reference (ref = "#/components/schemas/Model2" ),
180
+ ],
181
+ discriminator = oai .Discriminator .model_construct (propertyName = "type" ),
182
+ ),
183
+ oai .Schema .model_construct (
184
+ oneOf = [
185
+ oai .Reference (ref = "#/components/schemas/Model3" ),
186
+ oai .Reference (ref = "#/components/schemas/Model4" ),
187
+ ],
188
+ discriminator = oai .Discriminator .model_construct (propertyName = "type" ),
189
+ ),
190
+ ],
191
+ )
192
+
193
+ p , schemas = property_from_data (
194
+ name = "MyUnion" , required = False , data = data , schemas = schemas , parent_name = "parent" , config = config
195
+ )
196
+ _assert_valid_discriminator (
197
+ p ,
198
+ [
199
+ ("type" , {"Model1" : model1 , "Model2" : model2 }),
200
+ ("type" , {"Model3" : model3 , "Model4" : model4 }),
201
+ ],
202
+ )
203
+
204
+
205
+ def test_discriminators_in_nested_unions_different_property (config ):
206
+ from openapi_python_client .parser .properties import Schemas , property_from_data
207
+
208
+ schemas = Schemas ()
209
+ props1 = {"type" : oai .Schema .model_construct (type = "string" )}
210
+ props2 = {"other" : oai .Schema .model_construct (type = "string" )}
211
+ model1 , schemas = _make_basic_model ("Model1" , props1 , "type" , schemas , config )
212
+ model2 , schemas = _make_basic_model ("Model2" , props1 , "type" , schemas , config )
213
+ model3 , schemas = _make_basic_model ("Model3" , props2 , "other" , schemas , config )
214
+ model4 , schemas = _make_basic_model ("Model4" , props2 , "other" , schemas , config )
215
+ data = oai .Schema .model_construct (
216
+ oneOf = [
217
+ oai .Schema .model_construct (
218
+ oneOf = [
219
+ oai .Reference (ref = "#/components/schemas/Model1" ),
220
+ oai .Reference (ref = "#/components/schemas/Model2" ),
221
+ ],
222
+ discriminator = oai .Discriminator .model_construct (propertyName = "type" ),
223
+ ),
224
+ oai .Schema .model_construct (
225
+ oneOf = [
226
+ oai .Reference (ref = "#/components/schemas/Model3" ),
227
+ oai .Reference (ref = "#/components/schemas/Model4" ),
228
+ ],
229
+ discriminator = oai .Discriminator .model_construct (propertyName = "other" ),
230
+ ),
231
+ ],
232
+ )
233
+
234
+ p , schemas = property_from_data (
235
+ name = "MyUnion" , required = False , data = data , schemas = schemas , parent_name = "parent" , config = config
236
+ )
237
+ _assert_valid_discriminator (
238
+ p ,
239
+ [
240
+ ("type" , {"Model1" : model1 , "Model2" : model2 }),
241
+ ("other" , {"Model3" : model3 , "Model4" : model4 }),
242
+ ],
243
+ )
244
+
245
+
36
246
def test_build_union_property_invalid_property (config ):
37
247
name = "bad_union"
38
248
required = True
@@ -42,7 +252,7 @@ def test_build_union_property_invalid_property(config):
42
252
p , s = UnionProperty .build (
43
253
name = name , required = required , data = data , schemas = Schemas (), parent_name = "parent" , config = config
44
254
)
45
- assert p == PropertyError ( detail = f"Invalid property in union { name } " , data = reference )
255
+ assert_prop_error ( p , f"Invalid property in union { name } " , data = reference )
46
256
47
257
48
258
def test_invalid_default (config ):
@@ -82,3 +292,155 @@ def test_not_required_in_path(config):
82
292
83
293
err = prop .validate_location (ParameterLocation .PATH )
84
294
assert isinstance (err , ParseError )
295
+
296
+
297
+ @pytest .mark .parametrize ("bad_ref" , ["#/components/schemas/UnknownModel" , "http://remote/Model2" ])
298
+ def test_discriminator_invalid_reference (bad_ref , config ):
299
+ from openapi_python_client .parser .properties import Schemas , property_from_data
300
+
301
+ schemas = Schemas ()
302
+ props = {"type" : oai .Schema .model_construct (type = "string" )}
303
+ model1 , schemas = _make_basic_model ("Model1" , props , "type" , schemas , config )
304
+ model2 , schemas = _make_basic_model ("Model2" , props , "type" , schemas , config )
305
+ data = oai .Schema .model_construct (
306
+ oneOf = [
307
+ oai .Reference (ref = "#/components/schemas/Model1" ),
308
+ oai .Reference (ref = "#/components/schemas/Model2" ),
309
+ ],
310
+ discriminator = oai .Discriminator .model_construct (
311
+ propertyName = "type" ,
312
+ mapping = {
313
+ "Model1" : "#/components/schemas/Model1" ,
314
+ "Model2" : bad_ref ,
315
+ },
316
+ ),
317
+ )
318
+
319
+ p , schemas = property_from_data (
320
+ name = "MyUnion" , required = False , data = data , schemas = schemas , parent_name = "parent" , config = config
321
+ )
322
+ assert_prop_error (p , "^Invalid reference" )
323
+
324
+
325
+ def test_discriminator_mapping_uses_schema_not_in_list (config ):
326
+ from openapi_python_client .parser .properties import Schemas , property_from_data
327
+
328
+ schemas = Schemas ()
329
+ props = {"type" : oai .Schema .model_construct (type = "string" )}
330
+ model1 , schemas = _make_basic_model ("Model1" , props , "type" , schemas , config )
331
+ model2 , schemas = _make_basic_model ("Model2" , props , "type" , schemas , config )
332
+ model3 , schemas = _make_basic_model ("Model3" , props , "type" , schemas , config )
333
+ data = oai .Schema .model_construct (
334
+ oneOf = [
335
+ oai .Reference (ref = "#/components/schemas/Model1" ),
336
+ oai .Reference (ref = "#/components/schemas/Model2" ),
337
+ ],
338
+ discriminator = oai .Discriminator .model_construct (
339
+ propertyName = "type" ,
340
+ mapping = {
341
+ "Model1" : "#/components/schemas/Model1" ,
342
+ "Model3" : "#/components/schemas/Model3" ,
343
+ },
344
+ ),
345
+ )
346
+
347
+ p , schemas = property_from_data (
348
+ name = "MyUnion" , required = False , data = data , schemas = schemas , parent_name = "parent" , config = config
349
+ )
350
+ assert_prop_error (p , "not one of the schema variants" )
351
+
352
+
353
+ def test_discriminator_invalid_variant_is_not_object (config , string_property_factory ):
354
+ from openapi_python_client .parser .properties import Schemas , property_from_data
355
+
356
+ schemas = Schemas ()
357
+ props = {"type" : oai .Schema .model_construct (type = "string" )}
358
+ model_type , schemas = _make_basic_model ("ModelType" , props , "type" , schemas , config )
359
+ string_type = string_property_factory ()
360
+ schemas = evolve (
361
+ schemas ,
362
+ classes_by_reference = {
363
+ ** schemas .classes_by_reference ,
364
+ "/components/schemas/StringType" : string_type ,
365
+ },
366
+ )
367
+ data = oai .Schema .model_construct (
368
+ oneOf = [
369
+ oai .Reference (ref = "#/components/schemas/ModelType" ),
370
+ oai .Reference (ref = "#/components/schemas/StringType" ),
371
+ ],
372
+ discriminator = oai .Discriminator .model_construct (
373
+ propertyName = "type" ,
374
+ ),
375
+ )
376
+
377
+ p , schemas = property_from_data (
378
+ name = "MyUnion" , required = False , data = data , schemas = schemas , parent_name = "parent" , config = config
379
+ )
380
+ assert_prop_error (p , "must be objects" )
381
+
382
+
383
+ def test_discriminator_invalid_inline_schema_variant (config , string_property_factory ):
384
+ from openapi_python_client .parser .properties import Schemas , property_from_data
385
+
386
+ schemas = Schemas ()
387
+ schemas = Schemas ()
388
+ props = {"type" : oai .Schema .model_construct (type = "string" )}
389
+ model1 , schemas = _make_basic_model ("Model1" , props , "type" , schemas , config )
390
+ data = oai .Schema .model_construct (
391
+ oneOf = [
392
+ oai .Reference (ref = "#/components/schemas/Model1" ),
393
+ oai .Schema .model_construct (
394
+ type = "object" ,
395
+ properties = props ,
396
+ ),
397
+ ],
398
+ discriminator = oai .Discriminator .model_construct (
399
+ propertyName = "type" ,
400
+ ),
401
+ )
402
+
403
+ p , schemas = property_from_data (
404
+ name = "MyUnion" , required = False , data = data , schemas = schemas , parent_name = "parent" , config = config
405
+ )
406
+ assert_prop_error (p , "Inline schema" )
407
+
408
+
409
+ def test_conflicting_discriminator_mappings (config ):
410
+ from openapi_python_client .parser .properties import Schemas , property_from_data
411
+
412
+ schemas = Schemas ()
413
+ props = {"type" : oai .Schema .model_construct (type = "string" )}
414
+ model1 , schemas = _make_basic_model ("Model1" , props , "type" , schemas , config )
415
+ model2 , schemas = _make_basic_model ("Model2" , props , "type" , schemas , config )
416
+ model3 , schemas = _make_basic_model ("Model3" , props , "type" , schemas , config )
417
+ model4 , schemas = _make_basic_model ("Model4" , props , "type" , schemas , config )
418
+ data = oai .Schema .model_construct (
419
+ oneOf = [
420
+ oai .Schema .model_construct (
421
+ oneOf = [
422
+ oai .Reference (ref = "#/components/schemas/Model1" ),
423
+ oai .Reference (ref = "#/components/schemas/Model2" ),
424
+ ],
425
+ discriminator = oai .Discriminator .model_construct (
426
+ propertyName = "type" ,
427
+ mapping = {"a" : "Model1" , "b" : "Model2" },
428
+ ),
429
+ ),
430
+ oai .Schema .model_construct (
431
+ oneOf = [
432
+ oai .Reference (ref = "#/components/schemas/Model3" ),
433
+ oai .Reference (ref = "#/components/schemas/Model4" ),
434
+ ],
435
+ discriminator = oai .Discriminator .model_construct (
436
+ propertyName = "type" ,
437
+ mapping = {"a" : "Model3" , "x" : "Model4" },
438
+ ),
439
+ ),
440
+ ],
441
+ )
442
+
443
+ p , schemas = property_from_data (
444
+ name = "MyUnion" , required = False , data = data , schemas = schemas , parent_name = "parent" , config = config
445
+ )
446
+ assert_prop_error (p , '"type" had more than one schema for value "a"' )
0 commit comments