voluptuous-openapi 0.2.0__tar.gz → 0.4.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.2.0/voluptuous_openapi.egg-info → voluptuous_openapi-0.4.0}/PKG-INFO +2 -2
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/README.md +1 -1
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/pyproject.toml +1 -1
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/tests/test_lib.py +485 -0
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/tests/test_validation.py +67 -0
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/voluptuous_openapi/__init__.py +202 -23
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0/voluptuous_openapi.egg-info}/PKG-INFO +2 -2
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/LICENSE +0 -0
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/setup.cfg +0 -0
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/voluptuous_openapi.egg-info/SOURCES.txt +0 -0
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/voluptuous_openapi.egg-info/dependency_links.txt +0 -0
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/voluptuous_openapi.egg-info/requires.txt +0 -0
- {voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/voluptuous_openapi.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voluptuous-openapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Convert voluptuous schemas to OpenAPI Schema object
|
|
5
5
|
Author-email: Denis Shulyaka <Shulyaka@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -55,7 +55,7 @@ You can pass a custom serializer to be able to process custom validators. If the
|
|
|
55
55
|
|
|
56
56
|
```python
|
|
57
57
|
|
|
58
|
-
from
|
|
58
|
+
from voluptuous_openapi import UNSUPPORTED, convert
|
|
59
59
|
|
|
60
60
|
def custom_convert(value):
|
|
61
61
|
if value is my_custom_validator:
|
|
@@ -42,7 +42,7 @@ You can pass a custom serializer to be able to process custom validators. If the
|
|
|
42
42
|
|
|
43
43
|
```python
|
|
44
44
|
|
|
45
|
-
from
|
|
45
|
+
from voluptuous_openapi import UNSUPPORTED, convert
|
|
46
46
|
|
|
47
47
|
def custom_convert(value):
|
|
48
48
|
if value is my_custom_validator:
|
|
@@ -410,6 +410,11 @@ def test_key_any():
|
|
|
410
410
|
},
|
|
411
411
|
"required": [],
|
|
412
412
|
"type": "object",
|
|
413
|
+
"anyOf": [
|
|
414
|
+
{"required": ["hours"]},
|
|
415
|
+
{"required": ["minutes"]},
|
|
416
|
+
{"required": ["seconds"]},
|
|
417
|
+
],
|
|
413
418
|
} == convert(
|
|
414
419
|
{
|
|
415
420
|
vol.Required(vol.Any("hours", "minutes", "seconds")): int,
|
|
@@ -830,3 +835,483 @@ def test_convert_to_voluptuous_nullable_number_with_range():
|
|
|
830
835
|
|
|
831
836
|
with pytest.raises(vol.Invalid):
|
|
832
837
|
validator(101.0) # too high
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
TEST_TASK_ITEM = {"content": "a task ", "description": "a description"}
|
|
841
|
+
TASK_SCHEMA = {
|
|
842
|
+
"type": "object",
|
|
843
|
+
"properties": {
|
|
844
|
+
"tasks": {
|
|
845
|
+
"type": "array",
|
|
846
|
+
"items": {
|
|
847
|
+
"type": "object",
|
|
848
|
+
"properties": {
|
|
849
|
+
"content": {
|
|
850
|
+
"type": "string",
|
|
851
|
+
"description": "The content of the task to create.",
|
|
852
|
+
},
|
|
853
|
+
"description": {
|
|
854
|
+
"type": "string",
|
|
855
|
+
"description": "The description of the task.",
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
"required": ["content"],
|
|
859
|
+
"additionalProperties": False,
|
|
860
|
+
},
|
|
861
|
+
}
|
|
862
|
+
},
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
@pytest.mark.parametrize(
|
|
867
|
+
"extra_tasks_data, input_data",
|
|
868
|
+
[
|
|
869
|
+
({"minItems": 1, "maxItems": 2}, {"tasks": [TEST_TASK_ITEM]}),
|
|
870
|
+
({"minItems": 1, "maxItems": 2}, {"tasks": [TEST_TASK_ITEM, TEST_TASK_ITEM]}),
|
|
871
|
+
({"minItems": 1}, {"tasks": [TEST_TASK_ITEM]}),
|
|
872
|
+
({"maxItems": 2}, {"tasks": [TEST_TASK_ITEM, TEST_TASK_ITEM]}),
|
|
873
|
+
],
|
|
874
|
+
)
|
|
875
|
+
def test_with_min_max_items_success(extra_tasks_data, input_data):
|
|
876
|
+
task_schema = TASK_SCHEMA.copy()
|
|
877
|
+
task_schema["properties"]["tasks"].update(extra_tasks_data)
|
|
878
|
+
|
|
879
|
+
validator = convert_to_voluptuous(task_schema)
|
|
880
|
+
|
|
881
|
+
validator(input_data)
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
@pytest.mark.parametrize(
|
|
885
|
+
"extra_tasks_data, input_data",
|
|
886
|
+
[
|
|
887
|
+
({"minItems": 1, "maxItems": 2}, {"tasks": []}),
|
|
888
|
+
(
|
|
889
|
+
{"minItems": 1, "maxItems": 2},
|
|
890
|
+
{"tasks": [TEST_TASK_ITEM, TEST_TASK_ITEM, TEST_TASK_ITEM]},
|
|
891
|
+
),
|
|
892
|
+
({"minItems": 1}, {"tasks": []}),
|
|
893
|
+
({"maxItems": 2}, {"tasks": [TEST_TASK_ITEM, TEST_TASK_ITEM, TEST_TASK_ITEM]}),
|
|
894
|
+
],
|
|
895
|
+
)
|
|
896
|
+
def test_with_min_max_items_fails_validation(extra_tasks_data, input_data):
|
|
897
|
+
task_schema = TASK_SCHEMA.copy()
|
|
898
|
+
task_schema["properties"]["tasks"].update(extra_tasks_data)
|
|
899
|
+
|
|
900
|
+
validator = convert_to_voluptuous(task_schema)
|
|
901
|
+
|
|
902
|
+
with pytest.raises(vol.Invalid):
|
|
903
|
+
validator(input_data)
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
def test_required_any_of():
|
|
907
|
+
"""Test schemas with Required(Any(...)) constraints."""
|
|
908
|
+
assert {
|
|
909
|
+
"properties": {
|
|
910
|
+
"color": {"type": "string"},
|
|
911
|
+
"temperature": {"type": "integer"},
|
|
912
|
+
"brightness": {"type": "integer"},
|
|
913
|
+
},
|
|
914
|
+
"required": [],
|
|
915
|
+
"type": "object",
|
|
916
|
+
"anyOf": [
|
|
917
|
+
{"required": ["color"]},
|
|
918
|
+
{"required": ["temperature"]},
|
|
919
|
+
{"required": ["brightness"]},
|
|
920
|
+
],
|
|
921
|
+
} == convert(
|
|
922
|
+
{
|
|
923
|
+
vol.Required(vol.Any("color", "temperature", "brightness")): object,
|
|
924
|
+
vol.Optional("color"): str,
|
|
925
|
+
vol.Optional("temperature"): int,
|
|
926
|
+
vol.Optional("brightness"): int,
|
|
927
|
+
}
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
assert {
|
|
931
|
+
"properties": {
|
|
932
|
+
"color": {"type": "string"},
|
|
933
|
+
"temperature": {"type": "integer"},
|
|
934
|
+
"brightness": {"type": "integer"},
|
|
935
|
+
"mode": {"type": "string"},
|
|
936
|
+
"preset": {"type": "string"},
|
|
937
|
+
},
|
|
938
|
+
"required": [],
|
|
939
|
+
"type": "object",
|
|
940
|
+
"anyOf": [
|
|
941
|
+
{"required": ["color", "mode"]},
|
|
942
|
+
{"required": ["color", "preset"]},
|
|
943
|
+
{"required": ["temperature", "mode"]},
|
|
944
|
+
{"required": ["temperature", "preset"]},
|
|
945
|
+
{"required": ["brightness", "mode"]},
|
|
946
|
+
{"required": ["brightness", "preset"]},
|
|
947
|
+
],
|
|
948
|
+
} == convert(
|
|
949
|
+
{
|
|
950
|
+
vol.Required(vol.Any("color", "temperature", "brightness")): object,
|
|
951
|
+
vol.Required(vol.Any("mode", "preset")): str,
|
|
952
|
+
vol.Optional("color"): str,
|
|
953
|
+
vol.Optional("temperature"): int,
|
|
954
|
+
vol.Optional("brightness"): int,
|
|
955
|
+
vol.Optional("mode"): str,
|
|
956
|
+
vol.Optional("preset"): str,
|
|
957
|
+
}
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
assert {
|
|
961
|
+
"properties": {
|
|
962
|
+
"entity_id": {"type": "string"},
|
|
963
|
+
"color": {"type": "string"},
|
|
964
|
+
"temperature": {"type": "integer"},
|
|
965
|
+
},
|
|
966
|
+
"required": ["entity_id"],
|
|
967
|
+
"type": "object",
|
|
968
|
+
"anyOf": [{"required": ["color"]}, {"required": ["temperature"]}],
|
|
969
|
+
} == convert(
|
|
970
|
+
{
|
|
971
|
+
vol.Required("entity_id"): str,
|
|
972
|
+
vol.Required(vol.Any("color", "temperature")): str,
|
|
973
|
+
vol.Optional("color"): str,
|
|
974
|
+
vol.Optional("temperature"): int,
|
|
975
|
+
}
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def test_required_any_of_description():
|
|
980
|
+
"""Test that the description is preserved in a Required(Any(...)) constraint."""
|
|
981
|
+
assert {
|
|
982
|
+
"properties": {
|
|
983
|
+
"color": {"type": "string", "description": "Light appearance"},
|
|
984
|
+
"temperature": {"type": "string", "description": "Light appearance"},
|
|
985
|
+
},
|
|
986
|
+
"required": [],
|
|
987
|
+
"type": "object",
|
|
988
|
+
"anyOf": [{"required": ["color"]}, {"required": ["temperature"]}],
|
|
989
|
+
} == convert(
|
|
990
|
+
{
|
|
991
|
+
vol.Required(
|
|
992
|
+
vol.Any("color", "temperature"), description="Light appearance"
|
|
993
|
+
): str,
|
|
994
|
+
}
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def test_required_any_of_inner_description():
|
|
999
|
+
"""Test that inner descriptions are preferred in a Required(Any(...)) constraint."""
|
|
1000
|
+
assert {
|
|
1001
|
+
"properties": {
|
|
1002
|
+
"color": {"type": "string", "description": "Inner color description"},
|
|
1003
|
+
"temperature": {"type": "string", "description": "Outer description"},
|
|
1004
|
+
},
|
|
1005
|
+
"required": [],
|
|
1006
|
+
"type": "object",
|
|
1007
|
+
"anyOf": [{"required": ["color"]}, {"required": ["temperature"]}],
|
|
1008
|
+
} == convert(
|
|
1009
|
+
{
|
|
1010
|
+
vol.Required(
|
|
1011
|
+
vol.Any(
|
|
1012
|
+
vol.Optional("color", description="Inner color description"),
|
|
1013
|
+
"temperature",
|
|
1014
|
+
),
|
|
1015
|
+
description="Outer description",
|
|
1016
|
+
): str,
|
|
1017
|
+
}
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def test_simple_ref_defs() -> None:
|
|
1022
|
+
"""Test basic JSON pointer reference resolution using the $defs keyword."""
|
|
1023
|
+
schema = {
|
|
1024
|
+
"$defs": {"MyInt": {"type": "integer"}},
|
|
1025
|
+
"type": "object",
|
|
1026
|
+
"properties": {"age": {"$ref": "#/$defs/MyInt"}},
|
|
1027
|
+
}
|
|
1028
|
+
validator = convert_to_voluptuous(schema)
|
|
1029
|
+
# Check it validates valid input
|
|
1030
|
+
res = validator({"age": 42})
|
|
1031
|
+
assert res == {"age": 42}
|
|
1032
|
+
|
|
1033
|
+
# Check it raises vol.Invalid on invalid input
|
|
1034
|
+
with pytest.raises(vol.Invalid):
|
|
1035
|
+
validator({"age": "hello"})
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def test_definitions_compatibility() -> None:
|
|
1039
|
+
"""Test compatibility with older JSON Schema drafts using definitions instead of $defs."""
|
|
1040
|
+
schema = {
|
|
1041
|
+
"definitions": {"MyInt": {"type": "integer"}},
|
|
1042
|
+
"type": "object",
|
|
1043
|
+
"properties": {"age": {"$ref": "#/definitions/MyInt"}},
|
|
1044
|
+
}
|
|
1045
|
+
validator = convert_to_voluptuous(schema)
|
|
1046
|
+
res = validator({"age": 42})
|
|
1047
|
+
assert res == {"age": 42}
|
|
1048
|
+
|
|
1049
|
+
with pytest.raises(vol.Invalid):
|
|
1050
|
+
validator({"age": "not-an-int"})
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def test_chained_refs() -> None:
|
|
1054
|
+
"""Test chained references where one reference points to another reference."""
|
|
1055
|
+
schema = {
|
|
1056
|
+
"$defs": {"Level2": {"type": "integer"}, "Level1": {"$ref": "#/$defs/Level2"}},
|
|
1057
|
+
"type": "object",
|
|
1058
|
+
"properties": {"num": {"$ref": "#/$defs/Level1"}},
|
|
1059
|
+
}
|
|
1060
|
+
validator = convert_to_voluptuous(schema)
|
|
1061
|
+
res = validator({"num": 42})
|
|
1062
|
+
assert res == {"num": 42}
|
|
1063
|
+
|
|
1064
|
+
with pytest.raises(vol.Invalid):
|
|
1065
|
+
validator({"num": "yes"})
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def test_root_self_ref_valid() -> None:
|
|
1069
|
+
"""Test root self-reference (#) with valid recursively nested structures."""
|
|
1070
|
+
schema = {"type": "object", "properties": {"child": {"$ref": "#"}}}
|
|
1071
|
+
validator = convert_to_voluptuous(schema)
|
|
1072
|
+
|
|
1073
|
+
# Single level
|
|
1074
|
+
assert validator({"child": {}}) == {"child": {}}
|
|
1075
|
+
|
|
1076
|
+
# Nested level
|
|
1077
|
+
assert validator({"child": {"child": {"child": {}}}}) == {
|
|
1078
|
+
"child": {"child": {"child": {}}}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def test_root_self_ref_invalid() -> None:
|
|
1083
|
+
"""Test root self-reference (#) with invalid type values."""
|
|
1084
|
+
schema = {"type": "object", "properties": {"child": {"$ref": "#"}}}
|
|
1085
|
+
validator = convert_to_voluptuous(schema)
|
|
1086
|
+
|
|
1087
|
+
with pytest.raises(vol.Invalid):
|
|
1088
|
+
validator({"child": "not-an-object"})
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
def test_recursive_tree_valid() -> None:
|
|
1092
|
+
"""Test recursive tree node reference validation with valid structures."""
|
|
1093
|
+
schema = {
|
|
1094
|
+
"$defs": {
|
|
1095
|
+
"Node": {
|
|
1096
|
+
"type": "object",
|
|
1097
|
+
"properties": {
|
|
1098
|
+
"value": {"type": "string"},
|
|
1099
|
+
"children": {"type": "array", "items": {"$ref": "#/$defs/Node"}},
|
|
1100
|
+
},
|
|
1101
|
+
"required": ["value"],
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
"$ref": "#/$defs/Node",
|
|
1105
|
+
}
|
|
1106
|
+
validator = convert_to_voluptuous(schema)
|
|
1107
|
+
|
|
1108
|
+
# Leaf node
|
|
1109
|
+
assert validator({"value": "leaf"}) == {"value": "leaf"}
|
|
1110
|
+
|
|
1111
|
+
# Deep recursive tree
|
|
1112
|
+
tree = {
|
|
1113
|
+
"value": "root",
|
|
1114
|
+
"children": [
|
|
1115
|
+
{"value": "child1", "children": [{"value": "grandchild1"}]},
|
|
1116
|
+
{"value": "child2"},
|
|
1117
|
+
],
|
|
1118
|
+
}
|
|
1119
|
+
assert validator(tree) == tree
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def test_recursive_tree_invalid() -> None:
|
|
1123
|
+
"""Test recursive tree node reference validation with invalid type values."""
|
|
1124
|
+
schema = {
|
|
1125
|
+
"$defs": {
|
|
1126
|
+
"Node": {
|
|
1127
|
+
"type": "object",
|
|
1128
|
+
"properties": {
|
|
1129
|
+
"value": {"type": "string"},
|
|
1130
|
+
"children": {"type": "array", "items": {"$ref": "#/$defs/Node"}},
|
|
1131
|
+
},
|
|
1132
|
+
"required": ["value"],
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
"$ref": "#/$defs/Node",
|
|
1136
|
+
}
|
|
1137
|
+
validator = convert_to_voluptuous(schema)
|
|
1138
|
+
|
|
1139
|
+
invalid_tree = {"value": "root", "children": [{"value": 123}]} # Should be string
|
|
1140
|
+
with pytest.raises(vol.Invalid):
|
|
1141
|
+
validator(invalid_tree)
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
def test_anonymized_field_definition_valid() -> None:
|
|
1145
|
+
"""Test anonymized FieldDefinition schema with valid primitive and complex structures."""
|
|
1146
|
+
schema = {
|
|
1147
|
+
"$defs": {
|
|
1148
|
+
"FieldDefinition": {
|
|
1149
|
+
"type": "object",
|
|
1150
|
+
"properties": {
|
|
1151
|
+
"type": {
|
|
1152
|
+
"type": "string",
|
|
1153
|
+
"enum": ["BOOLEAN", "STRING", "INTEGER", "STRUCT", "LIST"],
|
|
1154
|
+
},
|
|
1155
|
+
"structFields": {
|
|
1156
|
+
"type": "object",
|
|
1157
|
+
"additionalProperties": {"$ref": "#/$defs/FieldDefinition"},
|
|
1158
|
+
},
|
|
1159
|
+
"listElement": {"$ref": "#/$defs/FieldDefinition"},
|
|
1160
|
+
},
|
|
1161
|
+
"required": ["type"],
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
"$ref": "#/$defs/FieldDefinition",
|
|
1165
|
+
}
|
|
1166
|
+
validator = convert_to_voluptuous(schema)
|
|
1167
|
+
|
|
1168
|
+
# Simple integer type definition
|
|
1169
|
+
assert validator({"type": "INTEGER"}) == {"type": "INTEGER"}
|
|
1170
|
+
|
|
1171
|
+
# Complex struct containing other field definitions (including a list element)
|
|
1172
|
+
complex_def = {
|
|
1173
|
+
"type": "STRUCT",
|
|
1174
|
+
"structFields": {
|
|
1175
|
+
"name": {"type": "STRING"},
|
|
1176
|
+
"tags": {"type": "LIST", "listElement": {"type": "STRING"}},
|
|
1177
|
+
},
|
|
1178
|
+
}
|
|
1179
|
+
assert validator(complex_def) == complex_def
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def test_anonymized_field_definition_invalid() -> None:
|
|
1183
|
+
"""Test anonymized FieldDefinition schema with invalid enum values and recursive types."""
|
|
1184
|
+
schema = {
|
|
1185
|
+
"$defs": {
|
|
1186
|
+
"FieldDefinition": {
|
|
1187
|
+
"type": "object",
|
|
1188
|
+
"properties": {
|
|
1189
|
+
"type": {
|
|
1190
|
+
"type": "string",
|
|
1191
|
+
"enum": ["BOOLEAN", "STRING", "INTEGER", "STRUCT", "LIST"],
|
|
1192
|
+
},
|
|
1193
|
+
"structFields": {
|
|
1194
|
+
"type": "object",
|
|
1195
|
+
"additionalProperties": {"$ref": "#/$defs/FieldDefinition"},
|
|
1196
|
+
},
|
|
1197
|
+
"listElement": {"$ref": "#/$defs/FieldDefinition"},
|
|
1198
|
+
},
|
|
1199
|
+
"required": ["type"],
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
"$ref": "#/$defs/FieldDefinition",
|
|
1203
|
+
}
|
|
1204
|
+
validator = convert_to_voluptuous(schema)
|
|
1205
|
+
|
|
1206
|
+
# Fails if enum value is wrong
|
|
1207
|
+
with pytest.raises(vol.Invalid):
|
|
1208
|
+
validator({"type": "FLOAT"})
|
|
1209
|
+
|
|
1210
|
+
# Fails if recursive validation fails
|
|
1211
|
+
with pytest.raises(vol.Invalid):
|
|
1212
|
+
validator(
|
|
1213
|
+
{
|
|
1214
|
+
"type": "STRUCT",
|
|
1215
|
+
"structFields": {"invalid_field": {"type": 123}}, # type must be string
|
|
1216
|
+
}
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def test_anonymized_ghp_home_automation_valid() -> None:
|
|
1221
|
+
"""Test anonymized GhpHomeAutomation schema with valid automation rules."""
|
|
1222
|
+
schema = {
|
|
1223
|
+
"$defs": {
|
|
1224
|
+
"Comparison": {
|
|
1225
|
+
"type": "object",
|
|
1226
|
+
"properties": {
|
|
1227
|
+
"fieldToCompare": {"type": "string"},
|
|
1228
|
+
"operator": {"type": "string", "enum": ["EQUALS", "LESS_THAN"]},
|
|
1229
|
+
"nestedComparison": {"$ref": "#/$defs/Comparison"},
|
|
1230
|
+
"operands": {"type": "array", "items": {"$ref": "#/$defs/Operand"}},
|
|
1231
|
+
},
|
|
1232
|
+
},
|
|
1233
|
+
"Operand": {
|
|
1234
|
+
"type": "object",
|
|
1235
|
+
"properties": {
|
|
1236
|
+
"comparison": {"$ref": "#/$defs/Comparison"},
|
|
1237
|
+
"value": {"type": "string"},
|
|
1238
|
+
},
|
|
1239
|
+
},
|
|
1240
|
+
"ConditionBlock": {
|
|
1241
|
+
"type": "object",
|
|
1242
|
+
"properties": {
|
|
1243
|
+
"comparison": {"$ref": "#/$defs/Comparison"},
|
|
1244
|
+
"description": {"type": "string"},
|
|
1245
|
+
},
|
|
1246
|
+
},
|
|
1247
|
+
},
|
|
1248
|
+
"type": "object",
|
|
1249
|
+
"properties": {
|
|
1250
|
+
"conditions": {"type": "array", "items": {"$ref": "#/$defs/ConditionBlock"}}
|
|
1251
|
+
},
|
|
1252
|
+
}
|
|
1253
|
+
validator = convert_to_voluptuous(schema)
|
|
1254
|
+
|
|
1255
|
+
valid_automation = {
|
|
1256
|
+
"conditions": [
|
|
1257
|
+
{
|
|
1258
|
+
"description": "Check temperature",
|
|
1259
|
+
"comparison": {
|
|
1260
|
+
"fieldToCompare": "temp",
|
|
1261
|
+
"operator": "LESS_THAN",
|
|
1262
|
+
"operands": [{"value": "72"}],
|
|
1263
|
+
},
|
|
1264
|
+
}
|
|
1265
|
+
]
|
|
1266
|
+
}
|
|
1267
|
+
assert validator(valid_automation) == valid_automation
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
def test_anonymized_ghp_home_automation_invalid() -> None:
|
|
1271
|
+
"""Test anonymized GhpHomeAutomation schema with invalid operator constraints."""
|
|
1272
|
+
schema = {
|
|
1273
|
+
"$defs": {
|
|
1274
|
+
"Comparison": {
|
|
1275
|
+
"type": "object",
|
|
1276
|
+
"properties": {
|
|
1277
|
+
"fieldToCompare": {"type": "string"},
|
|
1278
|
+
"operator": {"type": "string", "enum": ["EQUALS", "LESS_THAN"]},
|
|
1279
|
+
"nestedComparison": {"$ref": "#/$defs/Comparison"},
|
|
1280
|
+
"operands": {"type": "array", "items": {"$ref": "#/$defs/Operand"}},
|
|
1281
|
+
},
|
|
1282
|
+
},
|
|
1283
|
+
"Operand": {
|
|
1284
|
+
"type": "object",
|
|
1285
|
+
"properties": {
|
|
1286
|
+
"comparison": {"$ref": "#/$defs/Comparison"},
|
|
1287
|
+
"value": {"type": "string"},
|
|
1288
|
+
},
|
|
1289
|
+
},
|
|
1290
|
+
"ConditionBlock": {
|
|
1291
|
+
"type": "object",
|
|
1292
|
+
"properties": {
|
|
1293
|
+
"comparison": {"$ref": "#/$defs/Comparison"},
|
|
1294
|
+
"description": {"type": "string"},
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
},
|
|
1298
|
+
"type": "object",
|
|
1299
|
+
"properties": {
|
|
1300
|
+
"conditions": {"type": "array", "items": {"$ref": "#/$defs/ConditionBlock"}}
|
|
1301
|
+
},
|
|
1302
|
+
}
|
|
1303
|
+
validator = convert_to_voluptuous(schema)
|
|
1304
|
+
|
|
1305
|
+
# Invalid operator in nested comparison
|
|
1306
|
+
invalid_automation = {
|
|
1307
|
+
"conditions": [
|
|
1308
|
+
{
|
|
1309
|
+
"comparison": {
|
|
1310
|
+
"fieldToCompare": "temp",
|
|
1311
|
+
"operator": "GREATER_THAN", # Not in comparison enum ["EQUALS", "LESS_THAN"]
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
]
|
|
1315
|
+
}
|
|
1316
|
+
with pytest.raises(vol.Invalid):
|
|
1317
|
+
validator(invalid_automation)
|
|
@@ -576,3 +576,70 @@ def test_maybe(validator: Validator) -> None:
|
|
|
576
576
|
|
|
577
577
|
with pytest.raises(InvalidFormat):
|
|
578
578
|
validator({"key": "value"})
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@pytest.mark.parametrize(
|
|
582
|
+
"validator",
|
|
583
|
+
generate_validators(
|
|
584
|
+
{
|
|
585
|
+
"type": "object",
|
|
586
|
+
"properties": {
|
|
587
|
+
"color": {"type": "string"},
|
|
588
|
+
"temperature": {"type": "integer"},
|
|
589
|
+
"brightness": {
|
|
590
|
+
"type": "integer",
|
|
591
|
+
"minimum": 0,
|
|
592
|
+
"maximum": 100,
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
"anyOf": [
|
|
596
|
+
{"required": ["color"]},
|
|
597
|
+
{"required": ["temperature"]},
|
|
598
|
+
{"required": ["brightness"]},
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
vol.Schema(
|
|
602
|
+
{
|
|
603
|
+
vol.Required(vol.Any("color", "temperature", "brightness")): object,
|
|
604
|
+
vol.Optional("color"): str,
|
|
605
|
+
vol.Optional("temperature"): int,
|
|
606
|
+
vol.Optional("brightness"): vol.All(int, vol.Range(min=0, max=100)),
|
|
607
|
+
}
|
|
608
|
+
),
|
|
609
|
+
),
|
|
610
|
+
ids=TEST_IDS,
|
|
611
|
+
)
|
|
612
|
+
def test_any_of_constraint(validator: Validator) -> None:
|
|
613
|
+
"""Test anyOf constraint for requiring at least one of multiple properties."""
|
|
614
|
+
# Test valid cases
|
|
615
|
+
validator({"color": "red"})
|
|
616
|
+
validator({"temperature": 20})
|
|
617
|
+
validator({"brightness": 80})
|
|
618
|
+
validator({"brightness": 0})
|
|
619
|
+
validator({"brightness": 100})
|
|
620
|
+
validator({"color": "blue", "temperature": 25})
|
|
621
|
+
validator({"color": "green", "brightness": 100})
|
|
622
|
+
validator({"temperature": 22, "brightness": 90})
|
|
623
|
+
validator({"color": "purple", "temperature": 21, "brightness": 70})
|
|
624
|
+
|
|
625
|
+
# Test invalid cases
|
|
626
|
+
with pytest.raises(InvalidFormat):
|
|
627
|
+
validator({}) # Missing all required properties
|
|
628
|
+
|
|
629
|
+
with pytest.raises(InvalidFormat):
|
|
630
|
+
validator({"other_field": "value"}) # Missing all required properties
|
|
631
|
+
|
|
632
|
+
with pytest.raises(InvalidFormat):
|
|
633
|
+
validator({"brightness": -1}) # Out of range
|
|
634
|
+
|
|
635
|
+
with pytest.raises(InvalidFormat):
|
|
636
|
+
validator({"brightness": 101}) # Out of range
|
|
637
|
+
|
|
638
|
+
with pytest.raises(InvalidFormat):
|
|
639
|
+
validator({"brightness": "abc"}) # Wrong type
|
|
640
|
+
|
|
641
|
+
with pytest.raises(InvalidFormat):
|
|
642
|
+
validator({"color": 123}) # Wrong type
|
|
643
|
+
|
|
644
|
+
with pytest.raises(InvalidFormat):
|
|
645
|
+
validator({"temperature": "abc"}) # Wrong type
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from collections.abc import Callable, Mapping, Sequence
|
|
4
4
|
from inspect import signature
|
|
5
5
|
from enum import Enum, StrEnum
|
|
6
|
+
import itertools
|
|
6
7
|
import re
|
|
7
8
|
from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints
|
|
8
9
|
from types import NoneType, UnionType
|
|
@@ -24,11 +25,12 @@ UNSUPPORTED = object()
|
|
|
24
25
|
OPENAPI_UNSUPPORTED_KEYWORDS = {
|
|
25
26
|
"allOf",
|
|
26
27
|
"multipleOf",
|
|
27
|
-
"minItems",
|
|
28
|
-
"maxItems",
|
|
29
28
|
"uniqueItems",
|
|
30
29
|
}
|
|
31
30
|
|
|
31
|
+
# The standard JSON Schema reference keyword
|
|
32
|
+
JSON_SCHEMA_REF = "$ref"
|
|
33
|
+
|
|
32
34
|
|
|
33
35
|
class OpenApiVersion(StrEnum):
|
|
34
36
|
"""The OpenAPI version.
|
|
@@ -88,6 +90,9 @@ def convert(
|
|
|
88
90
|
if isinstance(schema, Mapping):
|
|
89
91
|
properties = {}
|
|
90
92
|
required = []
|
|
93
|
+
any_of_constraint_groups: list[list[str]] = (
|
|
94
|
+
[]
|
|
95
|
+
) # List of lists, each containing candidate keys for a Required(Any(...))
|
|
91
96
|
|
|
92
97
|
for key, value in schema.items():
|
|
93
98
|
description = None
|
|
@@ -101,22 +106,57 @@ def convert(
|
|
|
101
106
|
if description:
|
|
102
107
|
pval["description"] = key.description
|
|
103
108
|
|
|
104
|
-
if isinstance(key,
|
|
109
|
+
if isinstance(key, vol.Required):
|
|
110
|
+
if not isinstance(key.schema, vol.Any):
|
|
111
|
+
required.append(str(key.schema))
|
|
112
|
+
if key.default is not vol.UNDEFINED:
|
|
113
|
+
pval["default"] = key.default()
|
|
114
|
+
elif isinstance(key, vol.Optional):
|
|
105
115
|
if key.default is not vol.UNDEFINED:
|
|
106
116
|
pval["default"] = key.default()
|
|
107
117
|
|
|
108
118
|
pval = ensure_default(pval)
|
|
109
119
|
|
|
110
120
|
if isinstance(pkey, vol.Any):
|
|
111
|
-
for
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
# Handle Required(Any(...)) pattern for anyOf constraints
|
|
122
|
+
if isinstance(key, vol.Required):
|
|
123
|
+
# Extract candidate keys and their descriptions from Any validator
|
|
124
|
+
candidate_items = []
|
|
125
|
+
for val_item in pkey.validators:
|
|
126
|
+
item_key = ""
|
|
127
|
+
item_desc = None
|
|
128
|
+
if isinstance(val_item, vol.Marker):
|
|
129
|
+
item_key = str(val_item.schema)
|
|
130
|
+
item_desc = val_item.description
|
|
116
131
|
else:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
132
|
+
item_key = str(val_item)
|
|
133
|
+
candidate_items.append({"key": item_key, "desc": item_desc})
|
|
134
|
+
|
|
135
|
+
candidate_keys = [item["key"] for item in candidate_items]
|
|
136
|
+
any_of_constraint_groups.append(candidate_keys)
|
|
137
|
+
|
|
138
|
+
# If the value is not a wildcard, create properties for each
|
|
139
|
+
# candidate key, preserving any descriptions.
|
|
140
|
+
if value is not object:
|
|
141
|
+
for item in candidate_items:
|
|
142
|
+
prop_schema = pval.copy()
|
|
143
|
+
final_description = item["desc"] or description
|
|
144
|
+
if final_description:
|
|
145
|
+
prop_schema["description"] = final_description
|
|
146
|
+
properties[item["key"]] = prop_schema
|
|
147
|
+
else:
|
|
148
|
+
# Handle Optional(Any(...)) - expand to individual properties
|
|
149
|
+
for val_item in pkey.validators:
|
|
150
|
+
if isinstance(val_item, vol.Marker):
|
|
151
|
+
if val_item.description:
|
|
152
|
+
properties[str(val_item.schema)] = pval.copy()
|
|
153
|
+
properties[str(val_item.schema)][
|
|
154
|
+
"description"
|
|
155
|
+
] = val_item.description
|
|
156
|
+
else:
|
|
157
|
+
properties[str(val_item)] = pval
|
|
158
|
+
else:
|
|
159
|
+
properties[str(val_item)] = pval
|
|
120
160
|
elif isinstance(pkey, str):
|
|
121
161
|
properties[pkey] = pval
|
|
122
162
|
else:
|
|
@@ -126,15 +166,21 @@ def convert(
|
|
|
126
166
|
if additional_properties is None:
|
|
127
167
|
additional_properties = pval
|
|
128
168
|
|
|
129
|
-
if isinstance(key, vol.Required) and not isinstance(pkey, vol.Any):
|
|
130
|
-
required.append(str(pkey))
|
|
131
|
-
|
|
132
169
|
val = {"type": "object"}
|
|
133
170
|
if properties or not additional_properties:
|
|
134
171
|
val["properties"] = properties
|
|
135
172
|
val["required"] = required
|
|
136
173
|
if additional_properties:
|
|
137
174
|
val["additionalProperties"] = additional_properties
|
|
175
|
+
|
|
176
|
+
# Generate anyOf constraints from the Cartesian product of constraint groups
|
|
177
|
+
if any_of_constraint_groups:
|
|
178
|
+
# Generate all combinations (Cartesian product) of the constraint groups
|
|
179
|
+
val["anyOf"] = [
|
|
180
|
+
{"required": list(combination)}
|
|
181
|
+
for combination in itertools.product(*any_of_constraint_groups)
|
|
182
|
+
]
|
|
183
|
+
|
|
138
184
|
return val
|
|
139
185
|
|
|
140
186
|
if isinstance(schema, vol.All):
|
|
@@ -406,12 +452,112 @@ def convert(
|
|
|
406
452
|
raise ValueError("Unable to convert schema: {}".format(schema))
|
|
407
453
|
|
|
408
454
|
|
|
409
|
-
def
|
|
410
|
-
"""
|
|
455
|
+
def resolve_ref(ref: str, root_schema: dict) -> dict:
|
|
456
|
+
"""Resolve a JSON pointer reference within the root_schema.
|
|
457
|
+
|
|
458
|
+
A JSON pointer (defined in RFC 6901) is a string syntax for identifying a
|
|
459
|
+
specific value within a JSON document. It uses '/' to separate keys or list
|
|
460
|
+
indices. This function resolves local pointer references starting with '#'.
|
|
461
|
+
|
|
462
|
+
Example:
|
|
463
|
+
Given root_schema = {"$defs": {"User": {"type": "object"}}},
|
|
464
|
+
resolve_ref("#/$defs/User", root_schema) returns {"type": "object"}.
|
|
465
|
+
"""
|
|
466
|
+
if not ref.startswith("#"):
|
|
467
|
+
raise ValueError(f"External/relative $ref not supported: {ref}")
|
|
468
|
+
if ref == "#" or ref == "":
|
|
469
|
+
return root_schema
|
|
470
|
+
|
|
471
|
+
parts = ref.lstrip("#").split("/")
|
|
472
|
+
# If the ref was '#/something', lstrip('#') is '/something', and split('/')
|
|
473
|
+
# produces ['', 'something']. We pop the first empty element.
|
|
474
|
+
if parts[0] == "":
|
|
475
|
+
parts.pop(0)
|
|
476
|
+
|
|
477
|
+
current = root_schema
|
|
478
|
+
for part in parts:
|
|
479
|
+
# RFC 6901 specifies '~1' represents '/' and '~0' represents '~'
|
|
480
|
+
part = part.replace("~1", "/").replace("~0", "~")
|
|
481
|
+
if isinstance(current, dict) and part in current:
|
|
482
|
+
current = current[part]
|
|
483
|
+
elif isinstance(current, list):
|
|
484
|
+
try:
|
|
485
|
+
idx = int(part)
|
|
486
|
+
current = current[idx]
|
|
487
|
+
except (ValueError, IndexError):
|
|
488
|
+
raise ValueError(f"Could not resolve ref {ref} in list at index {part}")
|
|
489
|
+
else:
|
|
490
|
+
raise ValueError(f"Could not resolve ref {ref} at key {part}")
|
|
491
|
+
|
|
492
|
+
return current
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class LazySchema:
|
|
496
|
+
"""A wrapper validator that resolves and compiles a schema reference lazily.
|
|
497
|
+
|
|
498
|
+
This leverages voluptuous's ability to use any Python callable as a validator.
|
|
499
|
+
By returning a LazySchema instance during compilation, we avoid infinite
|
|
500
|
+
recursion/loops for circular or self-referential schemas. The schema is
|
|
501
|
+
resolved and compiled only on its first invocation, and cached for future runs.
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
def __init__(self, ref: str, root_schema: dict):
|
|
505
|
+
self.ref = ref
|
|
506
|
+
self.root_schema = root_schema
|
|
507
|
+
self._compiled_schema = None
|
|
508
|
+
|
|
509
|
+
def __call__(self, value: Any) -> Any:
|
|
510
|
+
"""Validate the value against the lazily compiled schema.
|
|
511
|
+
|
|
512
|
+
On the first validation call, this resolves the JSON pointer reference
|
|
513
|
+
and compiles it into a voluptuous validator. Subsequent validation calls
|
|
514
|
+
bypass the compilation and reuse the cached validator.
|
|
515
|
+
"""
|
|
516
|
+
if self._compiled_schema is None:
|
|
517
|
+
target_schema = resolve_ref(self.ref, self.root_schema)
|
|
518
|
+
self._compiled_schema = convert_to_voluptuous(
|
|
519
|
+
target_schema, self.root_schema
|
|
520
|
+
)
|
|
521
|
+
return self._compiled_schema(value)
|
|
522
|
+
|
|
523
|
+
def __eq__(self, other: Any) -> bool:
|
|
524
|
+
if not isinstance(other, LazySchema):
|
|
525
|
+
return False
|
|
526
|
+
return self.ref == other.ref and self.root_schema == other.root_schema
|
|
527
|
+
|
|
528
|
+
def __repr__(self) -> str:
|
|
529
|
+
return f"LazySchema({self.ref!r})"
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def convert_to_voluptuous(schema: dict, root_schema: dict | None = None) -> Any:
|
|
533
|
+
"""Convert an OpenAPI/JSON Schema object to a voluptuous schema.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
schema: The OpenAPI/JSON Schema dictionary to convert.
|
|
537
|
+
root_schema: The root schema document, which is propagated recursively
|
|
538
|
+
to resolve local JSON pointer references ($ref). If not provided,
|
|
539
|
+
defaults to the current schema.
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
A voluptuous Schema, a type validator, or a custom validator callable.
|
|
543
|
+
"""
|
|
411
544
|
|
|
412
545
|
if not isinstance(schema, dict):
|
|
413
546
|
raise ValueError("Invalid schema, expected a dictionary")
|
|
414
547
|
|
|
548
|
+
if root_schema is None:
|
|
549
|
+
root_schema = schema
|
|
550
|
+
|
|
551
|
+
if JSON_SCHEMA_REF in schema:
|
|
552
|
+
return LazySchema(schema[JSON_SCHEMA_REF], root_schema)
|
|
553
|
+
|
|
554
|
+
if (enum_values := schema.get("enum")) is not None:
|
|
555
|
+
if not isinstance(enum_values, list):
|
|
556
|
+
raise ValueError("Invalid schema, enum should be a list")
|
|
557
|
+
if schema.get("nullable") is True:
|
|
558
|
+
return vol.Any(vol.In(enum_values), None)
|
|
559
|
+
return vol.In(enum_values)
|
|
560
|
+
|
|
415
561
|
for keyword in OPENAPI_UNSUPPORTED_KEYWORDS:
|
|
416
562
|
if keyword in schema:
|
|
417
563
|
raise ValueError(f"{keyword} is not supported")
|
|
@@ -421,12 +567,26 @@ def convert_to_voluptuous(schema: dict) -> vol.Schema:
|
|
|
421
567
|
raise ValueError("Invalid schema, oneOf should be a list")
|
|
422
568
|
# This implements anyOf semantics sice it matches any of the subschemas,
|
|
423
569
|
# not just one of them.
|
|
424
|
-
return vol.Any(
|
|
570
|
+
return vol.Any(
|
|
571
|
+
*[convert_to_voluptuous(sub_schema, root_schema) for sub_schema in one_of]
|
|
572
|
+
)
|
|
425
573
|
|
|
426
574
|
if (any_of := schema.get("anyOf")) is not None:
|
|
427
575
|
if not isinstance(any_of, list):
|
|
428
576
|
raise ValueError("Invalid schema, anyOf should be a list")
|
|
429
|
-
|
|
577
|
+
# If the anyOf is a list of required properties, it's a constraint on an
|
|
578
|
+
# object and should be handled by the object validator.
|
|
579
|
+
is_required_constraint = all(
|
|
580
|
+
list(s.keys()) == ["required"] and isinstance(s["required"], list)
|
|
581
|
+
for s in any_of
|
|
582
|
+
)
|
|
583
|
+
if not (is_required_constraint and schema.get("type") == "object"):
|
|
584
|
+
return vol.Any(
|
|
585
|
+
*[
|
|
586
|
+
convert_to_voluptuous(sub_schema, root_schema)
|
|
587
|
+
for sub_schema in any_of
|
|
588
|
+
]
|
|
589
|
+
)
|
|
430
590
|
|
|
431
591
|
if (schema_type := schema.get("type")) is None:
|
|
432
592
|
raise ValueError("Invalid schema, missing type")
|
|
@@ -446,7 +606,7 @@ def convert_to_voluptuous(schema: dict) -> vol.Schema:
|
|
|
446
606
|
else:
|
|
447
607
|
base_schema = schema.copy()
|
|
448
608
|
base_schema["type"] = t
|
|
449
|
-
validators.append(convert_to_voluptuous(base_schema))
|
|
609
|
+
validators.append(convert_to_voluptuous(base_schema, root_schema))
|
|
450
610
|
return vol.Any(*validators)
|
|
451
611
|
|
|
452
612
|
if (basic_type := TYPES_MAP_REV.get(schema_type)) is not None:
|
|
@@ -481,7 +641,7 @@ def convert_to_voluptuous(schema: dict) -> vol.Schema:
|
|
|
481
641
|
properties = {}
|
|
482
642
|
required_properties = set(schema.get("required", []))
|
|
483
643
|
for key, value in schema.get("properties", {}).items():
|
|
484
|
-
value_type = convert_to_voluptuous(value)
|
|
644
|
+
value_type = convert_to_voluptuous(value, root_schema)
|
|
485
645
|
description: str | None = None
|
|
486
646
|
if isinstance(value, dict):
|
|
487
647
|
description = value.get("description")
|
|
@@ -491,9 +651,19 @@ def convert_to_voluptuous(schema: dict) -> vol.Schema:
|
|
|
491
651
|
key_type = vol.Optional
|
|
492
652
|
properties[key_type(key, description=description)] = value_type
|
|
493
653
|
|
|
494
|
-
|
|
495
|
-
|
|
654
|
+
if (any_of := schema.get("anyOf")) is not None:
|
|
655
|
+
any_of_keys = [
|
|
656
|
+
key for sub_schema in any_of for key in sub_schema.get("required", [])
|
|
657
|
+
]
|
|
658
|
+
if any_of_keys:
|
|
659
|
+
properties[vol.Required(vol.Any(*any_of_keys))] = object
|
|
660
|
+
|
|
661
|
+
extra_schema = schema.get("additionalProperties")
|
|
662
|
+
if extra_schema is True:
|
|
496
663
|
validator = vol.Schema(properties, extra=vol.ALLOW_EXTRA)
|
|
664
|
+
elif isinstance(extra_schema, dict):
|
|
665
|
+
properties[vol.Extra] = convert_to_voluptuous(extra_schema, root_schema)
|
|
666
|
+
validator = vol.Schema(properties)
|
|
497
667
|
else:
|
|
498
668
|
validator = vol.Schema(properties)
|
|
499
669
|
|
|
@@ -504,7 +674,16 @@ def convert_to_voluptuous(schema: dict) -> vol.Schema:
|
|
|
504
674
|
return validator
|
|
505
675
|
|
|
506
676
|
if schema_type == "array":
|
|
507
|
-
|
|
677
|
+
item_validator = convert_to_voluptuous(schema["items"], root_schema)
|
|
678
|
+
min_items = schema.get("minItems")
|
|
679
|
+
max_items = schema.get("maxItems")
|
|
680
|
+
|
|
681
|
+
if min_items is not None or max_items is not None:
|
|
682
|
+
validator = vol.Schema(
|
|
683
|
+
vol.All([item_validator], vol.Length(min=min_items, max=max_items))
|
|
684
|
+
)
|
|
685
|
+
else:
|
|
686
|
+
validator = vol.Schema([item_validator])
|
|
508
687
|
|
|
509
688
|
# Handle OpenAPI 3.0 nullable property
|
|
510
689
|
if schema.get("nullable") is True:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voluptuous-openapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Convert voluptuous schemas to OpenAPI Schema object
|
|
5
5
|
Author-email: Denis Shulyaka <Shulyaka@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -55,7 +55,7 @@ You can pass a custom serializer to be able to process custom validators. If the
|
|
|
55
55
|
|
|
56
56
|
```python
|
|
57
57
|
|
|
58
|
-
from
|
|
58
|
+
from voluptuous_openapi import UNSUPPORTED, convert
|
|
59
59
|
|
|
60
60
|
def custom_convert(value):
|
|
61
61
|
if value is my_custom_validator:
|
|
File without changes
|
|
File without changes
|
{voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/voluptuous_openapi.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/voluptuous_openapi.egg-info/requires.txt
RENAMED
|
File without changes
|
{voluptuous_openapi-0.2.0 → voluptuous_openapi-0.4.0}/voluptuous_openapi.egg-info/top_level.txt
RENAMED
|
File without changes
|