Implements: allOf, anyOf #11
@@ -13,11 +13,12 @@ class GenericTypeParser(ABC, Generic[T]):
|
||||
|
||||
json_schema_type: str = None
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def from_properties(
|
||||
name: str, properties: dict[str, any], required: bool = False
|
||||
) -> tuple[T, dict]: ...
|
||||
default_mappings = {
|
||||
"default": "default",
|
||||
"description": "description",
|
||||
}
|
||||
|
||||
type_mappings: dict[str, str] = None
|
||||
|
||||
@classmethod
|
||||
def get_impl(cls, type_name: str) -> Self:
|
||||
@@ -30,7 +31,24 @@ class GenericTypeParser(ABC, Generic[T]):
|
||||
|
||||
raise ValueError(f"Unknown type: {type_name}")
|
||||
|
||||
@staticmethod
|
||||
def validate_default(field_type: type, field_prop: dict, value):
|
||||
@abstractmethod
|
||||
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)]
|
||||
TypeAdapter(field).validate_python(value)
|
||||
|
||||
@@ -6,8 +6,7 @@ class AllOfTypeParser(GenericTypeParser):
|
||||
|
||||
json_schema_type = "allOf"
|
||||
|
||||
@staticmethod
|
||||
def from_properties(name, properties, required=False):
|
||||
def from_properties(self, name, properties, required=False):
|
||||
subProperties = properties.get("allOf")
|
||||
if not subProperties:
|
||||
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
|
||||
subProperty["type"] = _mapped_type
|
||||
|
||||
combined_properties = AllOfTypeParser._rebuild_properties_from_subproperties(
|
||||
subProperties
|
||||
)
|
||||
combined_properties = self._rebuild_properties_from_subproperties(subProperties)
|
||||
|
||||
return GenericTypeParser.get_impl(_mapped_type).from_properties(
|
||||
name, combined_properties
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _rebuild_properties_from_subproperties(subProperties):
|
||||
def _rebuild_properties_from_subproperties(self, subProperties):
|
||||
properties = {}
|
||||
for subProperty in subProperties:
|
||||
for name, prop in subProperty.items():
|
||||
|
||||
@@ -11,8 +11,7 @@ class AnyOfTypeParser(GenericTypeParser):
|
||||
|
||||
json_schema_type = "anyOf"
|
||||
|
||||
@staticmethod
|
||||
def from_properties(name, properties, required=False):
|
||||
def from_properties(self, name, properties, required=False):
|
||||
if "anyOf" not in properties:
|
||||
raise ValueError(f"Invalid JSON Schema: {properties}")
|
||||
|
||||
@@ -34,9 +33,7 @@ class AnyOfTypeParser(GenericTypeParser):
|
||||
if default_value is not None:
|
||||
for sub_type, sub_property in sub_types:
|
||||
try:
|
||||
GenericTypeParser.validate_default(
|
||||
sub_type, sub_property, default_value
|
||||
)
|
||||
self.validate_default(sub_type, sub_property, default_value)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
from jambo.parser._type_parser import GenericTypeParser
|
||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
||||
mappings_properties_builder,
|
||||
)
|
||||
|
||||
import copy
|
||||
from typing import TypeVar
|
||||
@@ -15,30 +12,29 @@ class ArrayTypeParser(GenericTypeParser):
|
||||
|
||||
json_schema_type = "array"
|
||||
|
||||
@staticmethod
|
||||
def from_properties(name, properties, required=False):
|
||||
default_mappings = {"description": "description"}
|
||||
|
||||
type_mappings = {
|
||||
"maxItems": "max_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)
|
||||
|
||||
_mappings = {
|
||||
"maxItems": "max_length",
|
||||
"minItems": "min_length",
|
||||
}
|
||||
|
||||
wrapper_type = set if properties.get("uniqueItems", False) else list
|
||||
field_type = wrapper_type[_item_type]
|
||||
|
||||
mapped_properties = mappings_properties_builder(
|
||||
mapped_properties = self.mappings_properties_builder(
|
||||
properties,
|
||||
_mappings,
|
||||
required=required,
|
||||
default_mappings={"description": "description"},
|
||||
)
|
||||
|
||||
default_list = properties.pop("default", None)
|
||||
if default_list is not None:
|
||||
ArrayTypeParser.validate_default(
|
||||
self.validate_default(
|
||||
field_type,
|
||||
mapped_properties,
|
||||
default_list,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
from jambo.parser._type_parser import GenericTypeParser
|
||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
||||
mappings_properties_builder,
|
||||
)
|
||||
|
||||
|
||||
class BooleanTypeParser(GenericTypeParser):
|
||||
@@ -9,13 +6,12 @@ class BooleanTypeParser(GenericTypeParser):
|
||||
|
||||
json_schema_type = "boolean"
|
||||
|
||||
@staticmethod
|
||||
def from_properties(name, properties, required=False):
|
||||
_mappings = {
|
||||
"default": "default",
|
||||
}
|
||||
type_mappings = {
|
||||
"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")
|
||||
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.utils.properties_builder.mappings_properties_builder import (
|
||||
mappings_properties_builder,
|
||||
)
|
||||
|
||||
|
||||
class FloatTypeParser(GenericTypeParser):
|
||||
@@ -9,20 +6,20 @@ class FloatTypeParser(GenericTypeParser):
|
||||
|
||||
json_schema_type = "number"
|
||||
|
||||
@staticmethod
|
||||
def from_properties(name, 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)
|
||||
type_mappings = {
|
||||
"minimum": "ge",
|
||||
"exclusiveMinimum": "gt",
|
||||
"maximum": "le",
|
||||
"exclusiveMaximum": "lt",
|
||||
"multipleOf": "multiple_of",
|
||||
"default": "default",
|
||||
}
|
||||
|
||||
def from_properties(self, name, properties, required=False):
|
||||
mapped_properties = self.mappings_properties_builder(properties, required)
|
||||
|
||||
default_value = mapped_properties.get("default")
|
||||
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
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
from jambo.parser._type_parser import GenericTypeParser
|
||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
||||
mappings_properties_builder,
|
||||
)
|
||||
|
||||
|
||||
class IntTypeParser(GenericTypeParser):
|
||||
@@ -9,20 +6,20 @@ class IntTypeParser(GenericTypeParser):
|
||||
|
||||
json_schema_type = "integer"
|
||||
|
||||
@staticmethod
|
||||
def from_properties(name, 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)
|
||||
type_mappings = {
|
||||
"minimum": "ge",
|
||||
"exclusiveMinimum": "gt",
|
||||
"maximum": "le",
|
||||
"exclusiveMaximum": "lt",
|
||||
"multipleOf": "multiple_of",
|
||||
"default": "default",
|
||||
}
|
||||
|
||||
def from_properties(self, name, properties, required=False):
|
||||
mapped_properties = self.mappings_properties_builder(properties, required)
|
||||
|
||||
default_value = mapped_properties.get("default")
|
||||
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
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
from jambo.parser._type_parser import GenericTypeParser
|
||||
from jambo.utils.properties_builder.mappings_properties_builder import (
|
||||
mappings_properties_builder,
|
||||
)
|
||||
|
||||
|
||||
class StringTypeParser(GenericTypeParser):
|
||||
@@ -9,18 +6,17 @@ class StringTypeParser(GenericTypeParser):
|
||||
|
||||
json_schema_type = "string"
|
||||
|
||||
@staticmethod
|
||||
def from_properties(name, properties, required=False):
|
||||
_mappings = {
|
||||
"maxLength": "max_length",
|
||||
"minLength": "min_length",
|
||||
"pattern": "pattern",
|
||||
}
|
||||
type_mappings = {
|
||||
"maxLength": "max_length",
|
||||
"minLength": "min_length",
|
||||
"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")
|
||||
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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -83,7 +83,7 @@ class TestAllOfTypeParser(TestCase):
|
||||
],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = AllOfTypeParser.from_properties(
|
||||
type_parsing, type_validator = AllOfTypeParser().from_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
|
||||
)
|
||||
|
||||
@@ -133,7 +133,7 @@ class TestAllOfTypeParser(TestCase):
|
||||
]
|
||||
}
|
||||
|
||||
type_parsing, type_validator = AllOfTypeParser.from_properties(
|
||||
type_parsing, type_validator = AllOfTypeParser().from_properties(
|
||||
"placeholder", properties
|
||||
)
|
||||
|
||||
@@ -155,7 +155,7 @@ class TestAllOfTypeParser(TestCase):
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
AllOfTypeParser.from_properties("placeholder", properties)
|
||||
AllOfTypeParser().from_properties("placeholder", properties)
|
||||
|
||||
def test_all_of_invalid_type_not_present(self):
|
||||
properties = {
|
||||
@@ -168,7 +168,7 @@ class TestAllOfTypeParser(TestCase):
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
AllOfTypeParser.from_properties("placeholder", properties)
|
||||
AllOfTypeParser().from_properties("placeholder", properties)
|
||||
|
||||
def test_all_of_invalid_type_in_fields(self):
|
||||
properties = {
|
||||
@@ -181,7 +181,7 @@ class TestAllOfTypeParser(TestCase):
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
AllOfTypeParser.from_properties("placeholder", properties)
|
||||
AllOfTypeParser().from_properties("placeholder", properties)
|
||||
|
||||
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(
|
||||
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()
|
||||
self.assertEqual(obj.name, "John")
|
||||
self.assertEqual(obj.age, 30)
|
||||
@@ -289,4 +289,4 @@ class TestAllOfTypeParser(TestCase):
|
||||
}
|
||||
|
||||
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
|
||||
self.assertEqual(get_origin(type_parsing), Union)
|
||||
@@ -45,7 +45,7 @@ class TestAnyOfTypeParser(TestCase):
|
||||
"default": 42,
|
||||
}
|
||||
|
||||
type_parsing, type_validator = AnyOfTypeParser.from_properties(
|
||||
type_parsing, type_validator = AnyOfTypeParser().from_properties(
|
||||
"placeholder", properties
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user