diff --git a/jambo/parser/_type_parser.py b/jambo/parser/_type_parser.py index 66c6245..1dc07d3 100644 --- a/jambo/parser/_type_parser.py +++ b/jambo/parser/_type_parser.py @@ -12,7 +12,7 @@ T = TypeVar("T") class GenericTypeParser(ABC, Generic[T]): json_schema_type: str = None - type_mappings: dict[str, str] = None + type_mappings: dict[str, str] = {} default_mappings = { "default": "default", @@ -20,14 +20,51 @@ class GenericTypeParser(ABC, Generic[T]): } @abstractmethod + def from_properties_impl( + self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions] + ) -> tuple[T, dict]: + """ + Abstract method to convert properties to a type and its fields properties. + :param name: The name of the type. + :param properties: The properties of the type. + :param kwargs: Additional options for type parsing. + :return: A tuple containing the type and its properties. + """ + pass + def from_properties( self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions] - ) -> tuple[T, dict]: ... + ) -> tuple[type, dict]: + """ + Converts properties to a type and its fields properties. + :param name: The name of the type. + :param properties: The properties of the type. + :param kwargs: Additional options for type parsing. + :return: A tuple containing the type and its properties. + """ + parsed_type, parsed_properties = self.from_properties_impl( + name, properties, **kwargs + ) + + if not self._validate_default(parsed_type, parsed_properties): + raise ValueError( + f"Default value {properties.get('default')} is not valid for type {parsed_type.__name__}" + ) + + return parsed_type, parsed_properties @classmethod def type_from_properties( cls, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions] ) -> tuple[type, dict]: + """ + Factory method to fetch the appropriate type parser based on properties + and generates the equivalent type and fields. + :param name: The name of the type to be created. + :param properties: The properties that define the type. + :param kwargs: Additional options for type parsing. + :return: A tuple containing the type and its properties. + """ parser = cls._get_impl(properties) return parser().from_properties(name=name, properties=properties, **kwargs) @@ -62,9 +99,6 @@ class GenericTypeParser(ABC, Generic[T]): def mappings_properties_builder( self, properties, **kwargs: Unpack[TypeParserOptions] ) -> dict[str, Any]: - if self.type_mappings is None: - raise NotImplementedError("Type mappings not defined") - if not kwargs.get("required", False): properties["default"] = properties.get("default", None) @@ -74,6 +108,20 @@ class GenericTypeParser(ABC, Generic[T]): 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) + @staticmethod + def _validate_default(field_type: type, field_prop: dict) -> bool: + value = field_prop.get("default") + + if value is None and field_prop.get("default_factory") is not None: + value = field_prop["default_factory"]() + + if value is None: + return True + + try: + field = Annotated[field_type, Field(**field_prop)] + TypeAdapter(field).validate_python(value) + except Exception as _: + return False + + return True diff --git a/jambo/parser/allof_type_parser.py b/jambo/parser/allof_type_parser.py index c3a275f..3180ae3 100644 --- a/jambo/parser/allof_type_parser.py +++ b/jambo/parser/allof_type_parser.py @@ -9,7 +9,9 @@ class AllOfTypeParser(GenericTypeParser): json_schema_type = "allOf" - def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]): + def from_properties_impl( + self, name, properties, **kwargs: Unpack[TypeParserOptions] + ): sub_properties = properties.get("allOf", []) root_type = properties.get("type") @@ -23,7 +25,7 @@ class AllOfTypeParser(GenericTypeParser): sub_properties ) - return parser().from_properties(name, combined_properties, **kwargs) + return parser().from_properties_impl(name, combined_properties, **kwargs) @staticmethod def _get_type_parser( diff --git a/jambo/parser/anyof_type_parser.py b/jambo/parser/anyof_type_parser.py index d617406..55ff3ec 100644 --- a/jambo/parser/anyof_type_parser.py +++ b/jambo/parser/anyof_type_parser.py @@ -10,14 +10,16 @@ class AnyOfTypeParser(GenericTypeParser): json_schema_type = "anyOf" - def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]): + def from_properties_impl( + self, name, properties, **kwargs: Unpack[TypeParserOptions] + ): if "anyOf" not in properties: raise ValueError(f"Invalid JSON Schema: {properties}") if not isinstance(properties["anyOf"], list): raise ValueError(f"Invalid JSON Schema: {properties['anyOf']}") - mapped_properties = dict() + mapped_properties = self.mappings_properties_builder(properties, **kwargs) sub_properties = properties["anyOf"] @@ -26,21 +28,6 @@ class AnyOfTypeParser(GenericTypeParser): for subProperty in sub_properties ] - default_value = properties.get("default") - if default_value is not None: - for sub_type, sub_property in sub_types: - try: - self.validate_default(sub_type, sub_property, default_value) - break - except ValueError: - continue - else: - raise ValueError( - f"Invalid default value {default_value} for anyOf types: {sub_types}" - ) - - mapped_properties["default"] = default_value - if not kwargs.get("required", False): mapped_properties["default"] = mapped_properties.get("default") diff --git a/jambo/parser/array_type_parser.py b/jambo/parser/array_type_parser.py index 612044e..c92de3d 100644 --- a/jambo/parser/array_type_parser.py +++ b/jambo/parser/array_type_parser.py @@ -1,7 +1,7 @@ from jambo.parser._type_parser import GenericTypeParser from jambo.types.type_parser_options import TypeParserOptions -from typing_extensions import TypeVar, Unpack +from typing_extensions import Iterable, TypeVar, Unpack import copy @@ -21,7 +21,9 @@ class ArrayTypeParser(GenericTypeParser): "minItems": "min_length", } - def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]): + def from_properties_impl( + self, name, properties, **kwargs: Unpack[TypeParserOptions] + ): item_properties = kwargs.copy() item_properties["required"] = True _item_type, _item_args = GenericTypeParser.type_from_properties( @@ -33,21 +35,20 @@ class ArrayTypeParser(GenericTypeParser): mapped_properties = self.mappings_properties_builder(properties, **kwargs) - default_list = properties.pop("default", None) - if default_list is not None: - self.validate_default( - field_type, - mapped_properties, - default_list, + if "default" not in mapped_properties: + mapped_properties["default_factory"] = self._build_default_factory( + properties.get("default"), wrapper_type ) - if wrapper_type is list: - mapped_properties["default_factory"] = lambda: copy.deepcopy( - wrapper_type(default_list) - ) - else: - mapped_properties["default_factory"] = lambda: wrapper_type( - default_list - ) - return field_type, mapped_properties + + def _build_default_factory(self, default_list, wrapper_type): + if default_list is None: + return lambda: None + + if not isinstance(default_list, Iterable): + raise ValueError( + f"Default value for array must be an iterable, got {type(default_list)}" + ) + + return lambda: copy.deepcopy(wrapper_type(default_list)) diff --git a/jambo/parser/boolean_type_parser.py b/jambo/parser/boolean_type_parser.py index b2fc1b0..ecb703a 100644 --- a/jambo/parser/boolean_type_parser.py +++ b/jambo/parser/boolean_type_parser.py @@ -13,7 +13,9 @@ class BooleanTypeParser(GenericTypeParser): "default": "default", } - def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]): + def from_properties_impl( + self, name, properties, **kwargs: Unpack[TypeParserOptions] + ): mapped_properties = self.mappings_properties_builder(properties, **kwargs) default_value = properties.get("default") diff --git a/jambo/parser/float_type_parser.py b/jambo/parser/float_type_parser.py index f682cf8..f4655c3 100644 --- a/jambo/parser/float_type_parser.py +++ b/jambo/parser/float_type_parser.py @@ -18,11 +18,7 @@ class FloatTypeParser(GenericTypeParser): "default": "default", } - def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]): - mapped_properties = self.mappings_properties_builder(properties, **kwargs) - - default_value = mapped_properties.get("default") - if default_value is not None: - self.validate_default(float, mapped_properties, default_value) - - return float, mapped_properties + def from_properties_impl( + self, name, properties, **kwargs: Unpack[TypeParserOptions] + ): + return float, self.mappings_properties_builder(properties, **kwargs) diff --git a/jambo/parser/int_type_parser.py b/jambo/parser/int_type_parser.py index e65ecda..161465b 100644 --- a/jambo/parser/int_type_parser.py +++ b/jambo/parser/int_type_parser.py @@ -18,11 +18,7 @@ class IntTypeParser(GenericTypeParser): "default": "default", } - def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]): - mapped_properties = self.mappings_properties_builder(properties, **kwargs) - - default_value = mapped_properties.get("default") - if default_value is not None: - self.validate_default(int, mapped_properties, default_value) - - return int, mapped_properties + def from_properties_impl( + self, name, properties, **kwargs: Unpack[TypeParserOptions] + ): + return int, self.mappings_properties_builder(properties, **kwargs) diff --git a/jambo/parser/object_type_parser.py b/jambo/parser/object_type_parser.py index 72546d9..456bc4d 100644 --- a/jambo/parser/object_type_parser.py +++ b/jambo/parser/object_type_parser.py @@ -11,7 +11,7 @@ class ObjectTypeParser(GenericTypeParser): json_schema_type = "type:object" - def from_properties( + def from_properties_impl( self, name: str, properties: dict[str, Any], **kwargs: Unpack[TypeParserOptions] ): type_parsing = self.to_model( @@ -39,7 +39,7 @@ class ObjectTypeParser(GenericTypeParser): """ Converts JSON Schema object properties to a Pydantic model. :param name: The name of the model. - :param properties: The properties of the JSON Schema object. + :param schema: The properties of the JSON Schema object. :param required_keys: List of required keys in the schema. :return: A Pydantic model class. """ diff --git a/jambo/parser/string_type_parser.py b/jambo/parser/string_type_parser.py index fe4df68..1eb25b9 100644 --- a/jambo/parser/string_type_parser.py +++ b/jambo/parser/string_type_parser.py @@ -34,24 +34,22 @@ class StringTypeParser(GenericTypeParser): "hostname": r"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$", } - def from_properties(self, name, properties, **kwargs: Unpack[TypeParserOptions]): - mapped_properties = self.mappings_properties_builder(properties, **kwargs) + def from_properties_impl( + self, name, properties, **kwargs: Unpack[TypeParserOptions] + ): + mapped_properties = self.mappings_properties_builder( + properties, **kwargs + ) format_type = properties.get("format") - if format_type: - if format_type in self.format_type_mapping: - mapped_type = self.format_type_mapping[format_type] - if format_type in self.format_pattern_mapping: - mapped_properties["pattern"] = self.format_pattern_mapping[ - format_type - ] - else: - raise ValueError(f"Unsupported string format: {format_type}") - else: - mapped_type = str + if not format_type: + return str, mapped_properties - default_value = properties.get("default") - if default_value is not None: - self.validate_default(mapped_type, mapped_properties, default_value) + if format_type not in self.format_type_mapping: + raise ValueError(f"Unsupported string format: {format_type}") + + mapped_type = self.format_type_mapping[format_type] + if format_type in self.format_pattern_mapping: + mapped_properties["pattern"] = self.format_pattern_mapping[format_type] return mapped_type, mapped_properties diff --git a/tests/parser/test_array_type_parser.py b/tests/parser/test_array_type_parser.py index c81c193..d27330f 100644 --- a/tests/parser/test_array_type_parser.py +++ b/tests/parser/test_array_type_parser.py @@ -73,7 +73,7 @@ class TestArrayTypeParser(TestCase): def test_array_parser_with_invalid_default_type(self): parser = ArrayTypeParser() - properties = {"items": {"type": "string"}, "default": "not_a_list"} + properties = {"items": {"type": "string"}, "default": 000} with self.assertRaises(ValueError): parser.from_properties("placeholder", properties) diff --git a/tests/parser/test_bool_type_parser.py b/tests/parser/test_bool_type_parser.py index f21b547..1e3a6c9 100644 --- a/tests/parser/test_bool_type_parser.py +++ b/tests/parser/test_bool_type_parser.py @@ -9,7 +9,9 @@ class TestBoolTypeParser(TestCase): properties = {"type": "boolean"} - type_parsing, type_validator = parser.from_properties("placeholder", properties) + type_parsing, type_validator = parser.from_properties_impl( + "placeholder", properties + ) self.assertEqual(type_parsing, bool) self.assertEqual(type_validator, {"default": None}) @@ -22,7 +24,9 @@ class TestBoolTypeParser(TestCase): "default": True, } - type_parsing, type_validator = parser.from_properties("placeholder", properties) + type_parsing, type_validator = parser.from_properties_impl( + "placeholder", properties + ) self.assertEqual(type_parsing, bool) self.assertEqual(type_validator["default"], True) @@ -36,4 +40,4 @@ class TestBoolTypeParser(TestCase): } with self.assertRaises(ValueError): - parser.from_properties("placeholder", properties) + parser.from_properties_impl("placeholder", properties) diff --git a/tests/parser/test_object_type_parser.py b/tests/parser/test_object_type_parser.py index 6f56727..4adbc52 100644 --- a/tests/parser/test_object_type_parser.py +++ b/tests/parser/test_object_type_parser.py @@ -15,7 +15,7 @@ class TestObjectTypeParser(TestCase): }, } - Model, _args = parser.from_properties("placeholder", properties) + Model, _args = parser.from_properties_impl("placeholder", properties) obj = Model(name="name", age=10) @@ -37,7 +37,7 @@ class TestObjectTypeParser(TestCase): }, } - _, type_validator = parser.from_properties("placeholder", properties) + _, type_validator = parser.from_properties_impl("placeholder", properties) # Check default value default_obj = type_validator["default_factory"]() diff --git a/tests/parser/test_type_parser.py b/tests/parser/test_type_parser.py index e26a3d5..1bf2687 100644 --- a/tests/parser/test_type_parser.py +++ b/tests/parser/test_type_parser.py @@ -11,7 +11,7 @@ def with_test_parser(): mapped_type = str json_schema_type = "type:invalid" - def from_properties( + def from_properties_impl( self, name: str, properties: dict[str, any], required: bool = False ): ... @@ -39,11 +39,3 @@ class TestGenericTypeParser(TestCase): ): InvalidGenericTypeParser.json_schema_type = None GenericTypeParser._get_impl({"type": "another_invalid_type"}) - - def test_invalid_mappings_properties_builder(self): - with ( - with_test_parser() as InvalidGenericTypeParser, - self.assertRaises(NotImplementedError), - ): - parser = InvalidGenericTypeParser() - parser.mappings_properties_builder({}, required=False) diff --git a/tests/test_schema_converter.py b/tests/test_schema_converter.py index 22dbddd..7b10b0f 100644 --- a/tests/test_schema_converter.py +++ b/tests/test_schema_converter.py @@ -255,6 +255,7 @@ class TestSchemaConverter(TestCase): self.assertEqual(obj.name, "John") + def test_invalid_default_for_string(self): # Test for default with maxLength schema_max_length = { "title": "Person",