From 6d1febbcc12dad61a35b6dbd5834c3e9bfb9c17e Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Mon, 14 Apr 2025 03:22:42 -0300 Subject: [PATCH] Initial allOf Implementation --- jambo/parser/__init__.py | 1 + jambo/parser/allof_type_parser.py | 76 +++++++++++++++++++++++++++++++ jambo/schema_converter.py | 14 +++++- tests/test_schema_converter.py | 30 ++++++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 jambo/parser/allof_type_parser.py diff --git a/jambo/parser/__init__.py b/jambo/parser/__init__.py index c71f93d..d4a71e3 100644 --- a/jambo/parser/__init__.py +++ b/jambo/parser/__init__.py @@ -8,3 +8,4 @@ from .string_type_parser import StringTypeParser as StringTypeParser from .array_type_parser import ArrayTypeParser as ArrayTypeParser from .boolean_type_parser import BooleanTypeParser as BooleanTypeParser from .float_type_parser import FloatTypeParser as FloatTypeParser +from .allof_type_parser import AllOfTypeParser as AllOfTypeParser diff --git a/jambo/parser/allof_type_parser.py b/jambo/parser/allof_type_parser.py new file mode 100644 index 0000000..c4677c5 --- /dev/null +++ b/jambo/parser/allof_type_parser.py @@ -0,0 +1,76 @@ +from jambo.parser._type_parser import GenericTypeParser + + +class AllOfTypeParser(GenericTypeParser): + mapped_type = any + + json_schema_type = "allOf" + + @staticmethod + def from_properties(name, properties): + subProperties = properties.get("allOf") + if not subProperties: + raise ValueError("Invalid JSON Schema: 'allOf' is not specified.") + + _mapped_type = properties.get("type") + if _mapped_type is None: + _mapped_type = subProperties[0].get("type") + + if _mapped_type is None: + raise ValueError("Invalid JSON Schema: 'type' is not specified.") + + if not all(prop.get("type") == _mapped_type for prop in subProperties): + raise ValueError("Invalid JSON Schema: allOf types do not match.") + + combined_properties = AllOfTypeParser._rebuild_properties_from_subproperties( + subProperties + ) + + return GenericTypeParser.get_impl(_mapped_type).from_properties( + name, combined_properties + ) + + @staticmethod + def _rebuild_properties_from_subproperties(subProperties): + properties = {} + for subProperty in subProperties: + for name, prop in subProperty.items(): + if name not in properties: + properties[name] = prop + else: + # Merge properties if they exist in both sub-properties + properties[name] = AllOfTypeParser._validate_prop( + name, properties[name], prop + ) + return properties + + @staticmethod + def _validate_prop(prop_name, old_value, new_value): + if prop_name == "type": + if old_value != new_value: + raise ValueError( + f"Invalid JSON Schema: conflicting types for '{prop_name}'" + ) + return old_value + + if prop_name == "description": + return f"{old_value} | {new_value}" + + if prop_name == "default": + if old_value != new_value: + raise ValueError( + f"Invalid JSON Schema: conflicting defaults for '{prop_name}'" + ) + return old_value + + if prop_name == "required": + return old_value + new_value + + if prop_name in ("maxLength", "maximum", "exclusiveMaximum"): + return old_value if old_value > new_value else new_value + + if prop_name in ("minLength", "minimum", "exclusiveMinimum"): + return old_value if old_value < new_value else new_value + + # Handle other properties by just returning the first valued + return old_value diff --git a/jambo/schema_converter.py b/jambo/schema_converter.py index 5ed8afb..8d498c7 100644 --- a/jambo/schema_converter.py +++ b/jambo/schema_converter.py @@ -79,8 +79,20 @@ class SchemaConverter: def _build_field( name, properties: dict, required_keys: list[str] ) -> tuple[type, dict]: + match properties: + case {"anyOf": _}: + _field_type = "anyOf" + case {"allOf": _}: + _field_type = "allOf" + case {"oneOf": _}: + _field_type = "oneOf" + case {"type": _}: + _field_type = properties["type"] + case _: + raise ValueError(f"Invalid JSON Schema: {properties}") + _field_type, _field_args = GenericTypeParser.get_impl( - properties["type"] + _field_type ).from_properties(name, properties) _field_args = _field_args or {} diff --git a/tests/test_schema_converter.py b/tests/test_schema_converter.py index 53a7e52..62655a0 100644 --- a/tests/test_schema_converter.py +++ b/tests/test_schema_converter.py @@ -281,3 +281,33 @@ class TestSchemaConverter(TestCase): self.assertEqual(obj.address.street, "123 Main St") self.assertEqual(obj.address.city, "Springfield") + + def test_all_of(self): + schema = { + "title": "Person", + "description": "A person", + "type": "object", + "properties": { + "name": { + "allOf": [ + {"type": "string", "maxLength": 4}, + {"type": "string", "minLength": 1}, + {"type": "string", "minLength": 2}, + ] + }, + }, + } + + Model = SchemaConverter.build(schema) + + obj = Model( + name="J", + ) + + self.assertEqual(obj.name, "J") + + with self.assertRaises(ValueError): + Model(name="John Invalid") + + with self.assertRaises(ValueError): + Model(name="")