From e61d48881f39d54a2ceba50138b74450ef2c36af Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Sat, 13 Sep 2025 20:43:30 -0300 Subject: [PATCH 1/6] feat: initial implementation of explicit exception types --- jambo/exceptions/__init__.py | 5 ++++ jambo/exceptions/invalid_schema_exception.py | 27 +++++++++++++++++++ .../unsupported_schema_exception.py | 23 ++++++++++++++++ jambo/schema_converter.py | 21 +++++++++++---- jambo/types/__init__.py | 16 +++++++++++ 5 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 jambo/exceptions/__init__.py create mode 100644 jambo/exceptions/invalid_schema_exception.py create mode 100644 jambo/exceptions/unsupported_schema_exception.py diff --git a/jambo/exceptions/__init__.py b/jambo/exceptions/__init__.py new file mode 100644 index 0000000..0292136 --- /dev/null +++ b/jambo/exceptions/__init__.py @@ -0,0 +1,5 @@ +from .invalid_schema_exception import InvalidSchemaException +from .unsupported_schema_exception import UnsupportedSchemaException + + +__all__ = ["InvalidSchemaException", "UnsupportedSchemaException"] diff --git a/jambo/exceptions/invalid_schema_exception.py b/jambo/exceptions/invalid_schema_exception.py new file mode 100644 index 0000000..2d7acff --- /dev/null +++ b/jambo/exceptions/invalid_schema_exception.py @@ -0,0 +1,27 @@ +from typing_extensions import Optional + + +class InvalidSchemaException(ValueError): + """Exception raised for invalid JSON schemas.""" + + def __init__( + self, + message: str, + invalid_field: Optional[str] = None, + cause: Optional[BaseException] = None, + ) -> None: + # Normalize message by stripping redundant prefix if present + message = message.removeprefix("Invalid JSON Schema: ") + self.invalid_field = invalid_field + self.cause = cause + super().__init__(message) + + def __str__(self) -> str: + base_msg = f"Invalid JSON Schema: {super().__str__()}" + if self.invalid_field: + return f"{base_msg} (invalid field: {self.invalid_field})" + if self.cause: + return ( + f"{base_msg} (caused by {self.cause.__class__.__name__}: {self.cause})" + ) + return base_msg diff --git a/jambo/exceptions/unsupported_schema_exception.py b/jambo/exceptions/unsupported_schema_exception.py new file mode 100644 index 0000000..14bf65a --- /dev/null +++ b/jambo/exceptions/unsupported_schema_exception.py @@ -0,0 +1,23 @@ +from typing_extensions import Optional + + +class UnsupportedSchemaException(ValueError): + """Exception raised for unsupported JSON schemas.""" + + def __init__( + self, + message: str, + unsupported_field: Optional[str] = None, + cause: Optional[BaseException] = None, + ) -> None: + # Normalize message by stripping redundant prefix if present + message = message.removeprefix("Unsupported JSON Schema: ") + self.unsupported_field = unsupported_field + self.cause = cause + super().__init__(message) + + def __str__(self) -> str: + base_msg = f"Unsupported JSON Schema: {super().__str__()}" + if self.unsupported_field: + return f"{base_msg} (unsupported field: {self.unsupported_field})" + return base_msg diff --git a/jambo/schema_converter.py b/jambo/schema_converter.py index 1624940..3d38dc7 100644 --- a/jambo/schema_converter.py +++ b/jambo/schema_converter.py @@ -1,5 +1,6 @@ +from jambo.exceptions import InvalidSchemaException, UnsupportedSchemaException from jambo.parser import ObjectTypeParser, RefTypeParser -from jambo.types.json_schema_type import JSONSchema +from jambo.types import JSONSchema from jsonschema.exceptions import SchemaError from jsonschema.validators import validator_for @@ -26,11 +27,15 @@ class SchemaConverter: try: validator = validator_for(schema) validator.check_schema(schema) # type: ignore - except SchemaError as e: - raise ValueError(f"Invalid JSON Schema: {e}") + except SchemaError as err: + raise InvalidSchemaException( + "Validation of JSON Schema failed.", cause=err + ) from err if "title" not in schema: - raise ValueError("JSON Schema must have a title.") + raise InvalidSchemaException( + "Schema must have a title.", invalid_field="title" + ) schema_type = SchemaConverter._get_schema_type(schema) @@ -55,7 +60,13 @@ class SchemaConverter: ) return parsed_model case _: - raise TypeError(f"Unsupported schema type: {schema_type}") + unsupported_type = ( + f"type:{schema_type}" if schema_type else "missing type" + ) + raise UnsupportedSchemaException( + "Only object and $ref schema types are supported.", + unsupported_field=unsupported_type, + ) @staticmethod def _get_schema_type(schema: JSONSchema) -> str | None: diff --git a/jambo/types/__init__.py b/jambo/types/__init__.py index e69de29..d5c88a2 100644 --- a/jambo/types/__init__.py +++ b/jambo/types/__init__.py @@ -0,0 +1,16 @@ +from .json_schema_type import ( + JSONSchema, + JSONSchemaNativeTypes, + JSONSchemaType, + JSONType, +) +from .type_parser_options import TypeParserOptions + + +__all__ = [ + "JSONSchemaType", + "JSONSchemaNativeTypes", + "JSONType", + "JSONSchema", + "TypeParserOptions", +] From f4d84d27491dd84b1fa0c99ac86f407b5cc2850e Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Sat, 13 Sep 2025 21:10:14 -0300 Subject: [PATCH 2/6] feat: better exceptions for GenericTypeParser and AllOfTypeParser --- jambo/parser/_type_parser.py | 9 ++++++--- jambo/parser/allof_type_parser.py | 14 ++++++++++---- tests/test_schema_converter.py | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/jambo/parser/_type_parser.py b/jambo/parser/_type_parser.py index cce8042..cb7885e 100644 --- a/jambo/parser/_type_parser.py +++ b/jambo/parser/_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.types.type_parser_options import JSONSchema, TypeParserOptions from pydantic import Field, TypeAdapter @@ -46,8 +47,8 @@ class GenericTypeParser(ABC, Generic[T]): ) 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__}" + raise InvalidSchemaException( + "Default value is not valid", invalid_field=name ) return parsed_type, parsed_properties @@ -79,7 +80,9 @@ class GenericTypeParser(ABC, Generic[T]): if schema_value is None or schema_value == properties[schema_type]: # type: ignore return subcls - raise ValueError("Unknown type") + raise InvalidSchemaException( + "No suitable type parser found", invalid_field=str(properties) + ) @classmethod def _get_schema_type(cls) -> tuple[str, str | None]: diff --git a/jambo/parser/allof_type_parser.py b/jambo/parser/allof_type_parser.py index 2fb62f9..92c5ff2 100644 --- a/jambo/parser/allof_type_parser.py +++ b/jambo/parser/allof_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser._type_parser import GenericTypeParser from jambo.types.json_schema_type import JSONSchema from jambo.types.type_parser_options import TypeParserOptions @@ -33,13 +34,18 @@ class AllOfTypeParser(GenericTypeParser): sub_properties: list[JSONSchema], ) -> type[GenericTypeParser]: if not sub_properties: - raise ValueError("Invalid JSON Schema: 'allOf' is empty.") + raise InvalidSchemaException( + "'allOf' must contain at least one schema", invalid_field="allOf" + ) parsers: set[type[GenericTypeParser]] = set( GenericTypeParser._get_impl(sub_property) for sub_property in sub_properties ) if len(parsers) != 1: - raise ValueError("Invalid JSON Schema: allOf types do not match.") + raise InvalidSchemaException( + "All sub-schemas in 'allOf' must resolve to the same type", + invalid_field="allOf", + ) return parsers.pop() @@ -68,8 +74,8 @@ class AllOfTypeParser(GenericTypeParser): if prop_name == "default": if old_value != new_value: - raise ValueError( - f"Invalid JSON Schema: conflicting defaults for '{prop_name}'" + raise InvalidSchemaException( + f"Conflicting defaults for '{prop_name}'", invalid_field=prop_name ) return old_value diff --git a/tests/test_schema_converter.py b/tests/test_schema_converter.py index 6f831f2..2d4099b 100644 --- a/tests/test_schema_converter.py +++ b/tests/test_schema_converter.py @@ -60,7 +60,7 @@ class TestSchemaConverter(TestCase): "type": "string", } - with self.assertRaises(TypeError): + with self.assertRaises(ValueError): SchemaConverter.build(schema) def test_is_invalid_field(self): From 30290771b193fe0976f92191bbf53018fb457d4b Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Sun, 14 Sep 2025 00:05:21 -0300 Subject: [PATCH 3/6] feat: alters all standart errors and messages for more specific errors --- jambo/parser/anyof_type_parser.py | 10 +++++-- jambo/parser/array_type_parser.py | 15 ++++++++-- jambo/parser/boolean_type_parser.py | 6 +++- jambo/parser/const_type_parser.py | 11 +++++-- jambo/parser/enum_type_parser.py | 16 ++++++++--- jambo/parser/oneof_type_parser.py | 23 +++++++++++---- jambo/parser/ref_type_parser.py | 38 +++++++++++++++---------- jambo/parser/string_type_parser.py | 5 +++- jambo/types/json_schema_type.py | 2 +- tests/parser/test_ref_type_parser.py | 4 +-- tests/parser/test_string_type_parser.py | 3 +- 11 files changed, 94 insertions(+), 39 deletions(-) diff --git a/jambo/parser/anyof_type_parser.py b/jambo/parser/anyof_type_parser.py index 55ff3ec..4b961ac 100644 --- a/jambo/parser/anyof_type_parser.py +++ b/jambo/parser/anyof_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser._type_parser import GenericTypeParser from jambo.types.type_parser_options import TypeParserOptions @@ -14,10 +15,15 @@ class AnyOfTypeParser(GenericTypeParser): self, name, properties, **kwargs: Unpack[TypeParserOptions] ): if "anyOf" not in properties: - raise ValueError(f"Invalid JSON Schema: {properties}") + raise InvalidSchemaException( + f"AnyOf type {name} must have 'anyOf' property defined.", + invalid_field="anyOf", + ) if not isinstance(properties["anyOf"], list): - raise ValueError(f"Invalid JSON Schema: {properties['anyOf']}") + raise InvalidSchemaException( + "AnyOf must be a list of types.", invalid_field="anyOf" + ) mapped_properties = self.mappings_properties_builder(properties, **kwargs) diff --git a/jambo/parser/array_type_parser.py b/jambo/parser/array_type_parser.py index 7d59ea5..790a65a 100644 --- a/jambo/parser/array_type_parser.py +++ b/jambo/parser/array_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser._type_parser import GenericTypeParser from jambo.types.type_parser_options import TypeParserOptions @@ -26,8 +27,15 @@ class ArrayTypeParser(GenericTypeParser): ): item_properties = kwargs.copy() item_properties["required"] = True + + if (items := properties.get("items")) is None: + raise InvalidSchemaException( + f"Array type {name} must have 'items' property defined.", + invalid_field="items", + ) + _item_type, _item_args = GenericTypeParser.type_from_properties( - name, properties["items"], **item_properties + name, items, **item_properties ) wrapper_type = set if properties.get("uniqueItems", False) else list @@ -47,8 +55,9 @@ class ArrayTypeParser(GenericTypeParser): return lambda: None if not isinstance(default_list, Iterable): - raise ValueError( - f"Default value for array must be an iterable, got {type(default_list)}" + raise InvalidSchemaException( + f"Default value for array must be an iterable, got {type(default_list)}", + invalid_field="default", ) 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 ecb703a..d948069 100644 --- a/jambo/parser/boolean_type_parser.py +++ b/jambo/parser/boolean_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser._type_parser import GenericTypeParser from jambo.types.type_parser_options import TypeParserOptions @@ -20,6 +21,9 @@ class BooleanTypeParser(GenericTypeParser): default_value = properties.get("default") if default_value is not None and not isinstance(default_value, bool): - raise ValueError(f"Default value for {name} must be a boolean.") + raise InvalidSchemaException( + f"Default value for {name} must be a boolean.", + invalid_field="default", + ) return bool, mapped_properties diff --git a/jambo/parser/const_type_parser.py b/jambo/parser/const_type_parser.py index da55bc0..76c6893 100644 --- a/jambo/parser/const_type_parser.py +++ b/jambo/parser/const_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser._type_parser import GenericTypeParser from jambo.types.json_schema_type import JSONSchemaNativeTypes from jambo.types.type_parser_options import TypeParserOptions @@ -18,13 +19,17 @@ class ConstTypeParser(GenericTypeParser): self, name, properties, **kwargs: Unpack[TypeParserOptions] ): if "const" not in properties: - raise ValueError(f"Const type {name} must have 'const' property defined.") + raise InvalidSchemaException( + f"Const type {name} must have 'const' property defined.", + invalid_field="const", + ) const_value = properties["const"] if not isinstance(const_value, JSONSchemaNativeTypes): - raise ValueError( - f"Const type {name} must have 'const' value of allowed types: {JSONSchemaNativeTypes}." + raise InvalidSchemaException( + f"Const type {name} must have 'const' value of allowed types: {JSONSchemaNativeTypes}.", + invalid_field="const", ) const_type = self._build_const_type(const_value) diff --git a/jambo/parser/enum_type_parser.py b/jambo/parser/enum_type_parser.py index c59a725..f0b001f 100644 --- a/jambo/parser/enum_type_parser.py +++ b/jambo/parser/enum_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser._type_parser import GenericTypeParser from jambo.types.json_schema_type import JSONSchemaNativeTypes from jambo.types.type_parser_options import JSONSchema, TypeParserOptions @@ -14,16 +15,23 @@ class EnumTypeParser(GenericTypeParser): self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions] ): if "enum" not in properties: - raise ValueError(f"Enum type {name} must have 'enum' property defined.") + raise InvalidSchemaException( + f"Enum type {name} must have 'enum' property defined.", + invalid_field="enum", + ) enum_values = properties["enum"] if not isinstance(enum_values, list): - raise ValueError(f"Enum type {name} must have 'enum' as a list of values.") + raise InvalidSchemaException( + f"Enum type {name} must have 'enum' as a list of values.", + invalid_field="enum", + ) if any(not isinstance(value, JSONSchemaNativeTypes) for value in enum_values): - raise ValueError( - f"Enum type {name} must have 'enum' values of allowed types: {JSONSchemaNativeTypes}." + raise InvalidSchemaException( + f"Enum type {name} must have 'enum' values of allowed types: {JSONSchemaNativeTypes}.", + invalid_field="enum", ) # Create a new Enum type dynamically diff --git a/jambo/parser/oneof_type_parser.py b/jambo/parser/oneof_type_parser.py index 317ce61..4713d9c 100644 --- a/jambo/parser/oneof_type_parser.py +++ b/jambo/parser/oneof_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser._type_parser import GenericTypeParser from jambo.types.type_parser_options import TypeParserOptions @@ -17,10 +18,14 @@ class OneOfTypeParser(GenericTypeParser): self, name, properties, **kwargs: Unpack[TypeParserOptions] ): if "oneOf" not in properties: - raise ValueError(f"Invalid JSON Schema: {properties}") + raise InvalidSchemaException( + f"Invalid JSON Schema: {properties}", invalid_field="oneOf" + ) if not isinstance(properties["oneOf"], list) or len(properties["oneOf"]) == 0: - raise ValueError(f"Invalid JSON Schema: {properties['oneOf']}") + raise InvalidSchemaException( + f"Invalid JSON Schema: {properties['oneOf']}", invalid_field="oneOf" + ) mapped_properties = self.mappings_properties_builder(properties, **kwargs) @@ -58,7 +63,9 @@ class OneOfTypeParser(GenericTypeParser): Build a type with a discriminator. """ if not isinstance(discriminator_prop, dict): - raise ValueError("Discriminator must be a dictionary") + raise InvalidSchemaException( + "Discriminator must be a dictionary", invalid_field="discriminator" + ) for field in subfield_types: field_type, field_info = get_args(field) @@ -66,13 +73,17 @@ class OneOfTypeParser(GenericTypeParser): if issubclass(field_type, BaseModel): continue - raise ValueError( - "When using a discriminator, all subfield types must be of type 'object'." + raise InvalidSchemaException( + "When using a discriminator, all subfield types must be of type 'object'.", + invalid_field="discriminator", ) property_name = discriminator_prop.get("propertyName") if property_name is None or not isinstance(property_name, str): - raise ValueError("Discriminator must have a 'propertyName' key") + raise InvalidSchemaException( + "Discriminator must have a 'propertyName' key", + invalid_field="propertyName", + ) return Annotated[Union[(*subfield_types,)], Field(discriminator=property_name)] diff --git a/jambo/parser/ref_type_parser.py b/jambo/parser/ref_type_parser.py index 7aa435d..615283e 100644 --- a/jambo/parser/ref_type_parser.py +++ b/jambo/parser/ref_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser import GenericTypeParser from jambo.types.json_schema_type import JSONSchema from jambo.types.type_parser_options import TypeParserOptions @@ -17,18 +18,21 @@ class RefTypeParser(GenericTypeParser): self, name: str, properties: JSONSchema, **kwargs: Unpack[TypeParserOptions] ) -> tuple[RefType, dict]: if "$ref" not in properties: - raise ValueError(f"RefTypeParser: Missing $ref in properties for {name}") + raise InvalidSchemaException( + f"Missing $ref in properties for {name}", invalid_field="$ref" + ) context = kwargs.get("context") if context is None: - raise RuntimeError( - f"RefTypeParser: Missing `content` in properties for {name}" + raise InvalidSchemaException( + f"Missing `context` in properties for {name}", invalid_field="context" ) ref_cache = kwargs.get("ref_cache") if ref_cache is None: - raise RuntimeError( - f"RefTypeParser: Missing `ref_cache` in properties for {name}" + raise InvalidSchemaException( + f"Missing `ref_cache` in properties for {name}", + invalid_field="ref_cache", ) mapped_properties = self.mappings_properties_builder(properties, **kwargs) @@ -63,8 +67,8 @@ class RefTypeParser(GenericTypeParser): ref_name, ref_property, **kwargs ) case _: - raise ValueError( - f"RefTypeParser: Unsupported $ref {ref_property['$ref']}" + raise InvalidSchemaException( + f"Unsupported $ref {ref_property['$ref']}", invalid_field="$ref" ) return mapped_type @@ -93,8 +97,9 @@ class RefTypeParser(GenericTypeParser): if properties.get("$ref") == "#": ref_name = kwargs["context"].get("title") if ref_name is None: - raise ValueError( - "RefTypeParser: Missing title in properties for $ref of Root Reference" + raise InvalidSchemaException( + "Missing title in properties for $ref of Root Reference", + invalid_field="title", ) return "forward_ref", ref_name, {} @@ -104,8 +109,9 @@ class RefTypeParser(GenericTypeParser): ) return "def_ref", target_name, target_property - raise ValueError( - "RefTypeParser: Only Root and $defs references are supported at the moment" + raise InvalidSchemaException( + "Only Root and $defs references are supported at the moment", + invalid_field="$ref", ) def _extract_target_ref( @@ -115,14 +121,16 @@ class RefTypeParser(GenericTypeParser): target_property = kwargs["context"] for prop_name in properties["$ref"].split("/")[1:]: if prop_name not in target_property: - raise ValueError( - f"RefTypeParser: Missing {prop_name} in" - " properties for $ref {properties['$ref']}" + raise InvalidSchemaException( + f"Missing {prop_name} in properties for $ref {properties['$ref']}", + invalid_field=prop_name, ) target_name = prop_name target_property = target_property[prop_name] # type: ignore if not isinstance(target_name, str) or target_property is None: - raise ValueError(f"RefTypeParser: Invalid $ref {properties['$ref']}") + raise InvalidSchemaException( + f"Invalid $ref {properties['$ref']}", invalid_field="$ref" + ) return target_name, target_property diff --git a/jambo/parser/string_type_parser.py b/jambo/parser/string_type_parser.py index 21e1fd6..8ec583e 100644 --- a/jambo/parser/string_type_parser.py +++ b/jambo/parser/string_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser._type_parser import GenericTypeParser from jambo.types.type_parser_options import TypeParserOptions @@ -54,7 +55,9 @@ class StringTypeParser(GenericTypeParser): return str, mapped_properties if format_type not in self.format_type_mapping: - raise ValueError(f"Unsupported string format: {format_type}") + raise InvalidSchemaException( + f"Unsupported string format: {format_type}", invalid_field="format" + ) mapped_type = self.format_type_mapping[format_type] if format_type in self.format_pattern_mapping: diff --git a/jambo/types/json_schema_type.py b/jambo/types/json_schema_type.py index 954db75..3e73387 100644 --- a/jambo/types/json_schema_type.py +++ b/jambo/types/json_schema_type.py @@ -52,7 +52,7 @@ JSONSchema = TypedDict( "minProperties": int, "maxProperties": int, "dependencies": Dict[str, Union[List[str], "JSONSchema"]], - "items": Union["JSONSchema", List["JSONSchema"]], + "items": "JSONSchema", "prefixItems": List["JSONSchema"], "additionalItems": Union[bool, "JSONSchema"], "contains": "JSONSchema", diff --git a/tests/parser/test_ref_type_parser.py b/tests/parser/test_ref_type_parser.py index 3e08ff4..c16d0b6 100644 --- a/tests/parser/test_ref_type_parser.py +++ b/tests/parser/test_ref_type_parser.py @@ -40,7 +40,7 @@ class TestRefTypeParser(TestCase): }, } - with self.assertRaises(RuntimeError): + with self.assertRaises(ValueError): RefTypeParser().from_properties( "person", properties, @@ -63,7 +63,7 @@ class TestRefTypeParser(TestCase): }, } - with self.assertRaises(RuntimeError): + with self.assertRaises(ValueError): RefTypeParser().from_properties( "person", properties, diff --git a/tests/parser/test_string_type_parser.py b/tests/parser/test_string_type_parser.py index 279e20f..2a242e2 100644 --- a/tests/parser/test_string_type_parser.py +++ b/tests/parser/test_string_type_parser.py @@ -187,7 +187,8 @@ class TestStringTypeParser(TestCase): parser.from_properties("placeholder", properties) self.assertEqual( - str(context.exception), "Unsupported string format: unsupported-format" + str(context.exception), + "Invalid JSON Schema: Unsupported string format: unsupported-format (invalid field: format)", ) def test_string_parser_with_date_format(self): From e31002af32ae6e2879539ee193635019ca8e4695 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Sun, 14 Sep 2025 00:47:24 -0300 Subject: [PATCH 4/6] feat: fixes tests to validate the type of exception thrown --- jambo/exceptions/__init__.py | 7 +- .../internal_assertion_exception.py | 16 ++++ jambo/parser/ref_type_parser.py | 14 ++- jambo/parser/string_type_parser.py | 10 +- tests/parser/test_allof_type_parser.py | 23 +++-- tests/parser/test_anyof_type_parser.py | 7 +- tests/parser/test_array_type_parser.py | 11 ++- tests/parser/test_bool_type_parser.py | 3 +- tests/parser/test_const_type_parser.py | 5 +- tests/parser/test_enum_type_parser.py | 7 +- tests/parser/test_float_type_parser.py | 13 +-- tests/parser/test_int_type_parser.py | 13 +-- tests/parser/test_oneof_type_parser.py | 46 ++++----- tests/parser/test_ref_type_parser.py | 23 +++-- tests/parser/test_string_type_parser.py | 9 +- tests/parser/test_type_parser.py | 3 +- tests/test_schema_converter.py | 93 +++++++++++-------- 17 files changed, 179 insertions(+), 124 deletions(-) create mode 100644 jambo/exceptions/internal_assertion_exception.py diff --git a/jambo/exceptions/__init__.py b/jambo/exceptions/__init__.py index 0292136..5fe1c00 100644 --- a/jambo/exceptions/__init__.py +++ b/jambo/exceptions/__init__.py @@ -1,5 +1,10 @@ +from .internal_assertion_exception import InternalAssertionException from .invalid_schema_exception import InvalidSchemaException from .unsupported_schema_exception import UnsupportedSchemaException -__all__ = ["InvalidSchemaException", "UnsupportedSchemaException"] +__all__ = [ + "InternalAssertionException", + "InvalidSchemaException", + "UnsupportedSchemaException", +] diff --git a/jambo/exceptions/internal_assertion_exception.py b/jambo/exceptions/internal_assertion_exception.py new file mode 100644 index 0000000..ce3338d --- /dev/null +++ b/jambo/exceptions/internal_assertion_exception.py @@ -0,0 +1,16 @@ +class InternalAssertionException(AssertionError): + """Exception raised for internal assertions.""" + + def __init__( + self, + message: str, + ) -> None: + # Normalize message by stripping redundant prefix if present + message = message.removeprefix("Internal Assertion Failed: ") + super().__init__(message) + + def __str__(self) -> str: + return ( + f"Internal Assertion Failed: {super().__str__()}\n" + "This is likely a bug in Jambo. Please report it at" + ) diff --git a/jambo/parser/ref_type_parser.py b/jambo/parser/ref_type_parser.py index 615283e..123260f 100644 --- a/jambo/parser/ref_type_parser.py +++ b/jambo/parser/ref_type_parser.py @@ -1,4 +1,4 @@ -from jambo.exceptions import InvalidSchemaException +from jambo.exceptions import InternalAssertionException, InvalidSchemaException from jambo.parser import GenericTypeParser from jambo.types.json_schema_type import JSONSchema from jambo.types.type_parser_options import TypeParserOptions @@ -22,17 +22,15 @@ class RefTypeParser(GenericTypeParser): f"Missing $ref in properties for {name}", invalid_field="$ref" ) - context = kwargs.get("context") - if context is None: - raise InvalidSchemaException( - f"Missing `context` in properties for {name}", invalid_field="context" + if kwargs.get("context") is None: + raise InternalAssertionException( + "`context` must be provided in kwargs for RefTypeParser" ) ref_cache = kwargs.get("ref_cache") if ref_cache is None: - raise InvalidSchemaException( - f"Missing `ref_cache` in properties for {name}", - invalid_field="ref_cache", + raise InternalAssertionException( + "`ref_cache` must be provided in kwargs for RefTypeParser" ) mapped_properties = self.mappings_properties_builder(properties, **kwargs) diff --git a/jambo/parser/string_type_parser.py b/jambo/parser/string_type_parser.py index 8ec583e..f8d0c9e 100644 --- a/jambo/parser/string_type_parser.py +++ b/jambo/parser/string_type_parser.py @@ -23,19 +23,19 @@ class StringTypeParser(GenericTypeParser): } format_type_mapping = { - # 7.3.1. Dates, Times, and Duration + # [7.3.1](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.1). Dates, Times, and Duration "date": date, "time": time, "date-time": datetime, "duration": timedelta, - # 7.3.2. Email Addresses + # [7.3.2](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.2). Email Addresses "email": EmailStr, - # 7.3.3. Hostnames + # [7.3.3](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.3). Hostnames "hostname": str, - # 7.3.4. IP Addresses + # [7.3.4](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.4). IP Addresses "ipv4": IPv4Address, "ipv6": IPv6Address, - # 7.3.5. Resource Identifiers + # [7.3.5](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.5). Resource Identifiers "uri": AnyUrl, # "iri" # Not supported by pydantic and currently not supported by jambo "uuid": UUID, diff --git a/tests/parser/test_allof_type_parser.py b/tests/parser/test_allof_type_parser.py index 2ae4bc2..f99c657 100644 --- a/tests/parser/test_allof_type_parser.py +++ b/tests/parser/test_allof_type_parser.py @@ -1,5 +1,8 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser.allof_type_parser import AllOfTypeParser +from pydantic import ValidationError + from unittest import TestCase @@ -42,13 +45,13 @@ class TestAllOfTypeParser(TestCase): "placeholder", properties ) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): type_parsing(name="John", age=101) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): type_parsing(name="", age=30) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): type_parsing(name="John Invalid", age=30) obj = type_parsing(name="John", age=30) @@ -87,10 +90,10 @@ class TestAllOfTypeParser(TestCase): "placeholder", properties ) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): type_parsing(name="John") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): type_parsing(age=30) obj = type_parsing(name="John", age=30) @@ -154,7 +157,7 @@ class TestAllOfTypeParser(TestCase): ] } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): AllOfTypeParser().from_properties("placeholder", properties) def test_all_of_invalid_type_not_present(self): @@ -167,7 +170,7 @@ class TestAllOfTypeParser(TestCase): ] } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): AllOfTypeParser().from_properties("placeholder", properties) def test_all_of_invalid_type_in_fields(self): @@ -180,7 +183,7 @@ class TestAllOfTypeParser(TestCase): ] } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): AllOfTypeParser().from_properties("placeholder", properties) def test_all_of_invalid_type_not_all_equal(self): @@ -196,7 +199,7 @@ class TestAllOfTypeParser(TestCase): ] } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): AllOfTypeParser().from_properties("placeholder", properties) def test_all_of_description_field(self): @@ -304,5 +307,5 @@ class TestAllOfTypeParser(TestCase): ], } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): AllOfTypeParser().from_properties("placeholder", properties) diff --git a/tests/parser/test_anyof_type_parser.py b/tests/parser/test_anyof_type_parser.py index 32c2f45..d7740fd 100644 --- a/tests/parser/test_anyof_type_parser.py +++ b/tests/parser/test_anyof_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser.anyof_type_parser import AnyOfTypeParser from typing_extensions import Annotated, Union, get_args, get_origin @@ -14,7 +15,7 @@ class TestAnyOfTypeParser(TestCase): ], } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): AnyOfTypeParser().from_properties("placeholder", properties) def test_any_of_with_invalid_properties(self): @@ -22,7 +23,7 @@ class TestAnyOfTypeParser(TestCase): "anyOf": None, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): AnyOfTypeParser().from_properties("placeholder", properties) def test_any_of_string_or_int(self): @@ -95,5 +96,5 @@ class TestAnyOfTypeParser(TestCase): "default": 3.14, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): AnyOfTypeParser().from_properties("placeholder", properties) diff --git a/tests/parser/test_array_type_parser.py b/tests/parser/test_array_type_parser.py index d27330f..1cac217 100644 --- a/tests/parser/test_array_type_parser.py +++ b/tests/parser/test_array_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser import ArrayTypeParser from typing_extensions import get_args @@ -67,7 +68,7 @@ class TestArrayTypeParser(TestCase): properties = {"items": {"type": "string"}, "default": ["a", 1, "c"]} - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_array_parser_with_invalid_default_type(self): @@ -75,15 +76,15 @@ class TestArrayTypeParser(TestCase): properties = {"items": {"type": "string"}, "default": 000} - with self.assertRaises(ValueError): - parser.from_properties("placeholder", properties) + with self.assertRaises(InvalidSchemaException): + parser.from_properties("placeholder", properties=properties) def test_array_parser_with_invalid_default_min(self): parser = ArrayTypeParser() properties = {"items": {"type": "string"}, "default": ["a"], "minItems": 2} - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_array_parser_with_invalid_default_max(self): @@ -95,5 +96,5 @@ class TestArrayTypeParser(TestCase): "maxItems": 3, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) diff --git a/tests/parser/test_bool_type_parser.py b/tests/parser/test_bool_type_parser.py index 1e3a6c9..2c2be6c 100644 --- a/tests/parser/test_bool_type_parser.py +++ b/tests/parser/test_bool_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser import BooleanTypeParser from unittest import TestCase @@ -39,5 +40,5 @@ class TestBoolTypeParser(TestCase): "default": "invalid", } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties_impl("placeholder", properties) diff --git a/tests/parser/test_const_type_parser.py b/tests/parser/test_const_type_parser.py index bd7dcc5..5e0fc29 100644 --- a/tests/parser/test_const_type_parser.py +++ b/tests/parser/test_const_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser import ConstTypeParser from typing_extensions import Annotated, Literal, get_args, get_origin @@ -80,7 +81,7 @@ class TestConstTypeParser(TestCase): expected_const_value = "United States of America" properties = {"notConst": expected_const_value} - with self.assertRaises(ValueError) as context: + with self.assertRaises(InvalidSchemaException) as context: parser.from_properties_impl("invalid_country", properties) self.assertIn( @@ -93,7 +94,7 @@ class TestConstTypeParser(TestCase): properties = {"const": {}} - with self.assertRaises(ValueError) as context: + with self.assertRaises(InvalidSchemaException) as context: parser.from_properties_impl("invalid_country", properties) self.assertIn( diff --git a/tests/parser/test_enum_type_parser.py b/tests/parser/test_enum_type_parser.py index aad95a4..dafb9d8 100644 --- a/tests/parser/test_enum_type_parser.py +++ b/tests/parser/test_enum_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser import EnumTypeParser from enum import Enum @@ -10,7 +11,7 @@ class TestEnumTypeParser(TestCase): schema = {} - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parsed_type, parsed_properties = parser.from_properties_impl( "TestEnum", schema, @@ -23,7 +24,7 @@ class TestEnumTypeParser(TestCase): "enum": "not_a_list", } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parsed_type, parsed_properties = parser.from_properties_impl( "TestEnum", schema, @@ -86,5 +87,5 @@ class TestEnumTypeParser(TestCase): "enum": ["value1", 42, dict()], } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties_impl("TestEnum", schema) diff --git a/tests/parser/test_float_type_parser.py b/tests/parser/test_float_type_parser.py index c462d64..1bdd65a 100644 --- a/tests/parser/test_float_type_parser.py +++ b/tests/parser/test_float_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser import FloatTypeParser from unittest import TestCase @@ -61,7 +62,7 @@ class TestFloatTypeParser(TestCase): "multipleOf": 0.5, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_float_parser_with_default_invalid_maximum(self): @@ -75,7 +76,7 @@ class TestFloatTypeParser(TestCase): "multipleOf": 0.5, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_float_parser_with_default_invalid_minimum(self): @@ -89,7 +90,7 @@ class TestFloatTypeParser(TestCase): "multipleOf": 0.5, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_float_parser_with_default_invalid_exclusive_maximum(self): @@ -103,7 +104,7 @@ class TestFloatTypeParser(TestCase): "multipleOf": 0.5, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_float_parser_with_default_invalid_exclusive_minimum(self): @@ -117,7 +118,7 @@ class TestFloatTypeParser(TestCase): "multipleOf": 0.5, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_float_parser_with_default_invalid_multiple(self): @@ -131,5 +132,5 @@ class TestFloatTypeParser(TestCase): "multipleOf": 2.0, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) diff --git a/tests/parser/test_int_type_parser.py b/tests/parser/test_int_type_parser.py index 5cfeed5..fa563f4 100644 --- a/tests/parser/test_int_type_parser.py +++ b/tests/parser/test_int_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser import IntTypeParser from unittest import TestCase @@ -61,7 +62,7 @@ class TestIntTypeParser(TestCase): "multipleOf": 2, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_int_parser_with_default_invalid_maximum(self): @@ -75,7 +76,7 @@ class TestIntTypeParser(TestCase): "multipleOf": 2, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_int_parser_with_default_invalid_minimum(self): @@ -89,7 +90,7 @@ class TestIntTypeParser(TestCase): "multipleOf": 2, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_int_parser_with_default_invalid_exclusive_maximum(self): @@ -103,7 +104,7 @@ class TestIntTypeParser(TestCase): "multipleOf": 2, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_int_parser_with_default_invalid_exclusive_minimum(self): @@ -117,7 +118,7 @@ class TestIntTypeParser(TestCase): "multipleOf": 2, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_int_parser_with_default_invalid_multipleOf(self): @@ -131,5 +132,5 @@ class TestIntTypeParser(TestCase): "multipleOf": 2, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) diff --git a/tests/parser/test_oneof_type_parser.py b/tests/parser/test_oneof_type_parser.py index 69104ae..99e9074 100644 --- a/tests/parser/test_oneof_type_parser.py +++ b/tests/parser/test_oneof_type_parser.py @@ -1,12 +1,15 @@ from jambo import SchemaConverter +from jambo.exceptions import InvalidSchemaException from jambo.parser.oneof_type_parser import OneOfTypeParser +from pydantic import ValidationError + from unittest import TestCase class TestOneOfTypeParser(TestCase): def test_oneof_raises_on_invalid_property(self): - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): OneOfTypeParser().from_properties_impl( "test_field", { @@ -17,7 +20,7 @@ class TestOneOfTypeParser(TestCase): ref_cache={}, ) - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): SchemaConverter.build( { "title": "Test", @@ -71,13 +74,13 @@ class TestOneOfTypeParser(TestCase): Model = SchemaConverter.build(schema) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(id=-5) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(id="invalid") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(id=123.45) def test_oneof_with_conflicting_schemas(self): @@ -103,11 +106,11 @@ class TestOneOfTypeParser(TestCase): obj2 = Model(data=9) self.assertEqual(obj2.data, 9) - with self.assertRaises(ValueError) as cm: + with self.assertRaises(ValidationError) as cm: Model(data=6) self.assertIn("matches multiple oneOf schemas", str(cm.exception)) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(data=5) def test_oneof_with_objects(self): @@ -147,7 +150,7 @@ class TestOneOfTypeParser(TestCase): obj2 = Model(contact_info={"phone": "123-456-7890"}) self.assertEqual(obj2.contact_info.phone, "123-456-7890") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(contact_info={"email": "user@example.com", "phone": "123-456-7890"}) def test_oneof_with_discriminator_basic(self): @@ -190,14 +193,14 @@ class TestOneOfTypeParser(TestCase): self.assertEqual(dog.pet.type, "dog") self.assertEqual(dog.pet.barks, False) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(pet={"type": "cat", "barks": True}) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(pet={"type": "bird", "flies": True}) def test_oneof_with_invalid_types(self): - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): SchemaConverter.build( { "title": "Pet", @@ -301,13 +304,13 @@ class TestOneOfTypeParser(TestCase): Model = SchemaConverter.build(schema) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(shape={"type": "triangle", "base": 5, "height": 3}) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(shape={"type": "circle", "side": 5}) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(shape={"radius": 5}) def test_oneof_missing_properties(self): @@ -324,7 +327,7 @@ class TestOneOfTypeParser(TestCase): }, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): SchemaConverter.build(schema) def test_oneof_invalid_properties(self): @@ -336,7 +339,7 @@ class TestOneOfTypeParser(TestCase): }, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): SchemaConverter.build(schema) def test_oneof_with_default_value(self): @@ -373,12 +376,12 @@ class TestOneOfTypeParser(TestCase): }, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): SchemaConverter.build(schema) def test_oneof_discriminator_without_property_name(self): # Should throw because the spec determines propertyName is required for discriminator - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): SchemaConverter.build( { "title": "Test", @@ -409,7 +412,7 @@ class TestOneOfTypeParser(TestCase): def test_oneof_discriminator_with_invalid_discriminator(self): # Should throw because a valid discriminator is required - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): SchemaConverter.build( { "title": "Test", @@ -465,8 +468,9 @@ class TestOneOfTypeParser(TestCase): self.assertEqual(obj2.value, "very long string") # Invalid: Medium string (matches BOTH schemas - violates oneOf) - with self.assertRaises(ValueError) as cm: + with self.assertRaises(ValidationError) 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): @@ -515,5 +519,5 @@ class TestOneOfTypeParser(TestCase): self.assertEqual(rectangle.shape.height, 20) # Invalid: Wrong properties for the type - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(shape={"type": "circle", "width": 10}) diff --git a/tests/parser/test_ref_type_parser.py b/tests/parser/test_ref_type_parser.py index c16d0b6..44b0949 100644 --- a/tests/parser/test_ref_type_parser.py +++ b/tests/parser/test_ref_type_parser.py @@ -1,5 +1,8 @@ +from jambo.exceptions import InternalAssertionException, InvalidSchemaException from jambo.parser import ObjectTypeParser, RefTypeParser +from pydantic import ValidationError + from typing import ForwardRef from unittest import TestCase @@ -16,7 +19,7 @@ class TestRefTypeParser(TestCase): "required": ["name", "age"], } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): RefTypeParser().from_properties( "person", properties, @@ -40,7 +43,7 @@ class TestRefTypeParser(TestCase): }, } - with self.assertRaises(ValueError): + with self.assertRaises(InternalAssertionException): RefTypeParser().from_properties( "person", properties, @@ -63,7 +66,7 @@ class TestRefTypeParser(TestCase): }, } - with self.assertRaises(ValueError): + with self.assertRaises(InternalAssertionException): RefTypeParser().from_properties( "person", properties, @@ -77,7 +80,7 @@ class TestRefTypeParser(TestCase): "$ref": "https://example.com/schemas/person.json", } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): RefTypeParser().from_properties( "person", properties, @@ -110,7 +113,7 @@ class TestRefTypeParser(TestCase): }, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): ObjectTypeParser().from_properties( "person", properties, @@ -126,7 +129,7 @@ class TestRefTypeParser(TestCase): "$defs": {}, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): RefTypeParser().from_properties( "person", properties, @@ -142,7 +145,7 @@ class TestRefTypeParser(TestCase): "$defs": {"person": None}, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): RefTypeParser().from_properties( "person", properties, @@ -232,7 +235,7 @@ class TestRefTypeParser(TestCase): "required": ["name", "age"], } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): ObjectTypeParser().from_properties( "person", properties, @@ -264,7 +267,7 @@ class TestRefTypeParser(TestCase): ) # checks if when created via FowardRef the model is validated correctly. - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model( name="John", age=30, @@ -421,7 +424,7 @@ class TestRefTypeParser(TestCase): }, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): ref_strategy, ref_name, ref_property = RefTypeParser()._parse_from_strategy( "invalid_strategy", "person", diff --git a/tests/parser/test_string_type_parser.py b/tests/parser/test_string_type_parser.py index 2a242e2..ac42145 100644 --- a/tests/parser/test_string_type_parser.py +++ b/tests/parser/test_string_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser import StringTypeParser from pydantic import AnyUrl, EmailStr @@ -62,7 +63,7 @@ class TestStringTypeParser(TestCase): "minLength": 5, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_string_parser_with_default_invalid_maxlength(self): @@ -75,7 +76,7 @@ class TestStringTypeParser(TestCase): "minLength": 1, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_string_parser_with_default_invalid_minlength(self): @@ -88,7 +89,7 @@ class TestStringTypeParser(TestCase): "minLength": 2, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): parser.from_properties("placeholder", properties) def test_string_parser_with_email_format(self): @@ -183,7 +184,7 @@ class TestStringTypeParser(TestCase): "format": "unsupported-format", } - with self.assertRaises(ValueError) as context: + with self.assertRaises(InvalidSchemaException) as context: parser.from_properties("placeholder", properties) self.assertEqual( diff --git a/tests/parser/test_type_parser.py b/tests/parser/test_type_parser.py index 4722de8..748ea7e 100644 --- a/tests/parser/test_type_parser.py +++ b/tests/parser/test_type_parser.py @@ -1,3 +1,4 @@ +from jambo.exceptions import InvalidSchemaException from jambo.parser import StringTypeParser from jambo.parser._type_parser import GenericTypeParser @@ -17,5 +18,5 @@ class TestGenericTypeParser(TestCase): StringTypeParser.json_schema_type = "type:string" def test_get_impl_invalid_type(self): - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): GenericTypeParser._get_impl({"type": "invalid_type"}) diff --git a/tests/test_schema_converter.py b/tests/test_schema_converter.py index 2d4099b..6f2b15a 100644 --- a/tests/test_schema_converter.py +++ b/tests/test_schema_converter.py @@ -1,6 +1,7 @@ from jambo import SchemaConverter +from jambo.exceptions import InvalidSchemaException, UnsupportedSchemaException -from pydantic import AnyUrl, BaseModel +from pydantic import AnyUrl, BaseModel, ValidationError from ipaddress import IPv4Address, IPv6Address from unittest import TestCase @@ -23,7 +24,7 @@ class TestSchemaConverter(TestCase): }, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): SchemaConverter.build(schema) def test_invalid_schema_type(self): @@ -37,7 +38,7 @@ class TestSchemaConverter(TestCase): }, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): SchemaConverter.build(schema) def test_build_expects_title(self): @@ -50,7 +51,7 @@ class TestSchemaConverter(TestCase): }, } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): SchemaConverter.build(schema) def test_build_expects_object(self): @@ -60,7 +61,7 @@ class TestSchemaConverter(TestCase): "type": "string", } - with self.assertRaises(ValueError): + with self.assertRaises(UnsupportedSchemaException): SchemaConverter.build(schema) def test_is_invalid_field(self): @@ -76,7 +77,7 @@ class TestSchemaConverter(TestCase): # 'required': ['name', 'age', 'is_active', 'friends', 'address'], } - with self.assertRaises(ValueError) as context: + with self.assertRaises(InvalidSchemaException) as context: SchemaConverter.build(schema) self.assertTrue("Unknown type" in str(context.exception)) @@ -117,16 +118,16 @@ class TestSchemaConverter(TestCase): self.assertEqual(model(name="John", age=30).name, "John") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model(name=123, age=30, email="teste@hideyoshi.com") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model(name="John Invalid", age=45, email="teste@hideyoshi.com") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model(name="", age=45, email="teste@hideyoshi.com") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model(name="John", age=45, email="hideyoshi.com") def test_validation_integer(self): @@ -148,10 +149,10 @@ class TestSchemaConverter(TestCase): self.assertEqual(model(age=30).age, 30) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model(age=-1) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model(age=121) def test_validation_float(self): @@ -173,10 +174,10 @@ class TestSchemaConverter(TestCase): self.assertEqual(model(age=30).age, 30.0) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model(age=-1.0) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model(age=121.0) def test_validation_boolean(self): @@ -219,10 +220,10 @@ class TestSchemaConverter(TestCase): model(friends=["John", "Jane", "John"]).friends, {"John", "Jane"} ) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model(friends=[]) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model(friends=["John", "Jane", "Invalid"]) def test_validation_list_with_missing_items(self): @@ -262,7 +263,7 @@ class TestSchemaConverter(TestCase): } ) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model() def test_validation_object(self): @@ -290,7 +291,7 @@ class TestSchemaConverter(TestCase): self.assertEqual(obj.address.street, "123 Main St") self.assertEqual(obj.address.city, "Springfield") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model() def test_default_for_string(self): @@ -329,7 +330,7 @@ class TestSchemaConverter(TestCase): "required": ["name"], } - with self.assertRaises(ValueError): + with self.assertRaises(InvalidSchemaException): SchemaConverter.build(schema_max_length) def test_default_for_list(self): @@ -421,10 +422,10 @@ class TestSchemaConverter(TestCase): self.assertEqual(obj.name, "J") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(name="John Invalid") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(name="") def test_any_of(self): @@ -450,13 +451,13 @@ class TestSchemaConverter(TestCase): obj = Model(id="12345678901") self.assertEqual(obj.id, "12345678901") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(id="") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(id="12345678901234567890") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(id=11) def test_string_format_email(self): @@ -465,9 +466,11 @@ class TestSchemaConverter(TestCase): "type": "object", "properties": {"email": {"type": "string", "format": "email"}}, } + model = SchemaConverter.build(schema) self.assertEqual(model(email="test@example.com").email, "test@example.com") - with self.assertRaises(ValueError): + + with self.assertRaises(ValidationError): model(email="invalid-email") def test_string_format_uri(self): @@ -476,11 +479,13 @@ class TestSchemaConverter(TestCase): "type": "object", "properties": {"website": {"type": "string", "format": "uri"}}, } + model = SchemaConverter.build(schema) self.assertEqual( model(website="https://example.com").website, AnyUrl("https://example.com") ) - with self.assertRaises(ValueError): + + with self.assertRaises(ValidationError): model(website="invalid-uri") def test_string_format_ipv4(self): @@ -489,9 +494,11 @@ class TestSchemaConverter(TestCase): "type": "object", "properties": {"ip": {"type": "string", "format": "ipv4"}}, } + model = SchemaConverter.build(schema) self.assertEqual(model(ip="192.168.1.1").ip, IPv4Address("192.168.1.1")) - with self.assertRaises(ValueError): + + with self.assertRaises(ValidationError): model(ip="256.256.256.256") def test_string_format_ipv6(self): @@ -500,12 +507,14 @@ class TestSchemaConverter(TestCase): "type": "object", "properties": {"ip": {"type": "string", "format": "ipv6"}}, } + model = SchemaConverter.build(schema) self.assertEqual( model(ip="2001:0db8:85a3:0000:0000:8a2e:0370:7334").ip, IPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), ) - with self.assertRaises(ValueError): + + with self.assertRaises(ValidationError): model(ip="invalid-ipv6") def test_string_format_uuid(self): @@ -514,6 +523,7 @@ class TestSchemaConverter(TestCase): "type": "object", "properties": {"id": {"type": "string", "format": "uuid"}}, } + model = SchemaConverter.build(schema) self.assertEqual( @@ -521,7 +531,7 @@ class TestSchemaConverter(TestCase): UUID("123e4567-e89b-12d3-a456-426614174000"), ) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): model(id="invalid-uuid") def test_string_format_hostname(self): @@ -530,9 +540,11 @@ class TestSchemaConverter(TestCase): "type": "object", "properties": {"hostname": {"type": "string", "format": "hostname"}}, } + model = SchemaConverter.build(schema) self.assertEqual(model(hostname="example.com").hostname, "example.com") - with self.assertRaises(ValueError): + + with self.assertRaises(ValidationError): model(hostname="invalid..hostname") def test_string_format_datetime(self): @@ -541,12 +553,14 @@ class TestSchemaConverter(TestCase): "type": "object", "properties": {"timestamp": {"type": "string", "format": "date-time"}}, } + model = SchemaConverter.build(schema) self.assertEqual( model(timestamp="2024-01-01T12:00:00Z").timestamp.isoformat(), "2024-01-01T12:00:00+00:00", ) - with self.assertRaises(ValueError): + + with self.assertRaises(ValidationError): model(timestamp="invalid-datetime") def test_string_format_time(self): @@ -555,11 +569,13 @@ class TestSchemaConverter(TestCase): "type": "object", "properties": {"time": {"type": "string", "format": "time"}}, } + model = SchemaConverter.build(schema) self.assertEqual( model(time="20:20:39+00:00").time.isoformat(), "20:20:39+00:00" ) - with self.assertRaises(ValueError): + + with self.assertRaises(ValidationError): model(time="25:00:00") def test_string_format_unsupported(self): @@ -568,7 +584,8 @@ class TestSchemaConverter(TestCase): "type": "object", "properties": {"field": {"type": "string", "format": "unsupported"}}, } - with self.assertRaises(ValueError): + + with self.assertRaises(InvalidSchemaException): SchemaConverter.build(schema) def test_ref_with_root_ref(self): @@ -726,10 +743,10 @@ class TestSchemaConverter(TestCase): obj = Model() self.assertEqual(obj.name, "United States of America") - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): obj.name = "Canada" - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(name="Canada") def test_const_type_parser_with_non_hashable_value(self): @@ -749,10 +766,10 @@ class TestSchemaConverter(TestCase): obj = Model() self.assertEqual(obj.name, ["Brazil"]) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): obj.name = ["Argentina"] - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(name=["Argentina"]) def test_null_type_parser(self): @@ -772,5 +789,5 @@ class TestSchemaConverter(TestCase): obj = Model(a_thing=None) self.assertIsNone(obj.a_thing) - with self.assertRaises(ValueError): + with self.assertRaises(ValidationError): Model(a_thing="not none") From 8c6a04bbdfc288e590b18ad9d4e45a4e96387918 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Sun, 14 Sep 2025 00:57:36 -0300 Subject: [PATCH 5/6] feat: adds simple tests for internal exceptions --- tests/exceptions/__init__.py | 0 .../test_internal_assertion_exception.py | 21 +++++++++ .../test_invalid_schema_exception.py | 44 +++++++++++++++++++ .../test_unsupported_schema_exception.py | 31 +++++++++++++ tests/parser/test_array_type_parser.py | 11 +++++ tests/parser/test_oneof_type_parser.py | 11 +++++ 6 files changed, 118 insertions(+) create mode 100644 tests/exceptions/__init__.py create mode 100644 tests/exceptions/test_internal_assertion_exception.py create mode 100644 tests/exceptions/test_invalid_schema_exception.py create mode 100644 tests/exceptions/test_unsupported_schema_exception.py diff --git a/tests/exceptions/__init__.py b/tests/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/exceptions/test_internal_assertion_exception.py b/tests/exceptions/test_internal_assertion_exception.py new file mode 100644 index 0000000..4a6d4de --- /dev/null +++ b/tests/exceptions/test_internal_assertion_exception.py @@ -0,0 +1,21 @@ +from jambo.exceptions.internal_assertion_exception import InternalAssertionException + +from unittest import TestCase + + +class TestInternalAssertionException(TestCase): + def test_inheritance(self): + self.assertTrue(issubclass(InternalAssertionException, AssertionError)) + + def test_message(self): + message = "This is an internal assertion error." + + expected_message = ( + f"Internal Assertion Failed: {message}\n" + "This is likely a bug in Jambo. Please report it at" + ) + + with self.assertRaises(InternalAssertionException) as ctx: + raise InternalAssertionException(message) + + self.assertEqual(str(ctx.exception), expected_message) diff --git a/tests/exceptions/test_invalid_schema_exception.py b/tests/exceptions/test_invalid_schema_exception.py new file mode 100644 index 0000000..8f96fd1 --- /dev/null +++ b/tests/exceptions/test_invalid_schema_exception.py @@ -0,0 +1,44 @@ +from jambo.exceptions.invalid_schema_exception import InvalidSchemaException + +from unittest import TestCase + + +class TestInternalAssertionException(TestCase): + def test_inheritance(self): + self.assertTrue(issubclass(InvalidSchemaException, ValueError)) + + def test_message(self): + message = "This is an internal assertion error." + + expected_message = f"Invalid JSON Schema: {message}" + + with self.assertRaises(InvalidSchemaException) as ctx: + raise InvalidSchemaException(message) + + self.assertEqual(str(ctx.exception), expected_message) + + def test_invalid_field(self): + message = "This is an internal assertion error." + invalid_field = "testField" + + expected_message = ( + f"Invalid JSON Schema: {message} (invalid field: {invalid_field})" + ) + + with self.assertRaises(InvalidSchemaException) as ctx: + raise InvalidSchemaException(message, invalid_field=invalid_field) + + self.assertEqual(str(ctx.exception), expected_message) + + def test_cause(self): + message = "This is an internal assertion error." + cause = ValueError("Underlying cause") + + expected_message = ( + f"Invalid JSON Schema: {message} (caused by ValueError: Underlying cause)" + ) + + with self.assertRaises(InvalidSchemaException) as ctx: + raise InvalidSchemaException(message, cause=cause) + + self.assertEqual(str(ctx.exception), expected_message) diff --git a/tests/exceptions/test_unsupported_schema_exception.py b/tests/exceptions/test_unsupported_schema_exception.py new file mode 100644 index 0000000..e01e3e3 --- /dev/null +++ b/tests/exceptions/test_unsupported_schema_exception.py @@ -0,0 +1,31 @@ +from jambo.exceptions.unsupported_schema_exception import UnsupportedSchemaException + +from unittest import TestCase + + +class TestUnsupportedSchemaException(TestCase): + def test_inheritance(self): + self.assertTrue(issubclass(UnsupportedSchemaException, ValueError)) + + def test_message(self): + message = "This is an internal assertion error." + + expected_message = f"Unsupported JSON Schema: {message}" + + with self.assertRaises(UnsupportedSchemaException) as ctx: + raise UnsupportedSchemaException(message) + + self.assertEqual(str(ctx.exception), expected_message) + + def test_unsupported_field(self): + message = "This is an internal assertion error." + invalid_field = "testField" + + expected_message = ( + f"Unsupported JSON Schema: {message} (unsupported field: {invalid_field})" + ) + + with self.assertRaises(UnsupportedSchemaException) as ctx: + raise UnsupportedSchemaException(message, unsupported_field=invalid_field) + + self.assertEqual(str(ctx.exception), expected_message) diff --git a/tests/parser/test_array_type_parser.py b/tests/parser/test_array_type_parser.py index 1cac217..dc9212f 100644 --- a/tests/parser/test_array_type_parser.py +++ b/tests/parser/test_array_type_parser.py @@ -19,6 +19,17 @@ class TestArrayTypeParser(TestCase): self.assertEqual(type_parsing.__origin__, list) self.assertEqual(element_type, str) + def test_array_parser_with_no_items(self): + parser = ArrayTypeParser() + + properties = { + "default": ["a", "b", "c", "d"], + "maxItems": 3, + } + + with self.assertRaises(InvalidSchemaException): + parser.from_properties("placeholder", properties) + def test_array_parser_with_options_unique(self): parser = ArrayTypeParser() diff --git a/tests/parser/test_oneof_type_parser.py b/tests/parser/test_oneof_type_parser.py index 99e9074..14bf942 100644 --- a/tests/parser/test_oneof_type_parser.py +++ b/tests/parser/test_oneof_type_parser.py @@ -20,6 +20,17 @@ class TestOneOfTypeParser(TestCase): ref_cache={}, ) + with self.assertRaises(InvalidSchemaException): + OneOfTypeParser().from_properties_impl( + "test_field", + { + "oneOf": [], # should throw because oneOf must be a list with at least one item + }, + required=True, + context={}, + ref_cache={}, + ) + with self.assertRaises(InvalidSchemaException): SchemaConverter.build( { From 7f44e84bce08f788e0923eb5d039ffa1605c36fd Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Sun, 14 Sep 2025 01:12:43 -0300 Subject: [PATCH 6/6] feat: updates outdated docs for exceptions --- docs/source/jambo.exceptions.rst | 37 ++++++++++++++++++++++++++++++++ docs/source/jambo.parser.rst | 32 +++++++++++++++++++++++++++ docs/source/jambo.rst | 1 + 3 files changed, 70 insertions(+) create mode 100644 docs/source/jambo.exceptions.rst diff --git a/docs/source/jambo.exceptions.rst b/docs/source/jambo.exceptions.rst new file mode 100644 index 0000000..93797bc --- /dev/null +++ b/docs/source/jambo.exceptions.rst @@ -0,0 +1,37 @@ +jambo.exceptions package +======================== + +Submodules +---------- + +jambo.exceptions.internal\_assertion\_exception module +------------------------------------------------------ + +.. automodule:: jambo.exceptions.internal_assertion_exception + :members: + :show-inheritance: + :undoc-members: + +jambo.exceptions.invalid\_schema\_exception module +-------------------------------------------------- + +.. automodule:: jambo.exceptions.invalid_schema_exception + :members: + :show-inheritance: + :undoc-members: + +jambo.exceptions.unsupported\_schema\_exception module +------------------------------------------------------ + +.. automodule:: jambo.exceptions.unsupported_schema_exception + :members: + :show-inheritance: + :undoc-members: + +Module contents +--------------- + +.. automodule:: jambo.exceptions + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/source/jambo.parser.rst b/docs/source/jambo.parser.rst index daa4c46..7c55a67 100644 --- a/docs/source/jambo.parser.rst +++ b/docs/source/jambo.parser.rst @@ -36,6 +36,22 @@ jambo.parser.boolean\_type\_parser module :show-inheritance: :undoc-members: +jambo.parser.const\_type\_parser module +--------------------------------------- + +.. automodule:: jambo.parser.const_type_parser + :members: + :show-inheritance: + :undoc-members: + +jambo.parser.enum\_type\_parser module +-------------------------------------- + +.. automodule:: jambo.parser.enum_type_parser + :members: + :show-inheritance: + :undoc-members: + jambo.parser.float\_type\_parser module --------------------------------------- @@ -52,6 +68,14 @@ jambo.parser.int\_type\_parser module :show-inheritance: :undoc-members: +jambo.parser.null\_type\_parser module +-------------------------------------- + +.. automodule:: jambo.parser.null_type_parser + :members: + :show-inheritance: + :undoc-members: + jambo.parser.object\_type\_parser module ---------------------------------------- @@ -60,6 +84,14 @@ jambo.parser.object\_type\_parser module :show-inheritance: :undoc-members: +jambo.parser.oneof\_type\_parser module +--------------------------------------- + +.. automodule:: jambo.parser.oneof_type_parser + :members: + :show-inheritance: + :undoc-members: + jambo.parser.ref\_type\_parser module ------------------------------------- diff --git a/docs/source/jambo.rst b/docs/source/jambo.rst index 791ad0d..0bd0d5b 100644 --- a/docs/source/jambo.rst +++ b/docs/source/jambo.rst @@ -7,6 +7,7 @@ Subpackages .. toctree:: :maxdepth: 4 + jambo.exceptions jambo.parser jambo.types