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/const_type_parser.py b/jambo/parser/const_type_parser.py index 1e4ce84..b5c846f 100644 --- a/jambo/parser/const_type_parser.py +++ b/jambo/parser/const_type_parser.py @@ -3,7 +3,7 @@ from jambo.types.json_schema_type import JSONSchemaNativeTypes from jambo.types.type_parser_options import TypeParserOptions from pydantic import AfterValidator -from typing_extensions import Annotated, Any, Literal, Unpack +from typing_extensions import Annotated, Any, Unpack class ConstTypeParser(GenericTypeParser): @@ -33,19 +33,11 @@ class ConstTypeParser(GenericTypeParser): return const_type, parsed_properties def _build_const_type(self, const_value): - # Try to use Literal for hashable types (required for discriminated unions) - # Fall back to validator approach for non-hashable types - try: - # Test if the value is hashable (can be used in Literal) - hash(const_value) - return Literal[const_value] - except TypeError: - # Non-hashable type (like list, dict), use validator approach - def _validate_const_value(value: Any) -> Any: - if value != const_value: - raise ValueError( - f"Value must be equal to the constant value: {const_value}" - ) - return value + def _validate_const_value(value: Any) -> Any: + if value != const_value: + raise ValueError( + f"Value must be equal to the constant value: {const_value}" + ) + return value - return Annotated[type(const_value), AfterValidator(_validate_const_value)] \ No newline at end of file + return Annotated[type(const_value), AfterValidator(_validate_const_value)] diff --git a/jambo/parser/oneof_type_parser.py b/jambo/parser/oneof_type_parser.py index 79146b9..c0eeecc 100644 --- a/jambo/parser/oneof_type_parser.py +++ b/jambo/parser/oneof_type_parser.py @@ -1,8 +1,8 @@ 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 +from pydantic import BeforeValidator, Field, TypeAdapter, ValidationError +from typing_extensions import Annotated, Any, Union, Unpack class OneOfTypeParser(GenericTypeParser): @@ -11,7 +11,7 @@ class OneOfTypeParser(GenericTypeParser): json_schema_type = "oneOf" def from_properties_impl( - self, name, properties, **kwargs: Unpack[TypeParserOptions] + self, name, properties, **kwargs: Unpack[TypeParserOptions] ): if "oneOf" not in properties: raise ValueError(f"Invalid JSON Schema: {properties}") @@ -42,7 +42,9 @@ class OneOfTypeParser(GenericTypeParser): if discriminator and isinstance(discriminator, dict): property_name = discriminator.get("propertyName") if property_name: - validated_type = Annotated[union_type, Field(discriminator=property_name)] + validated_type = Annotated[ + union_type, Field(discriminator=property_name) + ] return validated_type, mapped_properties def validate_one_of(value: Any) -> Any: @@ -59,11 +61,34 @@ class OneOfTypeParser(GenericTypeParser): continue if matched_count == 0: - raise ValueError(f"Value does not match any of the oneOf schemas") + raise ValueError("Value does not match any of the oneOf schemas") elif matched_count > 1: - raise ValueError(f"Value matches multiple oneOf schemas, exactly one expected") + raise ValueError( + "Value matches multiple oneOf schemas, exactly one expected" + ) return value validated_type = Annotated[union_type, BeforeValidator(validate_one_of)] return validated_type, mapped_properties + + @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/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_const_type_parser.py b/tests/parser/test_const_type_parser.py index 5a8c9c1..ca92bb0 100644 --- a/tests/parser/test_const_type_parser.py +++ b/tests/parser/test_const_type_parser.py @@ -1,13 +1,12 @@ from jambo.parser import ConstTypeParser -from typing_extensions import Annotated, Literal, get_args, get_origin +from typing_extensions import Annotated, get_args, get_origin from unittest import TestCase class TestConstTypeParser(TestCase): - def test_const_type_parser_hashable_value(self): - """Test const parser with hashable values (uses Literal)""" + def test_const_type_parser(self): parser = ConstTypeParser() expected_const_value = "United States of America" @@ -17,60 +16,8 @@ class TestConstTypeParser(TestCase): "country", properties ) - # Check that we get a Literal type for hashable values - self.assertEqual(get_origin(parsed_type), Literal) - self.assertEqual(get_args(parsed_type), (expected_const_value,)) - - self.assertEqual(parsed_properties["default"], expected_const_value) - - def test_const_type_parser_non_hashable_value(self): - """Test const parser with non-hashable values (uses Annotated with validator)""" - parser = ConstTypeParser() - - expected_const_value = [1, 2, 3] # Lists are not hashable - properties = {"const": expected_const_value} - - parsed_type, parsed_properties = parser.from_properties_impl( - "list_const", properties - ) - - # Check that we get an Annotated type for non-hashable values self.assertEqual(get_origin(parsed_type), Annotated) - self.assertIn(list, get_args(parsed_type)) - - self.assertEqual(parsed_properties["default"], expected_const_value) - - def test_const_type_parser_integer_value(self): - """Test const parser with integer values (uses Literal)""" - parser = ConstTypeParser() - - expected_const_value = 42 - properties = {"const": expected_const_value} - - parsed_type, parsed_properties = parser.from_properties_impl( - "int_const", properties - ) - - # Check that we get a Literal type for hashable values - self.assertEqual(get_origin(parsed_type), Literal) - self.assertEqual(get_args(parsed_type), (expected_const_value,)) - - self.assertEqual(parsed_properties["default"], expected_const_value) - - def test_const_type_parser_boolean_value(self): - """Test const parser with boolean values (uses Literal)""" - parser = ConstTypeParser() - - expected_const_value = True - properties = {"const": expected_const_value} - - parsed_type, parsed_properties = parser.from_properties_impl( - "bool_const", properties - ) - - # Check that we get a Literal type for hashable values - self.assertEqual(get_origin(parsed_type), Literal) - self.assertEqual(get_args(parsed_type), (expected_const_value,)) + self.assertIn(str, get_args(parsed_type)) self.assertEqual(parsed_properties["default"], expected_const_value) @@ -99,4 +46,4 @@ class TestConstTypeParser(TestCase): self.assertIn( "Const type invalid_country must have 'const' value of allowed types", str(context.exception), - ) \ No newline at end of file + ) diff --git a/tests/parser/test_oneof_type_parser.py b/tests/parser/test_oneof_type_parser.py index 8c75f04..fbe362c 100644 --- a/tests/parser/test_oneof_type_parser.py +++ b/tests/parser/test_oneof_type_parser.py @@ -97,7 +97,7 @@ class TestOneOfTypeParser(TestCase): "email": {"type": "string", "format": "email"} }, "required": ["email"], - "additionalProperties": False + "additionalProperties": False, }, { "type": "object", @@ -105,8 +105,8 @@ class TestOneOfTypeParser(TestCase): "phone": {"type": "string", "pattern": "^[0-9-]+$"} }, "required": ["phone"], - "additionalProperties": False - } + "additionalProperties": False, + }, ] }, }, @@ -135,25 +135,23 @@ class TestOneOfTypeParser(TestCase): "type": "object", "properties": { "type": {"const": "cat"}, - "meows": {"type": "boolean"} + "meows": {"type": "boolean"}, }, - "required": ["type", "meows"] + "required": ["type", "meows"], }, { "type": "object", "properties": { "type": {"const": "dog"}, - "barks": {"type": "boolean"} + "barks": {"type": "boolean"}, }, - "required": ["type", "barks"] - } + "required": ["type", "barks"], + }, ], - "discriminator": { - "propertyName": "type" - } + "discriminator": {"propertyName": "type"}, } }, - "required": ["pet"] + "required": ["pet"], } Model = SchemaConverter.build(schema) @@ -183,29 +181,33 @@ class TestOneOfTypeParser(TestCase): "type": "object", "properties": { "vehicle_type": {"const": "car"}, - "doors": {"type": "integer", "minimum": 2, "maximum": 4} + "doors": { + "type": "integer", + "minimum": 2, + "maximum": 4, + }, }, - "required": ["vehicle_type", "doors"] + "required": ["vehicle_type", "doors"], }, { "type": "object", "properties": { "vehicle_type": {"const": "motorcycle"}, - "engine_size": {"type": "number", "minimum": 125} + "engine_size": {"type": "number", "minimum": 125}, }, - "required": ["vehicle_type", "engine_size"] - } + "required": ["vehicle_type", "engine_size"], + }, ], "discriminator": { "propertyName": "vehicle_type", "mapping": { "car": "#/properties/vehicle/oneOf/0", - "motorcycle": "#/properties/vehicle/oneOf/1" - } - } + "motorcycle": "#/properties/vehicle/oneOf/1", + }, + }, } }, - "required": ["vehicle"] + "required": ["vehicle"], } Model = SchemaConverter.build(schema) @@ -229,25 +231,23 @@ class TestOneOfTypeParser(TestCase): "type": "object", "properties": { "type": {"const": "circle"}, - "radius": {"type": "number", "minimum": 0} + "radius": {"type": "number", "minimum": 0}, }, - "required": ["type", "radius"] + "required": ["type", "radius"], }, { "type": "object", "properties": { "type": {"const": "square"}, - "side": {"type": "number", "minimum": 0} + "side": {"type": "number", "minimum": 0}, }, - "required": ["type", "side"] - } + "required": ["type", "side"], + }, ], - "discriminator": { - "propertyName": "type" - } + "discriminator": {"propertyName": "type"}, } }, - "required": ["shape"] + "required": ["shape"], } Model = SchemaConverter.build(schema) @@ -283,9 +283,7 @@ class TestOneOfTypeParser(TestCase): "title": "Test", "type": "object", "properties": { - "value": { - "oneOf": None - }, + "value": {"oneOf": None}, }, } @@ -302,7 +300,7 @@ class TestOneOfTypeParser(TestCase): {"type": "string"}, {"type": "integer"}, ], - "default": "test" + "default": "test", }, }, } @@ -321,7 +319,7 @@ class TestOneOfTypeParser(TestCase): {"type": "string", "minLength": 5}, {"type": "integer", "minimum": 10}, ], - "default": "hi" + "default": "hi", }, }, } @@ -340,20 +338,20 @@ class TestOneOfTypeParser(TestCase): "type": "object", "properties": { "type": {"const": "a"}, - "value": {"type": "string"} - } + "value": {"type": "string"}, + }, }, { "type": "object", "properties": { "type": {"const": "b"}, - "value": {"type": "integer"} - } - } + "value": {"type": "integer"}, + }, + }, ], - "discriminator": {} # discriminator without propertyName + "discriminator": {}, # discriminator without propertyName } - } + }, } Model = SchemaConverter.build(schema) @@ -383,23 +381,18 @@ class TestOneOfTypeParser(TestCase): "properties": { "value": { "oneOf": [ - { - "type": "object", - "properties": { - "data": {"type": "string"} - } - }, + {"type": "object", "properties": {"data": {"type": "string"}}}, { "type": "object", "properties": { "data": {"type": "string"}, - "optional": {"type": "string"} - } - } + "optional": {"type": "string"}, + }, + }, ], - "discriminator": {} # discriminator without propertyName + "discriminator": {}, # discriminator without propertyName } - } + }, } Model = SchemaConverter.build(schema) @@ -419,11 +412,11 @@ class TestOneOfTypeParser(TestCase): "value": { "oneOf": [ {"type": "string", "maxLength": 6}, - {"type": "string", "minLength": 4} + {"type": "string", "minLength": 4}, ] } }, - "required": ["value"] + "required": ["value"], } Model = SchemaConverter.build(schema) @@ -453,26 +446,24 @@ class TestOneOfTypeParser(TestCase): "type": "object", "properties": { "type": {"const": "circle"}, - "radius": {"type": "number", "minimum": 0} + "radius": {"type": "number", "minimum": 0}, }, - "required": ["type", "radius"] + "required": ["type", "radius"], }, { "type": "object", "properties": { "type": {"const": "rectangle"}, "width": {"type": "number", "minimum": 0}, - "height": {"type": "number", "minimum": 0} + "height": {"type": "number", "minimum": 0}, }, - "required": ["type", "width", "height"] - } + "required": ["type", "width", "height"], + }, ], - "discriminator": { - "propertyName": "type" - } + "discriminator": {"propertyName": "type"}, } }, - "required": ["shape"] + "required": ["shape"], } Model = SchemaConverter.build(schema)