Better Defaults Validation Implementation

This commit is contained in:
2025-04-19 15:44:27 -03:00
parent 5c3d3a39ba
commit c6a37dab74
8 changed files with 67 additions and 122 deletions

View File

@@ -50,8 +50,8 @@ class AnyOfTypeParser(GenericTypeParser):
if not required: if not required:
mapped_properties["default"] = mapped_properties.get("default") mapped_properties["default"] = mapped_properties.get("default")
# By defining the type as Union, we can use the Field validator to enforce # By defining the type as Union of Annotated type we can use the Field validator
# the constraints on the union type. # to enforce the constraints of each union type when needed.
# We use Annotated to attach the Field validators to the type. # We use Annotated to attach the Field validators to the type.
field_types = [Annotated[t, Field(**v)] if v else t for t, v in sub_types] field_types = [Annotated[t, Field(**v)] if v else t for t, v in sub_types]

View File

@@ -27,6 +27,7 @@ class ArrayTypeParser(GenericTypeParser):
} }
wrapper_type = set if properties.get("uniqueItems", False) else list wrapper_type = set if properties.get("uniqueItems", False) else list
field_type = wrapper_type[_item_type]
mapped_properties = mappings_properties_builder( mapped_properties = mappings_properties_builder(
properties, properties,
@@ -37,25 +38,11 @@ class ArrayTypeParser(GenericTypeParser):
default_list = properties.get("default") default_list = properties.get("default")
if default_list is not None: if default_list is not None:
if not isinstance(default_list, list): ArrayTypeParser.validate_default(
raise ValueError( field_type,
f"Default value must be a list, got {type(default_list).__name__}" mapped_properties,
) default_list,
)
if len(default_list) > properties.get("maxItems", float("inf")):
raise ValueError(
f"Default list exceeds maxItems limit of {properties.get('maxItems')}"
)
if len(default_list) < properties.get("minItems", 0):
raise ValueError(
f"Default list is below minItems limit of {properties.get('minItems')}"
)
if not all(isinstance(item, _item_type) for item in default_list):
raise ValueError(
f"All items in the default list must be of type {_item_type.__name__}"
)
if wrapper_type is list: if wrapper_type is list:
mapped_properties["default_factory"] = lambda: copy.deepcopy( mapped_properties["default_factory"] = lambda: copy.deepcopy(
@@ -69,4 +56,4 @@ class ArrayTypeParser(GenericTypeParser):
if "default_factory" in mapped_properties and "default" in mapped_properties: if "default_factory" in mapped_properties and "default" in mapped_properties:
del mapped_properties["default"] del mapped_properties["default"]
return wrapper_type[_item_type], mapped_properties return field_type, mapped_properties

View File

@@ -14,4 +14,11 @@ class BooleanTypeParser(GenericTypeParser):
_mappings = { _mappings = {
"default": "default", "default": "default",
} }
return bool, mappings_properties_builder(properties, _mappings)
mapped_properties = mappings_properties_builder(properties, _mappings, required)
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.")
return bool, mapped_properties

View File

@@ -1,6 +1,6 @@
from jambo.parser._type_parser import GenericTypeParser from jambo.parser._type_parser import GenericTypeParser
from jambo.utils.properties_builder.numeric_properties_builder import ( from jambo.utils.properties_builder.mappings_properties_builder import (
numeric_properties_builder, mappings_properties_builder,
) )
@@ -11,4 +11,18 @@ class FloatTypeParser(GenericTypeParser):
@staticmethod @staticmethod
def from_properties(name, properties, required=False): def from_properties(name, properties, required=False):
return float, numeric_properties_builder(properties, required) _mappings = {
"minimum": "ge",
"exclusiveMinimum": "gt",
"maximum": "le",
"exclusiveMaximum": "lt",
"multipleOf": "multiple_of",
"default": "default",
}
mapped_properties = mappings_properties_builder(properties, _mappings, required)
default_value = mapped_properties.get("default")
if default_value is not None:
FloatTypeParser.validate_default(float, mapped_properties, default_value)
return float, mapped_properties

View File

@@ -1,6 +1,6 @@
from jambo.parser._type_parser import GenericTypeParser from jambo.parser._type_parser import GenericTypeParser
from jambo.utils.properties_builder.numeric_properties_builder import ( from jambo.utils.properties_builder.mappings_properties_builder import (
numeric_properties_builder, mappings_properties_builder,
) )
@@ -11,4 +11,18 @@ class IntTypeParser(GenericTypeParser):
@staticmethod @staticmethod
def from_properties(name, properties, required=False): def from_properties(name, properties, required=False):
return int, numeric_properties_builder(properties, required) _mappings = {
"minimum": "ge",
"exclusiveMinimum": "gt",
"maximum": "le",
"exclusiveMaximum": "lt",
"multipleOf": "multiple_of",
"default": "default",
}
mapped_properties = mappings_properties_builder(properties, _mappings, required)
default_value = mapped_properties.get("default")
if default_value is not None:
IntTypeParser.validate_default(int, mapped_properties, default_value)
return int, mapped_properties

View File

@@ -66,38 +66,25 @@ class TestArrayTypeParser(TestCase):
properties = {"items": {"type": "string"}, "default": ["a", 1, "c"]} properties = {"items": {"type": "string"}, "default": ["a", 1, "c"]}
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"All items in the default list must be of type str",
)
def test_array_parser_with_invalid_default_type(self): def test_array_parser_with_invalid_default_type(self):
parser = ArrayTypeParser() parser = ArrayTypeParser()
properties = {"items": {"type": "string"}, "default": "not_a_list"} properties = {"items": {"type": "string"}, "default": "not_a_list"}
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception), "Default value must be a list, got str"
)
def test_array_parser_with_invalid_default_min(self): def test_array_parser_with_invalid_default_min(self):
parser = ArrayTypeParser() parser = ArrayTypeParser()
properties = {"items": {"type": "string"}, "default": ["a"], "minItems": 2} properties = {"items": {"type": "string"}, "default": ["a"], "minItems": 2}
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception), "Default list is below minItems limit of 2"
)
def test_array_parser_with_invalid_default_max(self): def test_array_parser_with_invalid_default_max(self):
parser = ArrayTypeParser() parser = ArrayTypeParser()
@@ -107,9 +94,5 @@ class TestArrayTypeParser(TestCase):
"maxItems": 3, "maxItems": 3,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception), "Default list exceeds maxItems limit of 3"
)

