From fbbff0bd9ea1d5ac64ac42c62b5c360958dfb2c4 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Tue, 19 Aug 2025 18:48:43 -0300 Subject: [PATCH] Removes Changes Not Feature Specific --- docs/source/usage.oneof.rst | 108 ------ docs/source/usage.rst | 1 - jambo/parser/__init__.py | 4 +- jambo/parser/_type_parser.py | 23 -- jambo/parser/oneof_type_parser.py | 69 ---- jambo/parser/string_type_parser.py | 9 +- tests/parser/test_oneof_type_parser.py | 493 ------------------------- 7 files changed, 3 insertions(+), 704 deletions(-) delete mode 100644 docs/source/usage.oneof.rst delete mode 100644 jambo/parser/oneof_type_parser.py delete mode 100644 tests/parser/test_oneof_type_parser.py diff --git a/docs/source/usage.oneof.rst b/docs/source/usage.oneof.rst deleted file mode 100644 index a836df4..0000000 --- a/docs/source/usage.oneof.rst +++ /dev/null @@ -1,108 +0,0 @@ -OneOf Type -================= - -The OneOf type is used to specify that an object must conform to exactly one of the specified schemas. Unlike AnyOf which allows matching multiple schemas, OneOf enforces that the data matches one and only one of the provided schemas. - - -Examples ------------------ - -1. **Overlapping String Example** - A field that accepts strings with overlapping constraints: - -.. code-block:: python - - from jambo import SchemaConverter - - schema = { - "title": "SimpleExample", - "type": "object", - "properties": { - "value": { - "oneOf": [ - {"type": "string", "maxLength": 6}, - {"type": "string", "minLength": 4} - ] - } - }, - "required": ["value"] - } - - Model = SchemaConverter.build(schema) - - # Valid: Short string (matches first schema only) - obj1 = Model(value="hi") - print(obj1.value) # Output: hi - - # Valid: Long string (matches second schema only) - obj2 = Model(value="very long string") - print(obj2.value) # Output: very long string - - # Invalid: Medium string (matches BOTH schemas - violates oneOf) - try: - obj3 = Model(value="hello") # 5 chars: matches maxLength=6 AND minLength=4 - except ValueError as e: - print("Validation fails as expected:", e) - - -2. **Discriminator Example** - Different shapes with a type field: - -.. code-block:: python - - from jambo import SchemaConverter - - schema = { - "title": "Shape", - "type": "object", - "properties": { - "shape": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": {"const": "circle"}, - "radius": {"type": "number", "minimum": 0} - }, - "required": ["type", "radius"] - }, - { - "type": "object", - "properties": { - "type": {"const": "rectangle"}, - "width": {"type": "number", "minimum": 0}, - "height": {"type": "number", "minimum": 0} - }, - "required": ["type", "width", "height"] - } - ], - "discriminator": { - "propertyName": "type" - } - } - }, - "required": ["shape"] - } - - Model = SchemaConverter.build(schema) - - # Valid: Circle - circle = Model(shape={"type": "circle", "radius": 5.0}) - print(circle.shape.type) # Output: circle - - # Valid: Rectangle - rectangle = Model(shape={"type": "rectangle", "width": 10, "height": 20}) - print(rectangle.shape.type) # Output: rectangle - - # Invalid: Wrong properties for the type - try: - invalid = Model(shape={"type": "circle", "width": 10}) - except ValueError as e: - print("Validation fails as expected:", e) - - -.. note:: - - OneOf ensures exactly one schema matches. The discriminator helps Pydantic efficiently determine which schema to use based on a specific property value. - -.. warning:: - - If your data could match multiple schemas in a oneOf, validation will fail. Ensure schemas are mutually exclusive. diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 3bdb2d9..8896842 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -45,6 +45,5 @@ For more complex schemas and types see our documentation on usage.reference usage.allof usage.anyof - usage.oneof usage.enum usage.const \ No newline at end of file diff --git a/jambo/parser/__init__.py b/jambo/parser/__init__.py index 44b4424..f3b8b25 100644 --- a/jambo/parser/__init__.py +++ b/jambo/parser/__init__.py @@ -9,7 +9,6 @@ from .float_type_parser import FloatTypeParser from .int_type_parser import IntTypeParser from .null_type_parser import NullTypeParser from .object_type_parser import ObjectTypeParser -from .oneof_type_parser import OneOfTypeParser from .ref_type_parser import RefTypeParser from .string_type_parser import StringTypeParser @@ -26,7 +25,6 @@ __all__ = [ "IntTypeParser", "NullTypeParser", "ObjectTypeParser", - "OneOfTypeParser", "StringTypeParser", "RefTypeParser", -] \ No newline at end of file +] diff --git a/jambo/parser/_type_parser.py b/jambo/parser/_type_parser.py index 6c5cdc9..080965c 100644 --- a/jambo/parser/_type_parser.py +++ b/jambo/parser/_type_parser.py @@ -124,26 +124,3 @@ class GenericTypeParser(ABC, Generic[T]): return False return True - - @staticmethod - def _has_meaningful_constraints(field_props): - """ - Check if field properties contain meaningful constraints that require Field wrapping. - - Returns False if: - - field_props is None or empty - - field_props only contains {'default': None} - - Returns True if: - - field_props contains a non-None default value - - field_props contains other constraint properties (min_length, max_length, pattern, etc.) - """ - if not field_props: - return False - - # If only default is set and it's None, no meaningful constraints - if field_props == {"default": None}: - return False - - # If there are multiple properties or non-None default, that's meaningful - return True diff --git a/jambo/parser/oneof_type_parser.py b/jambo/parser/oneof_type_parser.py deleted file mode 100644 index 79146b9..0000000 --- a/jambo/parser/oneof_type_parser.py +++ /dev/null @@ -1,69 +0,0 @@ -from jambo.parser._type_parser import GenericTypeParser -from jambo.types.type_parser_options import TypeParserOptions - -from pydantic import Field, BeforeValidator, TypeAdapter, ValidationError -from typing_extensions import Annotated, Union, Unpack, Any - - -class OneOfTypeParser(GenericTypeParser): - mapped_type = Union - - json_schema_type = "oneOf" - - def from_properties_impl( - self, name, properties, **kwargs: Unpack[TypeParserOptions] - ): - if "oneOf" not in properties: - raise ValueError(f"Invalid JSON Schema: {properties}") - - if not isinstance(properties["oneOf"], list): - raise ValueError(f"Invalid JSON Schema: {properties['oneOf']}") - - mapped_properties = self.mappings_properties_builder(properties, **kwargs) - - sub_properties = properties["oneOf"] - - sub_types = [ - GenericTypeParser.type_from_properties(name, subProperty, **kwargs) - for subProperty in sub_properties - ] - - if not kwargs.get("required", False): - mapped_properties["default"] = mapped_properties.get("default") - - field_types = [ - Annotated[t, Field(**v)] if self._has_meaningful_constraints(v) else t - for t, v in sub_types - ] - - union_type = Union[(*field_types,)] - - discriminator = properties.get("discriminator") - if discriminator and isinstance(discriminator, dict): - property_name = discriminator.get("propertyName") - if property_name: - validated_type = Annotated[union_type, Field(discriminator=property_name)] - return validated_type, mapped_properties - - def validate_one_of(value: Any) -> Any: - matched_count = 0 - validation_errors = [] - - for field_type in field_types: - try: - adapter = TypeAdapter(field_type) - adapter.validate_python(value) - matched_count += 1 - except ValidationError as e: - validation_errors.append(str(e)) - continue - - if matched_count == 0: - raise ValueError(f"Value does not match any of the oneOf schemas") - elif matched_count > 1: - raise ValueError(f"Value matches multiple oneOf schemas, exactly one expected") - - return value - - validated_type = Annotated[union_type, BeforeValidator(validate_one_of)] - return validated_type, mapped_properties diff --git a/jambo/parser/string_type_parser.py b/jambo/parser/string_type_parser.py index 9f43034..7e94b0d 100644 --- a/jambo/parser/string_type_parser.py +++ b/jambo/parser/string_type_parser.py @@ -16,6 +16,7 @@ class StringTypeParser(GenericTypeParser): "maxLength": "max_length", "minLength": "min_length", "pattern": "pattern", + "format": "format", } format_type_mapping = { @@ -37,9 +38,7 @@ class StringTypeParser(GenericTypeParser): def from_properties_impl( self, name, properties, **kwargs: Unpack[TypeParserOptions] ): - mapped_properties = self.mappings_properties_builder( - properties, **kwargs - ) + mapped_properties = self.mappings_properties_builder(properties, **kwargs) format_type = properties.get("format") if not format_type: @@ -52,8 +51,4 @@ class StringTypeParser(GenericTypeParser): if format_type in self.format_pattern_mapping: mapped_properties["pattern"] = self.format_pattern_mapping[format_type] - if "json_schema_extra" not in mapped_properties: - mapped_properties["json_schema_extra"] = {} - mapped_properties["json_schema_extra"]["format"] = format_type - return mapped_type, mapped_properties diff --git a/tests/parser/test_oneof_type_parser.py b/tests/parser/test_oneof_type_parser.py deleted file mode 100644 index 8c75f04..0000000 --- a/tests/parser/test_oneof_type_parser.py +++ /dev/null @@ -1,493 +0,0 @@ -from jambo import SchemaConverter - -from unittest import TestCase - - -class TestOneOfTypeParser(TestCase): - def test_oneof_basic_integer_and_string(self): - schema = { - "title": "Person", - "description": "A person with an ID that can be either an integer or a formatted string", - "type": "object", - "properties": { - "id": { - "oneOf": [ - {"type": "integer", "minimum": 1}, - {"type": "string", "pattern": "^[A-Z]{2}[0-9]{4}$"}, - ] - }, - }, - "required": ["id"], - } - - Model = SchemaConverter.build(schema) - - obj1 = Model(id=123) - self.assertEqual(obj1.id, 123) - - obj2 = Model(id="AB1234") - self.assertEqual(obj2.id, "AB1234") - - def test_oneof_validation_failures(self): - schema = { - "title": "Person", - "type": "object", - "properties": { - "id": { - "oneOf": [ - {"type": "integer", "minimum": 1}, - {"type": "string", "pattern": "^[A-Z]{2}[0-9]{4}$"}, - ] - }, - }, - "required": ["id"], - } - - Model = SchemaConverter.build(schema) - - with self.assertRaises(ValueError): - Model(id=-5) - - with self.assertRaises(ValueError): - Model(id="invalid") - - with self.assertRaises(ValueError): - Model(id=123.45) - - def test_oneof_with_conflicting_schemas(self): - schema = { - "title": "Value", - "type": "object", - "properties": { - "data": { - "oneOf": [ - {"type": "number", "multipleOf": 2}, - {"type": "number", "multipleOf": 3}, - ] - }, - }, - "required": ["data"], - } - - Model = SchemaConverter.build(schema) - - obj1 = Model(data=4) - self.assertEqual(obj1.data, 4) - - obj2 = Model(data=9) - self.assertEqual(obj2.data, 9) - - with self.assertRaises(ValueError) as cm: - Model(data=6) - self.assertIn("matches multiple oneOf schemas", str(cm.exception)) - - with self.assertRaises(ValueError): - Model(data=5) - - def test_oneof_with_objects(self): - schema = { - "title": "Contact", - "type": "object", - "properties": { - "contact_info": { - "oneOf": [ - { - "type": "object", - "properties": { - "email": {"type": "string", "format": "email"} - }, - "required": ["email"], - "additionalProperties": False - }, - { - "type": "object", - "properties": { - "phone": {"type": "string", "pattern": "^[0-9-]+$"} - }, - "required": ["phone"], - "additionalProperties": False - } - ] - }, - }, - "required": ["contact_info"], - } - - Model = SchemaConverter.build(schema) - - obj1 = Model(contact_info={"email": "user@example.com"}) - self.assertEqual(obj1.contact_info.email, "user@example.com") - - obj2 = Model(contact_info={"phone": "123-456-7890"}) - self.assertEqual(obj2.contact_info.phone, "123-456-7890") - - with self.assertRaises(ValueError): - Model(contact_info={"email": "user@example.com", "phone": "123-456-7890"}) - - def test_oneof_with_discriminator_basic(self): - schema = { - "title": "Pet", - "type": "object", - "properties": { - "pet": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": {"const": "cat"}, - "meows": {"type": "boolean"} - }, - "required": ["type", "meows"] - }, - { - "type": "object", - "properties": { - "type": {"const": "dog"}, - "barks": {"type": "boolean"} - }, - "required": ["type", "barks"] - } - ], - "discriminator": { - "propertyName": "type" - } - } - }, - "required": ["pet"] - } - - Model = SchemaConverter.build(schema) - - cat = Model(pet={"type": "cat", "meows": True}) - self.assertEqual(cat.pet.type, "cat") - self.assertEqual(cat.pet.meows, True) - - dog = Model(pet={"type": "dog", "barks": False}) - self.assertEqual(dog.pet.type, "dog") - self.assertEqual(dog.pet.barks, False) - - with self.assertRaises(ValueError): - Model(pet={"type": "cat", "barks": True}) - - with self.assertRaises(ValueError): - Model(pet={"type": "bird", "flies": True}) - - def test_oneof_with_discriminator_mapping(self): - schema = { - "title": "Vehicle", - "type": "object", - "properties": { - "vehicle": { - "oneOf": [ - { - "type": "object", - "properties": { - "vehicle_type": {"const": "car"}, - "doors": {"type": "integer", "minimum": 2, "maximum": 4} - }, - "required": ["vehicle_type", "doors"] - }, - { - "type": "object", - "properties": { - "vehicle_type": {"const": "motorcycle"}, - "engine_size": {"type": "number", "minimum": 125} - }, - "required": ["vehicle_type", "engine_size"] - } - ], - "discriminator": { - "propertyName": "vehicle_type", - "mapping": { - "car": "#/properties/vehicle/oneOf/0", - "motorcycle": "#/properties/vehicle/oneOf/1" - } - } - } - }, - "required": ["vehicle"] - } - - Model = SchemaConverter.build(schema) - - car = Model(vehicle={"vehicle_type": "car", "doors": 4}) - self.assertEqual(car.vehicle.vehicle_type, "car") - self.assertEqual(car.vehicle.doors, 4) - - motorcycle = Model(vehicle={"vehicle_type": "motorcycle", "engine_size": 600.0}) - self.assertEqual(motorcycle.vehicle.vehicle_type, "motorcycle") - self.assertEqual(motorcycle.vehicle.engine_size, 600.0) - - def test_oneof_with_discriminator_invalid_values(self): - schema = { - "title": "Shape", - "type": "object", - "properties": { - "shape": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": {"const": "circle"}, - "radius": {"type": "number", "minimum": 0} - }, - "required": ["type", "radius"] - }, - { - "type": "object", - "properties": { - "type": {"const": "square"}, - "side": {"type": "number", "minimum": 0} - }, - "required": ["type", "side"] - } - ], - "discriminator": { - "propertyName": "type" - } - } - }, - "required": ["shape"] - } - - Model = SchemaConverter.build(schema) - - with self.assertRaises(ValueError): - Model(shape={"type": "triangle", "base": 5, "height": 3}) - - with self.assertRaises(ValueError): - Model(shape={"type": "circle", "side": 5}) - - with self.assertRaises(ValueError): - Model(shape={"radius": 5}) - - def test_oneof_missing_properties(self): - schema = { - "title": "Test", - "type": "object", - "properties": { - "value": { - "notOneOf": [ - {"type": "string"}, - {"type": "integer"}, - ] - }, - }, - } - - with self.assertRaises(ValueError): - SchemaConverter.build(schema) - - def test_oneof_invalid_properties(self): - schema = { - "title": "Test", - "type": "object", - "properties": { - "value": { - "oneOf": None - }, - }, - } - - with self.assertRaises(ValueError): - SchemaConverter.build(schema) - - def test_oneof_with_default_value(self): - schema = { - "title": "Test", - "type": "object", - "properties": { - "value": { - "oneOf": [ - {"type": "string"}, - {"type": "integer"}, - ], - "default": "test" - }, - }, - } - - Model = SchemaConverter.build(schema) - obj = Model() - self.assertEqual(obj.value, "test") - - def test_oneof_with_invalid_default_value(self): - schema = { - "title": "Test", - "type": "object", - "properties": { - "value": { - "oneOf": [ - {"type": "string", "minLength": 5}, - {"type": "integer", "minimum": 10}, - ], - "default": "hi" - }, - }, - } - - with self.assertRaises(ValueError): - SchemaConverter.build(schema) - - def test_oneof_discriminator_without_property_name(self): - schema = { - "title": "Test", - "type": "object", - "properties": { - "value": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": {"const": "a"}, - "value": {"type": "string"} - } - }, - { - "type": "object", - "properties": { - "type": {"const": "b"}, - "value": {"type": "integer"} - } - } - ], - "discriminator": {} # discriminator without propertyName - } - } - } - - Model = SchemaConverter.build(schema) - - # Should succeed because input matches exactly one schema (the first one) - # The first schema matches: type="a" matches const("a"), value="test" is a string - # The second schema doesn't match: type="a" does not match const("b") - obj = Model(value={"type": "a", "value": "test", "extra": "invalid"}) - self.assertEqual(obj.value.type, "a") - self.assertEqual(obj.value.value, "test") - - # Test with input that matches the second schema - obj2 = Model(value={"type": "b", "value": 42}) - self.assertEqual(obj2.value.type, "b") - self.assertEqual(obj2.value.value, 42) - - # Test with input that matches neither schema (should fail) - with self.assertRaises(ValueError) as cm: - Model(value={"type": "c", "value": "test"}) - self.assertIn("does not match any of the oneOf schemas", str(cm.exception)) - - def test_oneof_multiple_matches_without_discriminator(self): - """Test case where input genuinely matches multiple oneOf schemas""" - schema = { - "title": "Test", - "type": "object", - "properties": { - "value": { - "oneOf": [ - { - "type": "object", - "properties": { - "data": {"type": "string"} - } - }, - { - "type": "object", - "properties": { - "data": {"type": "string"}, - "optional": {"type": "string"} - } - } - ], - "discriminator": {} # discriminator without propertyName - } - } - } - - Model = SchemaConverter.build(schema) - - # This input matches both schemas since both accept data as string - # and neither requires specific additional properties - with self.assertRaises(ValueError) as cm: - Model(value={"data": "test"}) - self.assertIn("matches multiple oneOf schemas", str(cm.exception)) - - def test_oneof_overlapping_strings_from_docs(self): - """Test the overlapping strings example from documentation""" - schema = { - "title": "SimpleExample", - "type": "object", - "properties": { - "value": { - "oneOf": [ - {"type": "string", "maxLength": 6}, - {"type": "string", "minLength": 4} - ] - } - }, - "required": ["value"] - } - - Model = SchemaConverter.build(schema) - - # Valid: Short string (matches first schema only) - obj1 = Model(value="hi") - self.assertEqual(obj1.value, "hi") - - # Valid: Long string (matches second schema only) - obj2 = Model(value="very long string") - self.assertEqual(obj2.value, "very long string") - - # Invalid: Medium string (matches BOTH schemas - violates oneOf) - with self.assertRaises(ValueError) as cm: - Model(value="hello") # 5 chars: matches maxLength=6 AND minLength=4 - self.assertIn("matches multiple oneOf schemas", str(cm.exception)) - - def test_oneof_shapes_discriminator_from_docs(self): - """Test the shapes discriminator example from documentation""" - schema = { - "title": "Shape", - "type": "object", - "properties": { - "shape": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": {"const": "circle"}, - "radius": {"type": "number", "minimum": 0} - }, - "required": ["type", "radius"] - }, - { - "type": "object", - "properties": { - "type": {"const": "rectangle"}, - "width": {"type": "number", "minimum": 0}, - "height": {"type": "number", "minimum": 0} - }, - "required": ["type", "width", "height"] - } - ], - "discriminator": { - "propertyName": "type" - } - } - }, - "required": ["shape"] - } - - Model = SchemaConverter.build(schema) - - # Valid: Circle - circle = Model(shape={"type": "circle", "radius": 5.0}) - self.assertEqual(circle.shape.type, "circle") - self.assertEqual(circle.shape.radius, 5.0) - - # Valid: Rectangle - rectangle = Model(shape={"type": "rectangle", "width": 10, "height": 20}) - self.assertEqual(rectangle.shape.type, "rectangle") - self.assertEqual(rectangle.shape.width, 10) - self.assertEqual(rectangle.shape.height, 20) - - # Invalid: Wrong properties for the type - with self.assertRaises(ValueError): - Model(shape={"type": "circle", "width": 10})