Implements: allOf, anyOf #11
@@ -13,11 +13,12 @@ class GenericTypeParser(ABC, Generic[T]):
|
|||||||
|
|
||||||
json_schema_type: str = None
|
json_schema_type: str = None
|
||||||
|
|
||||||
@staticmethod
|
default_mappings = {
|
||||||
@abstractmethod
|
"default": "default",
|
||||||
def from_properties(
|
"description": "description",
|
||||||
name: str, properties: dict[str, any], required: bool = False
|
}
|
||||||
) -> tuple[T, dict]: ...
|
|
||||||
|
type_mappings: dict[str, str] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_impl(cls, type_name: str) -> Self:
|
def get_impl(cls, type_name: str) -> Self:
|
||||||
@@ -30,7 +31,24 @@ class GenericTypeParser(ABC, Generic[T]):
|
|||||||
|
|
||||||
raise ValueError(f"Unknown type: {type_name}")
|
raise ValueError(f"Unknown type: {type_name}")
|
||||||
|
|
||||||
@staticmethod
|
@abstractmethod
|
||||||
def validate_default(field_type: type, field_prop: dict, value):
|
def from_properties(
|
||||||
|
self, name: str, properties: dict[str, any], required: bool = False
|
||||||
|
) -> tuple[T, dict]: ...
|
||||||
|
|
||||||
|
def mappings_properties_builder(self, properties, required=False) -> dict[str, any]:
|
||||||
|
if self.type_mappings is None:
|
||||||
|
raise NotImplementedError("Type mappings not defined")
|
||||||
|
|
||||||
|
if not required:
|
||||||
|
properties["default"] = properties.get("default", None)
|
||||||
|
|
||||||
|
mappings = self.default_mappings | self.type_mappings
|
||||||
|
|
||||||
|
return {
|
||||||
|
mappings[key]: value for key, value in properties.items() if key in mappings
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate_default(self, field_type: type, field_prop: dict, value) -> None:
|
||||||
field = Annotated[field_type, Field(**field_prop)]
|
field = Annotated[field_type, Field(**field_prop)]
|
||||||
TypeAdapter(field).validate_python(value)
|
TypeAdapter(field).validate_python(value)
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ class AllOfTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "allOf"
|
json_schema_type = "allOf"
|
||||||
|
|
||||||
@staticmethod
|
def from_properties(self, name, properties, required=False):
|
||||||
def from_properties(name, properties, required=False):
|
|
||||||
subProperties = properties.get("allOf")
|
subProperties = properties.get("allOf")
|
||||||
if not subProperties:
|
if not subProperties:
|
||||||
raise ValueError("Invalid JSON Schema: 'allOf' is not specified.")
|
raise ValueError("Invalid JSON Schema: 'allOf' is not specified.")
|
||||||
@@ -28,16 +27,13 @@ class AllOfTypeParser(GenericTypeParser):
|
|||||||
# If a sub-property has not defined a type, we need to set it to the top-level type
|
# If a sub-property has not defined a type, we need to set it to the top-level type
|
||||||
subProperty["type"] = _mapped_type
|
subProperty["type"] = _mapped_type
|
||||||
|
|
||||||
combined_properties = AllOfTypeParser._rebuild_properties_from_subproperties(
|
combined_properties = self._rebuild_properties_from_subproperties(subProperties)
|
||||||
subProperties
|
|
||||||
)
|
|
||||||
|
|
||||||
return GenericTypeParser.get_impl(_mapped_type).from_properties(
|
return GenericTypeParser.get_impl(_mapped_type).from_properties(
|
||||||
name, combined_properties
|
name, combined_properties
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def _rebuild_properties_from_subproperties(self, subProperties):
|
||||||
def _rebuild_properties_from_subproperties(subProperties):
|
|
||||||
properties = {}
|
properties = {}
|
||||||
for subProperty in subProperties:
|
for subProperty in subProperties:
|
||||||
for name, prop in subProperty.items():
|
for name, prop in subProperty.items():
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ class AnyOfTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "anyOf"
|
json_schema_type = "anyOf"
|
||||||
|
|
||||||
@staticmethod
|
def from_properties(self, name, properties, required=False):
|
||||||
def from_properties(name, properties, required=False):
|
|
||||||
if "anyOf" not in properties:
|
if "anyOf" not in properties:
|
||||||
raise ValueError(f"Invalid JSON Schema: {properties}")
|
raise ValueError(f"Invalid JSON Schema: {properties}")
|
||||||
|
|
||||||
@@ -34,9 +33,7 @@ class AnyOfTypeParser(GenericTypeParser):
|
|||||||
if default_value is not None:
|
if default_value is not None:
|
||||||
for sub_type, sub_property in sub_types:
|
for sub_type, sub_property in sub_types:
|
||||||
try:
|
try:
|
||||||
GenericTypeParser.validate_default(
|
self.validate_default(sub_type, sub_property, default_value)
|
||||||
sub_type, sub_property, default_value
|
|
||||||
)
|
|
||||||
break
|
break
|
||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
|
||||||
mappings_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
@@ -15,30 +12,29 @@ class ArrayTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "array"
|
json_schema_type = "array"
|
||||||
|
|
||||||
@staticmethod
|
default_mappings = {"description": "description"}
|
||||||
def from_properties(name, properties, required=False):
|
|
||||||
_item_type, _item_args = GenericTypeParser.get_impl(
|
|
||||||
properties["items"]["type"]
|
|
||||||
).from_properties(name, properties["items"], required=True)
|
|
||||||
|
|
||||||
_mappings = {
|
type_mappings = {
|
||||||
"maxItems": "max_length",
|
"maxItems": "max_length",
|
||||||
"minItems": "min_length",
|
"minItems": "min_length",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def from_properties(self, name, properties, required=False):
|
||||||
|
_item_type, _item_args = GenericTypeParser.get_impl(
|
||||||
|
properties["items"]["type"]
|
||||||
|
).from_properties(name, properties["items"], required=True)
|
||||||
|
|
||||||
wrapper_type = set if properties.get("uniqueItems", False) else list
|
wrapper_type = set if properties.get("uniqueItems", False) else list
|
||||||
field_type = wrapper_type[_item_type]
|
field_type = wrapper_type[_item_type]
|
||||||
|
|
||||||
mapped_properties = mappings_properties_builder(
|
mapped_properties = self.mappings_properties_builder(
|
||||||
properties,
|
properties,
|
||||||
_mappings,
|
|
||||||
required=required,
|
required=required,
|
||||||
default_mappings={"description": "description"},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
default_list = properties.pop("default", None)
|
default_list = properties.pop("default", None)
|
||||||
if default_list is not None:
|
if default_list is not None:
|
||||||
ArrayTypeParser.validate_default(
|
self.validate_default(
|
||||||
field_type,
|
field_type,
|
||||||
mapped_properties,
|
mapped_properties,
|
||||||
default_list,
|
default_list,
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
|
||||||
mappings_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BooleanTypeParser(GenericTypeParser):
|
class BooleanTypeParser(GenericTypeParser):
|
||||||
@@ -9,13 +6,12 @@ class BooleanTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "boolean"
|
json_schema_type = "boolean"
|
||||||
|
|
||||||
@staticmethod
|
type_mappings = {
|
||||||
def from_properties(name, properties, required=False):
|
|
||||||
_mappings = {
|
|
||||||
"default": "default",
|
"default": "default",
|
||||||
}
|
}
|
||||||
|
|
||||||
mapped_properties = mappings_properties_builder(properties, _mappings, required)
|
def from_properties(self, name, properties, required=False):
|
||||||
|
mapped_properties = self.mappings_properties_builder(properties, required)
|
||||||
|
|
||||||
default_value = properties.get("default")
|
default_value = properties.get("default")
|
||||||
if default_value is not None and not isinstance(default_value, bool):
|
if default_value is not None and not isinstance(default_value, bool):
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
|
||||||
mappings_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FloatTypeParser(GenericTypeParser):
|
class FloatTypeParser(GenericTypeParser):
|
||||||
@@ -9,9 +6,7 @@ class FloatTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "number"
|
json_schema_type = "number"
|
||||||
|
|
||||||
@staticmethod
|
type_mappings = {
|
||||||
def from_properties(name, properties, required=False):
|
|
||||||
_mappings = {
|
|
||||||
"minimum": "ge",
|
"minimum": "ge",
|
||||||
"exclusiveMinimum": "gt",
|
"exclusiveMinimum": "gt",
|
||||||
"maximum": "le",
|
"maximum": "le",
|
||||||
@@ -19,10 +14,12 @@ class FloatTypeParser(GenericTypeParser):
|
|||||||
"multipleOf": "multiple_of",
|
"multipleOf": "multiple_of",
|
||||||
"default": "default",
|
"default": "default",
|
||||||
}
|
}
|
||||||
mapped_properties = mappings_properties_builder(properties, _mappings, required)
|
|
||||||
|
def from_properties(self, name, properties, required=False):
|
||||||
|
mapped_properties = self.mappings_properties_builder(properties, required)
|
||||||
|
|
||||||
default_value = mapped_properties.get("default")
|
default_value = mapped_properties.get("default")
|
||||||
if default_value is not None:
|
if default_value is not None:
|
||||||
FloatTypeParser.validate_default(float, mapped_properties, default_value)
|
self.validate_default(float, mapped_properties, default_value)
|
||||||
|
|
||||||
return float, mapped_properties
|
return float, mapped_properties
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
|
||||||
mappings_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class IntTypeParser(GenericTypeParser):
|
class IntTypeParser(GenericTypeParser):
|
||||||
@@ -9,9 +6,7 @@ class IntTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "integer"
|
json_schema_type = "integer"
|
||||||
|
|
||||||
@staticmethod
|
type_mappings = {
|
||||||
def from_properties(name, properties, required=False):
|
|
||||||
_mappings = {
|
|
||||||
"minimum": "ge",
|
"minimum": "ge",
|
||||||
"exclusiveMinimum": "gt",
|
"exclusiveMinimum": "gt",
|
||||||
"maximum": "le",
|
"maximum": "le",
|
||||||
@@ -19,10 +14,12 @@ class IntTypeParser(GenericTypeParser):
|
|||||||
"multipleOf": "multiple_of",
|
"multipleOf": "multiple_of",
|
||||||
"default": "default",
|
"default": "default",
|
||||||
}
|
}
|
||||||
mapped_properties = mappings_properties_builder(properties, _mappings, required)
|
|
||||||
|
def from_properties(self, name, properties, required=False):
|
||||||
|
mapped_properties = self.mappings_properties_builder(properties, required)
|
||||||
|
|
||||||
default_value = mapped_properties.get("default")
|
default_value = mapped_properties.get("default")
|
||||||
if default_value is not None:
|
if default_value is not None:
|
||||||
IntTypeParser.validate_default(int, mapped_properties, default_value)
|
self.validate_default(int, mapped_properties, default_value)
|
||||||
|
|
||||||
return int, mapped_properties
|
return int, mapped_properties
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
from jambo.parser._type_parser import GenericTypeParser
|
from jambo.parser._type_parser import GenericTypeParser
|
||||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
|
||||||
mappings_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StringTypeParser(GenericTypeParser):
|
class StringTypeParser(GenericTypeParser):
|
||||||
@@ -9,18 +6,17 @@ class StringTypeParser(GenericTypeParser):
|
|||||||
|
|
||||||
json_schema_type = "string"
|
json_schema_type = "string"
|
||||||
|
|
||||||
@staticmethod
|
type_mappings = {
|
||||||
def from_properties(name, properties, required=False):
|
|
||||||
_mappings = {
|
|
||||||
"maxLength": "max_length",
|
"maxLength": "max_length",
|
||||||
"minLength": "min_length",
|
"minLength": "min_length",
|
||||||
"pattern": "pattern",
|
"pattern": "pattern",
|
||||||
}
|
}
|
||||||
|
|
||||||
mapped_properties = mappings_properties_builder(properties, _mappings, required)
|
def from_properties(self, name, properties, required=False):
|
||||||
|
mapped_properties = self.mappings_properties_builder(properties, required)
|
||||||
|
|
||||||
default_value = properties.get("default")
|
default_value = properties.get("default")
|
||||||
if default_value is not None:
|
if default_value is not None:
|
||||||
StringTypeParser.validate_default(str, mapped_properties, default_value)
|
self.validate_default(str, mapped_properties, default_value)
|
||||||
|
|
||||||
return str, mapped_properties
|
return str, mapped_properties
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
def mappings_properties_builder(
|
|
||||||
properties, mappings, required=False, default_mappings=None
|
|
||||||
):
|
|
||||||
if not required:
|
|
||||||
properties["default"] = properties.get("default", None)
|
|
||||||
|
|
||||||
default_mappings = default_mappings or {
|
|
||||||
"default": "default",
|
|
||||||
"description": "description",
|
|
||||||
}
|
|
||||||
|
|
||||||
mappings = default_mappings | mappings
|
|
||||||
|
|
||||||
return {
|
|
||||||
mappings[key]: value for key, value in properties.items() if key in mappings
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
|
||||||
mappings_properties_builder,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def numeric_properties_builder(properties, required=False):
|
|
||||||
_mappings = {
|
|
||||||
"minimum": "ge",
|
|
||||||
"exclusiveMinimum": "gt",
|
|
||||||
"maximum": "le",
|
|
||||||
"exclusiveMaximum": "lt",
|
|
||||||
"multipleOf": "multiple_of",
|
|
||||||
"default": "default",
|
|
||||||
}
|
|
||||||
|
|
||||||
mapped_properties = mappings_properties_builder(properties, _mappings, required)
|
|
||||||
|
|
||||||
default_value = properties.get("default")
|
|
||||||
if default_value is not None:
|
|
||||||
default_value = properties["default"]
|
|
||||||
if not isinstance(default_value, (int, float)):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value must be a number, got {type(default_value).__name__}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if default_value > properties.get("maximum", float("inf")):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value exceeds maximum limit of {properties.get('maximum')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if default_value < properties.get("minimum", float("-inf")):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value is below minimum limit of {properties.get('minimum')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if default_value >= properties.get("exclusiveMaximum", float("inf")):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value exceeds exclusive maximum limit of {properties.get('exclusiveMaximum')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if default_value <= properties.get("exclusiveMinimum", float("-inf")):
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value is below exclusive minimum limit of {properties.get('exclusiveMinimum')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "multipleOf" in properties:
|
|
||||||
if default_value % properties["multipleOf"] != 0:
|
|
||||||
raise ValueError(
|
|
||||||
f"Default value {default_value} is not a multiple of {properties['multipleOf']}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return mapped_properties
|
|
||||||
@@ -38,7 +38,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, type_validator = AllOfTypeParser.from_properties(
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
"placeholder", properties
|
"placeholder", properties
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, type_validator = AllOfTypeParser.from_properties(
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
"placeholder", properties
|
"placeholder", properties
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, type_validator = AllOfTypeParser.from_properties(
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
"placeholder", properties
|
"placeholder", properties
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, type_validator = AllOfTypeParser.from_properties(
|
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||||
"placeholder", properties
|
"placeholder", properties
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
AllOfTypeParser.from_properties("placeholder", properties)
|
AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|
||||||
def test_all_of_invalid_type_not_present(self):
|
def test_all_of_invalid_type_not_present(self):
|
||||||
properties = {
|
properties = {
|
||||||
@@ -168,7 +168,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
AllOfTypeParser.from_properties("placeholder", properties)
|
AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|
||||||
def test_all_of_invalid_type_in_fields(self):
|
def test_all_of_invalid_type_in_fields(self):
|
||||||
properties = {
|
properties = {
|
||||||
@@ -181,7 +181,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
AllOfTypeParser.from_properties("placeholder", properties)
|
AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|
||||||
def test_all_of_description_field(self):
|
def test_all_of_description_field(self):
|
||||||
"""
|
"""
|
||||||
@@ -218,7 +218,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, _ = AllOfTypeParser.from_properties("placeholder", properties)
|
type_parsing, _ = AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
type_parsing.schema()["properties"]["name"]["description"],
|
type_parsing.schema()["properties"]["name"]["description"],
|
||||||
@@ -256,7 +256,7 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, _ = AllOfTypeParser.from_properties("placeholder", properties)
|
type_parsing, _ = AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
obj = type_parsing()
|
obj = type_parsing()
|
||||||
self.assertEqual(obj.name, "John")
|
self.assertEqual(obj.name, "John")
|
||||||
self.assertEqual(obj.age, 30)
|
self.assertEqual(obj.age, 30)
|
||||||
@@ -289,4 +289,4 @@ class TestAllOfTypeParser(TestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
AllOfTypeParser.from_properties("placeholder", properties)
|
AllOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class TestAnyOfTypeParser(TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, _ = AnyOfTypeParser.from_properties("placeholder", properties)
|
type_parsing, _ = AnyOfTypeParser().from_properties("placeholder", properties)
|
||||||
|
|
||||||
# check union type has string and int
|
# check union type has string and int
|
||||||
self.assertEqual(get_origin(type_parsing), Union)
|
self.assertEqual(get_origin(type_parsing), Union)
|
||||||
@@ -45,7 +45,7 @@ class TestAnyOfTypeParser(TestCase):
|
|||||||
"default": 42,
|
"default": 42,
|
||||||
}
|
}
|
||||||
|
|
||||||
type_parsing, type_validator = AnyOfTypeParser.from_properties(
|
type_parsing, type_validator = AnyOfTypeParser().from_properties(
|
||||||
"placeholder", properties
|
"placeholder", properties
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user