View File

@@ -61,14 +61,9 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 0.5, "multipleOf": 0.5,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value must be a number, got str",
)
def test_float_parser_with_default_invalid_maximum(self): def test_float_parser_with_default_invalid_maximum(self):
parser = FloatTypeParser() parser = FloatTypeParser()
@@ -80,14 +75,9 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 0.5, "multipleOf": 0.5,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value exceeds maximum limit of 10.5",
)
def test_float_parser_with_default_invalid_minimum(self): def test_float_parser_with_default_invalid_minimum(self):
parser = FloatTypeParser() parser = FloatTypeParser()
@@ -99,14 +89,9 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 0.5, "multipleOf": 0.5,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value is below minimum limit of 1.0",
)
def test_float_parser_with_default_invalid_exclusive_maximum(self): def test_float_parser_with_default_invalid_exclusive_maximum(self):
parser = FloatTypeParser() parser = FloatTypeParser()
@@ -118,14 +103,9 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 0.5, "multipleOf": 0.5,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value exceeds exclusive maximum limit of 10.5",
)
def test_float_parser_with_default_invalid_exclusive_minimum(self): def test_float_parser_with_default_invalid_exclusive_minimum(self):
parser = FloatTypeParser() parser = FloatTypeParser()
@@ -137,14 +117,9 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 0.5, "multipleOf": 0.5,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value is below exclusive minimum limit of 1.0",
)
def test_float_parser_with_default_invalid_multiple(self): def test_float_parser_with_default_invalid_multiple(self):
parser = FloatTypeParser() parser = FloatTypeParser()
@@ -156,10 +131,5 @@ class TestFloatTypeParser(TestCase):
"multipleOf": 2.0, "multipleOf": 2.0,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value 5.0 is not a multiple of 2.0",
)

View File

@@ -61,14 +61,9 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2, "multipleOf": 2,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value must be a number, got str",
)
def test_int_parser_with_default_invalid_maximum(self): def test_int_parser_with_default_invalid_maximum(self):
parser = IntTypeParser() parser = IntTypeParser()
@@ -80,14 +75,9 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2, "multipleOf": 2,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value exceeds maximum limit of 10",
)
def test_int_parser_with_default_invalid_minimum(self): def test_int_parser_with_default_invalid_minimum(self):
parser = IntTypeParser() parser = IntTypeParser()
@@ -99,14 +89,9 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2, "multipleOf": 2,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value is below minimum limit of 1",
)
def test_int_parser_with_default_invalid_exclusive_maximum(self): def test_int_parser_with_default_invalid_exclusive_maximum(self):
parser = IntTypeParser() parser = IntTypeParser()
@@ -118,14 +103,9 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2, "multipleOf": 2,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value exceeds exclusive maximum limit of 10",
)
def test_int_parser_with_default_invalid_exclusive_minimum(self): def test_int_parser_with_default_invalid_exclusive_minimum(self):
parser = IntTypeParser() parser = IntTypeParser()
@@ -137,14 +117,9 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2, "multipleOf": 2,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value is below exclusive minimum limit of 1",
)
def test_int_parser_with_default_invalid_multipleOf(self): def test_int_parser_with_default_invalid_multipleOf(self):
parser = IntTypeParser() parser = IntTypeParser()
@@ -156,10 +131,5 @@ class TestIntTypeParser(TestCase):
"multipleOf": 2, "multipleOf": 2,
} }
with self.assertRaises(ValueError) as context: with self.assertRaises(ValueError):
parser.from_properties("placeholder", properties) parser.from_properties("placeholder", properties)
self.assertEqual(
str(context.exception),
"Default value 5 is not a multiple of 2",
)