voluptuous-openapi 0.0.7__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {voluptuous_openapi-0.0.7/voluptuous_openapi.egg-info → voluptuous_openapi-0.2.0}/PKG-INFO +1 -1
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/pyproject.toml +1 -1
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/tests/test_lib.py +225 -1
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/tests/test_validation.py +197 -6
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/voluptuous_openapi/__init__.py +109 -41
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0/voluptuous_openapi.egg-info}/PKG-INFO +1 -1
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/LICENSE +0 -0
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/README.md +0 -0
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/setup.cfg +0 -0
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/voluptuous_openapi.egg-info/SOURCES.txt +0 -0
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/voluptuous_openapi.egg-info/dependency_links.txt +0 -0
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/voluptuous_openapi.egg-info/requires.txt +0 -0
- {voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/voluptuous_openapi.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,12 @@ from typing import Any, TypeVar
|
|
|
4
4
|
import pytest
|
|
5
5
|
import voluptuous as vol
|
|
6
6
|
|
|
7
|
-
from voluptuous_openapi import
|
|
7
|
+
from voluptuous_openapi import (
|
|
8
|
+
UNSUPPORTED,
|
|
9
|
+
convert,
|
|
10
|
+
convert_to_voluptuous,
|
|
11
|
+
OpenApiVersion,
|
|
12
|
+
)
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
def test_int_schema():
|
|
@@ -226,6 +231,15 @@ def test_maybe():
|
|
|
226
231
|
} == convert(vol.Schema(vol.Maybe(str)))
|
|
227
232
|
|
|
228
233
|
|
|
234
|
+
def test_maybe_v3_1():
|
|
235
|
+
assert {
|
|
236
|
+
"anyOf": [
|
|
237
|
+
{"type": "null"},
|
|
238
|
+
{"type": "string"},
|
|
239
|
+
],
|
|
240
|
+
} == convert(vol.Schema(vol.Maybe(str)), openapi_version=OpenApiVersion.V3_1)
|
|
241
|
+
|
|
242
|
+
|
|
229
243
|
def test_custom_serializer():
|
|
230
244
|
def custem_serializer(schema):
|
|
231
245
|
if schema is str:
|
|
@@ -257,6 +271,12 @@ def test_constant():
|
|
|
257
271
|
"nullable": True,
|
|
258
272
|
"description": "Must be null",
|
|
259
273
|
} == convert(vol.Schema(type(None)))
|
|
274
|
+
assert {
|
|
275
|
+
"type": "null",
|
|
276
|
+
} == convert(vol.Schema(None), openapi_version=OpenApiVersion.V3_1)
|
|
277
|
+
assert {
|
|
278
|
+
"type": "null",
|
|
279
|
+
} == convert(vol.Schema(type(None)), openapi_version=OpenApiVersion.V3_1)
|
|
260
280
|
|
|
261
281
|
|
|
262
282
|
def test_enum():
|
|
@@ -327,6 +347,11 @@ def test_any_of():
|
|
|
327
347
|
"anyOf": [{"type": "number"}, {"type": "integer"}],
|
|
328
348
|
"nullable": True,
|
|
329
349
|
} == convert(vol.Any(vol.Maybe(float), vol.Maybe(int)))
|
|
350
|
+
assert {
|
|
351
|
+
"anyOf": [{"type": "null"}, {"type": "number"}, {"type": "integer"}],
|
|
352
|
+
} == convert(
|
|
353
|
+
vol.Any(vol.Maybe(float), vol.Maybe(int)), openapi_version=OpenApiVersion.V3_1
|
|
354
|
+
)
|
|
330
355
|
|
|
331
356
|
|
|
332
357
|
def test_all_of():
|
|
@@ -421,6 +446,9 @@ def test_function():
|
|
|
421
446
|
assert {"type": "number", "nullable": True} == convert(
|
|
422
447
|
vol.Schema(validator_nullable)
|
|
423
448
|
)
|
|
449
|
+
assert {"anyOf": [{"type": "number"}, {"type": "null"}]} == convert(
|
|
450
|
+
vol.Schema(validator_nullable), openapi_version=OpenApiVersion.V3_1
|
|
451
|
+
)
|
|
424
452
|
|
|
425
453
|
def validator_union(data: float | int):
|
|
426
454
|
return data
|
|
@@ -606,3 +634,199 @@ def test_convert_to_voluptuous_marker_description(required: list[str]):
|
|
|
606
634
|
("query", "The query to search for"),
|
|
607
635
|
("max_results", "The maximum number of results to return"),
|
|
608
636
|
]
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def test_convert_to_voluptuous_nullable_string_openapi_3_0():
|
|
640
|
+
"""Test OpenAPI 3.0 nullable string."""
|
|
641
|
+
validator = convert_to_voluptuous({"type": "string", "nullable": True})
|
|
642
|
+
validator("test")
|
|
643
|
+
validator(None)
|
|
644
|
+
|
|
645
|
+
with pytest.raises(vol.Invalid):
|
|
646
|
+
validator(123)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def test_convert_to_voluptuous_non_nullable_string_openapi_3_0():
|
|
650
|
+
"""Test OpenAPI 3.0 non-nullable string."""
|
|
651
|
+
validator_type = convert_to_voluptuous({"type": "string", "nullable": False})
|
|
652
|
+
# Wrap in a Schema for proper validation
|
|
653
|
+
validator = vol.Schema(validator_type)
|
|
654
|
+
validator("test")
|
|
655
|
+
validator("") # empty string should be valid
|
|
656
|
+
|
|
657
|
+
with pytest.raises(vol.Invalid):
|
|
658
|
+
validator(None)
|
|
659
|
+
|
|
660
|
+
with pytest.raises(vol.Invalid):
|
|
661
|
+
validator(123)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def test_convert_to_voluptuous_nullable_integer_openapi_3_0():
|
|
665
|
+
"""Test OpenAPI 3.0 nullable integer."""
|
|
666
|
+
validator = convert_to_voluptuous({"type": "integer", "nullable": True})
|
|
667
|
+
validator(42)
|
|
668
|
+
validator(None)
|
|
669
|
+
|
|
670
|
+
with pytest.raises(vol.Invalid):
|
|
671
|
+
validator("string")
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def test_convert_to_voluptuous_nullable_number_openapi_3_0():
|
|
675
|
+
"""Test OpenAPI 3.0 nullable number."""
|
|
676
|
+
validator = convert_to_voluptuous({"type": "number", "nullable": True})
|
|
677
|
+
validator(3.14)
|
|
678
|
+
validator(None)
|
|
679
|
+
|
|
680
|
+
with pytest.raises(vol.Invalid):
|
|
681
|
+
validator("string")
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def test_convert_to_voluptuous_nullable_boolean_openapi_3_0():
|
|
685
|
+
"""Test OpenAPI 3.0 nullable boolean."""
|
|
686
|
+
validator = convert_to_voluptuous({"type": "boolean", "nullable": True})
|
|
687
|
+
validator(True)
|
|
688
|
+
validator(False)
|
|
689
|
+
validator(None)
|
|
690
|
+
|
|
691
|
+
with pytest.raises(vol.Invalid):
|
|
692
|
+
validator("string")
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def test_convert_to_voluptuous_nullable_object_openapi_3_0():
|
|
696
|
+
"""Test OpenAPI 3.0 nullable object."""
|
|
697
|
+
validator = convert_to_voluptuous(
|
|
698
|
+
{"type": "object", "properties": {"name": {"type": "string"}}, "nullable": True}
|
|
699
|
+
)
|
|
700
|
+
validator({"name": "test"})
|
|
701
|
+
validator(None)
|
|
702
|
+
|
|
703
|
+
with pytest.raises(vol.Invalid):
|
|
704
|
+
validator("string")
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def test_convert_to_voluptuous_nullable_array_openapi_3_0():
|
|
708
|
+
"""Test OpenAPI 3.0 nullable array."""
|
|
709
|
+
validator = convert_to_voluptuous(
|
|
710
|
+
{"type": "array", "items": {"type": "string"}, "nullable": True}
|
|
711
|
+
)
|
|
712
|
+
validator(["a", "b"])
|
|
713
|
+
validator(None)
|
|
714
|
+
|
|
715
|
+
with pytest.raises(vol.Invalid):
|
|
716
|
+
validator("string")
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def test_convert_to_voluptuous_nullable_string_openapi_3_1():
|
|
720
|
+
"""Test OpenAPI 3.1 nullable string using type array."""
|
|
721
|
+
validator = convert_to_voluptuous({"type": ["string", "null"]})
|
|
722
|
+
validator("test")
|
|
723
|
+
validator(None)
|
|
724
|
+
|
|
725
|
+
with pytest.raises(vol.Invalid):
|
|
726
|
+
validator(123)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def test_convert_to_voluptuous_nullable_integer_openapi_3_1():
|
|
730
|
+
"""Test OpenAPI 3.1 nullable integer using type array."""
|
|
731
|
+
validator = convert_to_voluptuous({"type": ["integer", "null"]})
|
|
732
|
+
validator(42)
|
|
733
|
+
validator(None)
|
|
734
|
+
|
|
735
|
+
with pytest.raises(vol.Invalid):
|
|
736
|
+
validator("string")
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def test_convert_to_voluptuous_multiple_types_with_null_openapi_3_1():
|
|
740
|
+
"""Test OpenAPI 3.1 multiple types with null."""
|
|
741
|
+
validator = convert_to_voluptuous({"type": ["string", "integer", "null"]})
|
|
742
|
+
validator("test")
|
|
743
|
+
validator(42)
|
|
744
|
+
validator(None)
|
|
745
|
+
|
|
746
|
+
with pytest.raises(vol.Invalid):
|
|
747
|
+
validator(3.14)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def test_convert_to_voluptuous_multiple_types_without_null_openapi_3_1():
|
|
751
|
+
"""Test OpenAPI 3.1 multiple types without null."""
|
|
752
|
+
validator = convert_to_voluptuous({"type": ["string", "integer"]})
|
|
753
|
+
validator("test")
|
|
754
|
+
validator(42)
|
|
755
|
+
|
|
756
|
+
with pytest.raises(vol.Invalid):
|
|
757
|
+
validator(None)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def test_convert_to_voluptuous_only_null_openapi_3_1():
|
|
761
|
+
"""Test OpenAPI 3.1 type array with only null."""
|
|
762
|
+
validator = convert_to_voluptuous({"type": ["null"]})
|
|
763
|
+
validator(None)
|
|
764
|
+
|
|
765
|
+
with pytest.raises(vol.Invalid):
|
|
766
|
+
validator("test")
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def test_convert_to_voluptuous_nullable_string_with_pattern():
|
|
770
|
+
"""Test nullable string with pattern constraint."""
|
|
771
|
+
validator = convert_to_voluptuous(
|
|
772
|
+
{"type": "string", "pattern": r"^test", "nullable": True}
|
|
773
|
+
)
|
|
774
|
+
validator("testing")
|
|
775
|
+
validator(None)
|
|
776
|
+
|
|
777
|
+
with pytest.raises(vol.Invalid):
|
|
778
|
+
validator("nottest")
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def test_convert_to_voluptuous_nullable_integer_with_range_openapi_3_0():
|
|
782
|
+
"""Test nullable integer with range constraint (OpenAPI 3.0 style)."""
|
|
783
|
+
validator = convert_to_voluptuous(
|
|
784
|
+
{"type": "integer", "minimum": 1, "maximum": 10, "nullable": True}
|
|
785
|
+
)
|
|
786
|
+
validator(5)
|
|
787
|
+
validator(None)
|
|
788
|
+
|
|
789
|
+
with pytest.raises(vol.Invalid):
|
|
790
|
+
validator(15)
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def test_convert_to_voluptuous_nullable_integer_with_range_openapi_3_1():
|
|
794
|
+
"""Test nullable integer with range constraint (OpenAPI 3.1 style)."""
|
|
795
|
+
validator = convert_to_voluptuous(
|
|
796
|
+
{"type": ["integer", "null"], "minimum": 1, "maximum": 10}
|
|
797
|
+
)
|
|
798
|
+
validator(5)
|
|
799
|
+
validator(None)
|
|
800
|
+
|
|
801
|
+
with pytest.raises(vol.Invalid):
|
|
802
|
+
validator(15)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def test_convert_to_voluptuous_nullable_string_with_length_constraints():
|
|
806
|
+
"""Test nullable string with length constraints."""
|
|
807
|
+
validator = convert_to_voluptuous(
|
|
808
|
+
{"type": "string", "minLength": 3, "maxLength": 10, "nullable": True}
|
|
809
|
+
)
|
|
810
|
+
validator("hello")
|
|
811
|
+
validator(None)
|
|
812
|
+
|
|
813
|
+
with pytest.raises(vol.Invalid):
|
|
814
|
+
validator("hi") # too short
|
|
815
|
+
|
|
816
|
+
with pytest.raises(vol.Invalid):
|
|
817
|
+
validator("verylongstring") # too long
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def test_convert_to_voluptuous_nullable_number_with_range():
|
|
821
|
+
"""Test nullable number with range constraints."""
|
|
822
|
+
validator = convert_to_voluptuous(
|
|
823
|
+
{"type": "number", "minimum": 0.0, "maximum": 100.0, "nullable": True}
|
|
824
|
+
)
|
|
825
|
+
validator(50.5)
|
|
826
|
+
validator(None)
|
|
827
|
+
|
|
828
|
+
with pytest.raises(vol.Invalid):
|
|
829
|
+
validator(-1.0) # too low
|
|
830
|
+
|
|
831
|
+
with pytest.raises(vol.Invalid):
|
|
832
|
+
validator(101.0) # too high
|
|
@@ -9,7 +9,6 @@ and are tested by exercising with both valid and invalid data.
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from collections.abc import Callable, Generator
|
|
12
|
-
import datetime
|
|
13
12
|
|
|
14
13
|
import pytest
|
|
15
14
|
import voluptuous as vol
|
|
@@ -17,7 +16,7 @@ import openapi_schema_validator
|
|
|
17
16
|
from typing import Any
|
|
18
17
|
import logging
|
|
19
18
|
|
|
20
|
-
from voluptuous_openapi import convert, convert_to_voluptuous
|
|
19
|
+
from voluptuous_openapi import convert, convert_to_voluptuous, OpenApiVersion
|
|
21
20
|
from jsonschema.exceptions import ValidationError
|
|
22
21
|
|
|
23
22
|
|
|
@@ -36,7 +35,7 @@ def voluptuous_validator(schema: vol.Schema) -> Validator:
|
|
|
36
35
|
|
|
37
36
|
def validator(data: Any) -> Any:
|
|
38
37
|
try:
|
|
39
|
-
_LOGGER.debug("Validating %s with schema %s", data, schema)
|
|
38
|
+
_LOGGER.debug("Validating voluptuous %s with schema %s", data, schema)
|
|
40
39
|
return schema(data)
|
|
41
40
|
except (vol.Invalid, ValueError) as e:
|
|
42
41
|
raise InvalidFormat(str(e))
|
|
@@ -49,7 +48,7 @@ def openapi_validator(schema: dict) -> Any:
|
|
|
49
48
|
|
|
50
49
|
def validator(data: Any) -> Any:
|
|
51
50
|
try:
|
|
52
|
-
_LOGGER.debug("Validating %s with schema %s", data, schema)
|
|
51
|
+
_LOGGER.debug("Validating openai %s with schema %s", data, schema)
|
|
53
52
|
openapi_schema_validator.validate(data, schema)
|
|
54
53
|
return data
|
|
55
54
|
except ValidationError as e:
|
|
@@ -71,8 +70,11 @@ def generate_validators(
|
|
|
71
70
|
yield openapi_validator(openapi_schema)
|
|
72
71
|
yield voluptuous_validator(voluptuous_schema)
|
|
73
72
|
|
|
74
|
-
# Converted schema validations
|
|
75
|
-
|
|
73
|
+
# Converted schema validations. We use OpenAPI version 3.1 because it has equivalent
|
|
74
|
+
# semantics to voluptuous.
|
|
75
|
+
yield openapi_validator(
|
|
76
|
+
convert(voluptuous_schema, openapi_version=OpenApiVersion.V3_1)
|
|
77
|
+
)
|
|
76
78
|
yield voluptuous_validator(convert_to_voluptuous(openapi_schema))
|
|
77
79
|
|
|
78
80
|
|
|
@@ -343,6 +345,26 @@ def test_allow_extra(validator: Validator) -> None:
|
|
|
343
345
|
validator(123)
|
|
344
346
|
|
|
345
347
|
|
|
348
|
+
@pytest.mark.parametrize(
|
|
349
|
+
"validator",
|
|
350
|
+
generate_validators(
|
|
351
|
+
{"type": "null"},
|
|
352
|
+
vol.Schema(None),
|
|
353
|
+
),
|
|
354
|
+
ids=TEST_IDS,
|
|
355
|
+
)
|
|
356
|
+
def test_none(validator: Validator) -> None:
|
|
357
|
+
"""Test null or None values in the schema."""
|
|
358
|
+
|
|
359
|
+
validator(None)
|
|
360
|
+
|
|
361
|
+
with pytest.raises(InvalidFormat):
|
|
362
|
+
validator("abc")
|
|
363
|
+
|
|
364
|
+
with pytest.raises(InvalidFormat):
|
|
365
|
+
validator(1.0)
|
|
366
|
+
|
|
367
|
+
|
|
346
368
|
@pytest.mark.parametrize(
|
|
347
369
|
"validator",
|
|
348
370
|
generate_validators(
|
|
@@ -385,3 +407,172 @@ def test_one_of(validator: Validator) -> None:
|
|
|
385
407
|
|
|
386
408
|
with pytest.raises(InvalidFormat):
|
|
387
409
|
validator(1.4)
|
|
410
|
+
|
|
411
|
+
with pytest.raises(InvalidFormat):
|
|
412
|
+
validator({"key": "value"})
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@pytest.mark.parametrize(
|
|
416
|
+
"validator",
|
|
417
|
+
generate_validators(
|
|
418
|
+
{"anyOf": [{"type": "string"}, {"type": "integer"}]},
|
|
419
|
+
vol.Any(str, int),
|
|
420
|
+
),
|
|
421
|
+
ids=TEST_IDS,
|
|
422
|
+
)
|
|
423
|
+
def test_any_of(validator: Validator) -> None:
|
|
424
|
+
"""Test anyOf multiple types."""
|
|
425
|
+
|
|
426
|
+
validator(1)
|
|
427
|
+
validator(10)
|
|
428
|
+
validator("hello")
|
|
429
|
+
|
|
430
|
+
with pytest.raises(InvalidFormat):
|
|
431
|
+
validator(1.4)
|
|
432
|
+
|
|
433
|
+
with pytest.raises(InvalidFormat):
|
|
434
|
+
validator({"key": "value"})
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@pytest.mark.parametrize(
|
|
438
|
+
"validator",
|
|
439
|
+
generate_validators(
|
|
440
|
+
{"anyOf": [{"type": "string"}, {"type": "null"}]},
|
|
441
|
+
vol.Any(str, None),
|
|
442
|
+
),
|
|
443
|
+
ids=TEST_IDS,
|
|
444
|
+
)
|
|
445
|
+
def test_any_of_with_null(validator: Validator) -> None:
|
|
446
|
+
"""Test anyOf multiple types that includes null."""
|
|
447
|
+
|
|
448
|
+
validator("hello")
|
|
449
|
+
validator("")
|
|
450
|
+
# 'None' is allowed with type: null in openapi
|
|
451
|
+
validator(None)
|
|
452
|
+
|
|
453
|
+
with pytest.raises(InvalidFormat):
|
|
454
|
+
validator(1)
|
|
455
|
+
|
|
456
|
+
with pytest.raises(InvalidFormat):
|
|
457
|
+
validator(1.4)
|
|
458
|
+
|
|
459
|
+
with pytest.raises(InvalidFormat):
|
|
460
|
+
validator({"key": "value"})
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@pytest.mark.parametrize(
|
|
464
|
+
"validator",
|
|
465
|
+
generate_validators(
|
|
466
|
+
{
|
|
467
|
+
"type": "object",
|
|
468
|
+
"properties": {
|
|
469
|
+
"id": {"type": "integer"},
|
|
470
|
+
"name": {"type": "string", "nullable": True},
|
|
471
|
+
},
|
|
472
|
+
"required": ["id"],
|
|
473
|
+
},
|
|
474
|
+
vol.Schema({vol.Required("id"): int, vol.Optional("name"): str}),
|
|
475
|
+
),
|
|
476
|
+
ids=TEST_IDS,
|
|
477
|
+
)
|
|
478
|
+
def test_object_with_nullable(validator: Validator) -> None:
|
|
479
|
+
"""Test an object with a nullable field."""
|
|
480
|
+
|
|
481
|
+
validator({"id": 1, "name": "hello"})
|
|
482
|
+
|
|
483
|
+
# Note: The openapi-schema-validator library doesn't properly support
|
|
484
|
+
# OpenAPI 3.0's nullable property, so None values will fail for the
|
|
485
|
+
# native OpenAPI validator but work for converted validators
|
|
486
|
+
try:
|
|
487
|
+
validator({"id": 1, "name": None})
|
|
488
|
+
# If this succeeds, it means the validator properly handles nullable
|
|
489
|
+
except InvalidFormat:
|
|
490
|
+
# This is expected for the native OpenAPI validator due to library limitations
|
|
491
|
+
pass
|
|
492
|
+
|
|
493
|
+
with pytest.raises(InvalidFormat):
|
|
494
|
+
validator(1)
|
|
495
|
+
|
|
496
|
+
with pytest.raises(InvalidFormat):
|
|
497
|
+
validator({"name": "hello"})
|
|
498
|
+
|
|
499
|
+
with pytest.raises(InvalidFormat):
|
|
500
|
+
validator({"id": 1, "name": 1})
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def test_convert_to_voluptuous_nullable_field():
|
|
504
|
+
"""Test that convert_to_voluptuous properly handles nullable fields."""
|
|
505
|
+
# Test OpenAPI 3.0 nullable syntax
|
|
506
|
+
openapi_schema = {
|
|
507
|
+
"type": "object",
|
|
508
|
+
"properties": {
|
|
509
|
+
"id": {"type": "integer"},
|
|
510
|
+
"name": {"type": "string", "nullable": True},
|
|
511
|
+
},
|
|
512
|
+
"required": ["id"],
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
validator = voluptuous_validator(convert_to_voluptuous(openapi_schema))
|
|
516
|
+
|
|
517
|
+
# Test valid cases
|
|
518
|
+
validator({"id": 1, "name": "hello"})
|
|
519
|
+
validator({"id": 1, "name": None}) # This should work with our fix
|
|
520
|
+
validator({"id": 1}) # Optional field can be omitted
|
|
521
|
+
|
|
522
|
+
# Test invalid cases
|
|
523
|
+
with pytest.raises(InvalidFormat):
|
|
524
|
+
validator({"name": "hello"}) # Missing required id
|
|
525
|
+
|
|
526
|
+
with pytest.raises(InvalidFormat):
|
|
527
|
+
validator({"id": 1, "name": 1}) # Wrong type for name
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def test_convert_to_voluptuous_nullable_field_openapi_3_1():
|
|
531
|
+
"""Test that convert_to_voluptuous properly handles OpenAPI 3.1 nullable syntax."""
|
|
532
|
+
# Test OpenAPI 3.1 type array syntax
|
|
533
|
+
openapi_schema = {
|
|
534
|
+
"type": "object",
|
|
535
|
+
"properties": {
|
|
536
|
+
"id": {"type": "integer"},
|
|
537
|
+
"name": {"type": ["string", "null"]},
|
|
538
|
+
},
|
|
539
|
+
"required": ["id"],
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
validator = voluptuous_validator(convert_to_voluptuous(openapi_schema))
|
|
543
|
+
|
|
544
|
+
# Test valid cases
|
|
545
|
+
validator({"id": 1, "name": "hello"})
|
|
546
|
+
validator({"id": 1, "name": None}) # This should work with our fix
|
|
547
|
+
validator({"id": 1}) # Optional field can be omitted
|
|
548
|
+
|
|
549
|
+
# Test invalid cases
|
|
550
|
+
with pytest.raises(InvalidFormat):
|
|
551
|
+
validator({"name": "hello"}) # Missing required id
|
|
552
|
+
|
|
553
|
+
with pytest.raises(InvalidFormat):
|
|
554
|
+
validator({"id": 1, "name": 1}) # Wrong type for name
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@pytest.mark.parametrize(
|
|
558
|
+
"validator",
|
|
559
|
+
generate_validators(
|
|
560
|
+
{"anyOf": [{"type": "string"}, {"type": "null"}]},
|
|
561
|
+
vol.Maybe(str),
|
|
562
|
+
),
|
|
563
|
+
ids=TEST_IDS,
|
|
564
|
+
)
|
|
565
|
+
def test_maybe(validator: Validator) -> None:
|
|
566
|
+
"""Test voluptuous Maybe type that allows None."""
|
|
567
|
+
|
|
568
|
+
validator("hello")
|
|
569
|
+
validator(None)
|
|
570
|
+
|
|
571
|
+
with pytest.raises(InvalidFormat):
|
|
572
|
+
validator(1)
|
|
573
|
+
|
|
574
|
+
with pytest.raises(InvalidFormat):
|
|
575
|
+
validator(1.4)
|
|
576
|
+
|
|
577
|
+
with pytest.raises(InvalidFormat):
|
|
578
|
+
validator({"key": "value"})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable, Mapping, Sequence
|
|
4
4
|
from inspect import signature
|
|
5
|
-
from enum import Enum
|
|
5
|
+
from enum import Enum, StrEnum
|
|
6
6
|
import re
|
|
7
7
|
from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints
|
|
8
8
|
from types import NoneType, UnionType
|
|
@@ -22,7 +22,6 @@ UNSUPPORTED = object()
|
|
|
22
22
|
|
|
23
23
|
# These are not supported when converting from OpenAPI to voluptuous
|
|
24
24
|
OPENAPI_UNSUPPORTED_KEYWORDS = {
|
|
25
|
-
"anyOf",
|
|
26
25
|
"allOf",
|
|
27
26
|
"multipleOf",
|
|
28
27
|
"minItems",
|
|
@@ -31,10 +30,31 @@ OPENAPI_UNSUPPORTED_KEYWORDS = {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
class OpenApiVersion(StrEnum):
|
|
34
|
+
"""The OpenAPI version.
|
|
35
|
+
|
|
36
|
+
This is used to change the behavior when converting schemas to OpenAPI.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
V3 = "3.0"
|
|
40
|
+
V3_1 = "3.1.0"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def convert(
|
|
44
|
+
schema: Any,
|
|
45
|
+
*,
|
|
46
|
+
custom_serializer: Callable | None = None,
|
|
47
|
+
openapi_version: OpenApiVersion = OpenApiVersion.V3,
|
|
48
|
+
) -> dict:
|
|
35
49
|
"""Convert a voluptuous schema to a OpenAPI Schema object."""
|
|
36
50
|
# pylint: disable=too-many-return-statements,too-many-branches
|
|
37
51
|
|
|
52
|
+
def convert_with_args(schema: Any) -> dict:
|
|
53
|
+
"""Convert schema for recusing and propagating arguments."""
|
|
54
|
+
return convert(
|
|
55
|
+
schema, custom_serializer=custom_serializer, openapi_version=openapi_version
|
|
56
|
+
)
|
|
57
|
+
|
|
38
58
|
def ensure_default(value: dict[str:Any]):
|
|
39
59
|
"""Make sure that type is set."""
|
|
40
60
|
if all(x not in value for x in ("type", "anyOf", "oneOf", "allOf", "not")):
|
|
@@ -77,7 +97,7 @@ def convert(schema: Any, *, custom_serializer: Callable | None = None) -> dict:
|
|
|
77
97
|
else:
|
|
78
98
|
pkey = key
|
|
79
99
|
|
|
80
|
-
pval =
|
|
100
|
+
pval = convert_with_args(value)
|
|
81
101
|
if description:
|
|
82
102
|
pval["description"] = key.description
|
|
83
103
|
|
|
@@ -122,7 +142,7 @@ def convert(schema: Any, *, custom_serializer: Callable | None = None) -> dict:
|
|
|
122
142
|
fallback = False
|
|
123
143
|
allOf = []
|
|
124
144
|
for validator in schema.validators:
|
|
125
|
-
v =
|
|
145
|
+
v = convert_with_args(validator)
|
|
126
146
|
if (
|
|
127
147
|
not v
|
|
128
148
|
or v in allOf
|
|
@@ -208,17 +228,19 @@ def convert(schema: Any, *, custom_serializer: Callable | None = None) -> dict:
|
|
|
208
228
|
|
|
209
229
|
if isinstance(schema, vol.Any):
|
|
210
230
|
schema = schema.validators
|
|
211
|
-
|
|
231
|
+
# Infer the enum type based on the type of the first value, but default
|
|
232
|
+
# to a string as a fallback.
|
|
233
|
+
if (
|
|
234
|
+
None in schema or NoneType in schema
|
|
235
|
+
) and openapi_version == OpenApiVersion.V3:
|
|
212
236
|
schema = [val for val in schema if val is not None and val is not NoneType]
|
|
213
237
|
nullable = True
|
|
214
238
|
else:
|
|
215
239
|
nullable = False
|
|
216
240
|
if len(schema) == 1:
|
|
217
|
-
result =
|
|
241
|
+
result = convert_with_args(schema[0])
|
|
218
242
|
else:
|
|
219
|
-
anyOf = [
|
|
220
|
-
convert(val, custom_serializer=custom_serializer) for val in schema
|
|
221
|
-
]
|
|
243
|
+
anyOf = [convert_with_args(val) for val in schema]
|
|
222
244
|
|
|
223
245
|
# Merge nested anyOf
|
|
224
246
|
tmpAnyOf = []
|
|
@@ -302,6 +324,8 @@ def convert(schema: Any, *, custom_serializer: Callable | None = None) -> dict:
|
|
|
302
324
|
return {"type": TYPES_MAP[type(schema)], "enum": [schema]}
|
|
303
325
|
|
|
304
326
|
if schema is None:
|
|
327
|
+
if openapi_version == OpenApiVersion.V3_1:
|
|
328
|
+
return {"type": "null"}
|
|
305
329
|
return {"type": "object", "nullable": True, "description": "Must be null"}
|
|
306
330
|
|
|
307
331
|
if (
|
|
@@ -315,16 +339,11 @@ def convert(schema: Any, *, custom_serializer: Callable | None = None) -> dict:
|
|
|
315
339
|
if len(schema) == 1:
|
|
316
340
|
return {
|
|
317
341
|
"type": "array",
|
|
318
|
-
"items": ensure_default(
|
|
319
|
-
convert(schema[0], custom_serializer=custom_serializer)
|
|
320
|
-
),
|
|
342
|
+
"items": ensure_default(convert_with_args(schema[0])),
|
|
321
343
|
}
|
|
322
344
|
return {
|
|
323
345
|
"type": "array",
|
|
324
|
-
"items": [
|
|
325
|
-
ensure_default(convert(s, custom_serializer=custom_serializer))
|
|
326
|
-
for s in schema.items()
|
|
327
|
-
],
|
|
346
|
+
"items": [ensure_default(convert_with_args(s)) for s in schema.items()],
|
|
328
347
|
}
|
|
329
348
|
|
|
330
349
|
if schema in TYPES_MAP:
|
|
@@ -334,7 +353,7 @@ def convert(schema: Any, *, custom_serializer: Callable | None = None) -> dict:
|
|
|
334
353
|
if get_args(schema)[1] is Any or isinstance(get_args(schema)[1], TypeVar):
|
|
335
354
|
schema = dict
|
|
336
355
|
else:
|
|
337
|
-
return
|
|
356
|
+
return convert_with_args({get_args(schema)[0]: get_args(schema)[1]})
|
|
338
357
|
|
|
339
358
|
if isinstance(schema, type):
|
|
340
359
|
if schema is dict:
|
|
@@ -360,6 +379,8 @@ def convert(schema: Any, *, custom_serializer: Callable | None = None) -> dict:
|
|
|
360
379
|
return {"type": enum_type, "enum": enum_values, "nullable": True}
|
|
361
380
|
return {"type": enum_type, "enum": enum_values}
|
|
362
381
|
elif schema is NoneType:
|
|
382
|
+
if openapi_version == OpenApiVersion.V3_1:
|
|
383
|
+
return {"type": "null"}
|
|
363
384
|
return {"type": "object", "nullable": True, "description": "Must be null"}
|
|
364
385
|
|
|
365
386
|
if schema is object:
|
|
@@ -380,7 +401,7 @@ def convert(schema: Any, *, custom_serializer: Callable | None = None) -> dict:
|
|
|
380
401
|
else:
|
|
381
402
|
return {}
|
|
382
403
|
|
|
383
|
-
return ensure_default(
|
|
404
|
+
return ensure_default(convert_with_args(schema))
|
|
384
405
|
|
|
385
406
|
raise ValueError("Unable to convert schema: {}".format(schema))
|
|
386
407
|
|
|
@@ -398,33 +419,65 @@ def convert_to_voluptuous(schema: dict) -> vol.Schema:
|
|
|
398
419
|
if (one_of := schema.get("oneOf")) is not None:
|
|
399
420
|
if not isinstance(one_of, list):
|
|
400
421
|
raise ValueError("Invalid schema, oneOf should be a list")
|
|
422
|
+
# This implements anyOf semantics sice it matches any of the subschemas,
|
|
423
|
+
# not just one of them.
|
|
401
424
|
return vol.Any(*[convert_to_voluptuous(sub_schema) for sub_schema in one_of])
|
|
402
425
|
|
|
426
|
+
if (any_of := schema.get("anyOf")) is not None:
|
|
427
|
+
if not isinstance(any_of, list):
|
|
428
|
+
raise ValueError("Invalid schema, anyOf should be a list")
|
|
429
|
+
return vol.Any(*[convert_to_voluptuous(sub_schema) for sub_schema in any_of])
|
|
430
|
+
|
|
403
431
|
if (schema_type := schema.get("type")) is None:
|
|
404
432
|
raise ValueError("Invalid schema, missing type")
|
|
405
433
|
|
|
406
|
-
if
|
|
407
|
-
|
|
408
|
-
if (pattern := schema.get("pattern")) is not None:
|
|
409
|
-
return vol.Match(re.compile(pattern))
|
|
434
|
+
if schema_type == "null":
|
|
435
|
+
return vol.Schema(None)
|
|
410
436
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
437
|
+
# Handle OpenAPI 3.1 style: type can be an array like ["string", "null"]
|
|
438
|
+
if isinstance(schema_type, list):
|
|
439
|
+
if len(schema_type) == 0:
|
|
440
|
+
raise ValueError("Invalid schema, type array cannot be empty")
|
|
414
441
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if
|
|
418
|
-
|
|
442
|
+
validators = []
|
|
443
|
+
for t in schema_type:
|
|
444
|
+
if t == "null":
|
|
445
|
+
validators.append(None)
|
|
446
|
+
else:
|
|
447
|
+
base_schema = schema.copy()
|
|
448
|
+
base_schema["type"] = t
|
|
449
|
+
validators.append(convert_to_voluptuous(base_schema))
|
|
450
|
+
return vol.Any(*validators)
|
|
419
451
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
max = schema.get("maximum")
|
|
423
|
-
if min is not None or max is not None:
|
|
424
|
-
return vol.All(basic_type, vol.Range(min=min, max=max))
|
|
425
|
-
return basic_type
|
|
452
|
+
if (basic_type := TYPES_MAP_REV.get(schema_type)) is not None:
|
|
453
|
+
validator = basic_type
|
|
426
454
|
|
|
427
|
-
|
|
455
|
+
if schema_type == "string":
|
|
456
|
+
if (pattern := schema.get("pattern")) is not None:
|
|
457
|
+
validator = vol.Match(re.compile(pattern))
|
|
458
|
+
elif (format := schema.get("format")) is not None and format == "date-time":
|
|
459
|
+
validator = vol.Datetime()
|
|
460
|
+
else:
|
|
461
|
+
min_length = schema.get("minLength")
|
|
462
|
+
max_length = schema.get("maxLength")
|
|
463
|
+
if min_length is not None or max_length is not None:
|
|
464
|
+
validator = vol.All(
|
|
465
|
+
basic_type, vol.Length(min=min_length, max=max_length)
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
elif schema_type in ("integer", "number"):
|
|
469
|
+
min_val = schema.get("minimum")
|
|
470
|
+
max_val = schema.get("maximum")
|
|
471
|
+
if min_val is not None or max_val is not None:
|
|
472
|
+
validator = vol.All(basic_type, vol.Range(min=min_val, max=max_val))
|
|
473
|
+
|
|
474
|
+
# Handle OpenAPI 3.0 nullable property
|
|
475
|
+
if schema.get("nullable") is True:
|
|
476
|
+
return vol.Any(validator, None)
|
|
477
|
+
|
|
478
|
+
return validator
|
|
479
|
+
|
|
480
|
+
if schema_type == "object":
|
|
428
481
|
properties = {}
|
|
429
482
|
required_properties = set(schema.get("required", []))
|
|
430
483
|
for key, value in schema.get("properties", {}).items():
|
|
@@ -437,11 +490,26 @@ def convert_to_voluptuous(schema: dict) -> vol.Schema:
|
|
|
437
490
|
else:
|
|
438
491
|
key_type = vol.Optional
|
|
439
492
|
properties[key_type(key, description=description)] = value_type
|
|
493
|
+
|
|
494
|
+
validator = None
|
|
440
495
|
if schema.get("additionalProperties") is True:
|
|
441
|
-
|
|
442
|
-
|
|
496
|
+
validator = vol.Schema(properties, extra=vol.ALLOW_EXTRA)
|
|
497
|
+
else:
|
|
498
|
+
validator = vol.Schema(properties)
|
|
499
|
+
|
|
500
|
+
# Handle OpenAPI 3.0 nullable property
|
|
501
|
+
if schema.get("nullable") is True:
|
|
502
|
+
return vol.Any(validator, None)
|
|
503
|
+
|
|
504
|
+
return validator
|
|
505
|
+
|
|
506
|
+
if schema_type == "array":
|
|
507
|
+
validator = vol.Schema([convert_to_voluptuous(schema["items"])])
|
|
508
|
+
|
|
509
|
+
# Handle OpenAPI 3.0 nullable property
|
|
510
|
+
if schema.get("nullable") is True:
|
|
511
|
+
return vol.Any(validator, None)
|
|
443
512
|
|
|
444
|
-
|
|
445
|
-
return vol.Schema([convert_to_voluptuous(schema["items"])])
|
|
513
|
+
return validator
|
|
446
514
|
|
|
447
515
|
raise ValueError("Unable to convert schema: {}".format(schema))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/voluptuous_openapi.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/voluptuous_openapi.egg-info/requires.txt
RENAMED
|
File without changes
|
{voluptuous_openapi-0.0.7 → voluptuous_openapi-0.2.0}/voluptuous_openapi.egg-info/top_level.txt
RENAMED
|
File without changes
|