@@ -18,6 +18,7 @@ class GenericTypeParser(ABC, Generic[T]):
|
||||
default_mappings = {
|
||||
"default": "default",
|
||||
"description": "description",
|
||||
"examples": "examples",
|
||||
}
|
||||
|
||||
@abstractmethod
|
||||
@@ -51,6 +52,11 @@ class GenericTypeParser(ABC, Generic[T]):
|
||||
"Default value is not valid", invalid_field=name
|
||||
)
|
||||
|
||||
if not self._validate_default(parsed_type, parsed_properties):
|
||||
raise InvalidSchemaException(
|
||||
"Examples values are not valid", invalid_field=name
|
||||
)
|
||||
|
||||
return parsed_type, parsed_properties
|
||||
|
||||
@classmethod
|
||||
@@ -127,3 +133,22 @@ class GenericTypeParser(ABC, Generic[T]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _validate_examples(field_type: T, field_prop: dict) -> bool:
|
||||
values = field_prop.get("examples")
|
||||
|
||||
if values is None:
|
||||
return True
|
||||
|
||||
if not isinstance(values, list):
|
||||
return False
|
||||
|
||||
try:
|
||||
field = Annotated[field_type, Field(**field_prop)] # type: ignore
|
||||
for value in values:
|
||||
TypeAdapter(field).validate_python(value)
|
||||
except Exception as _:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -13,6 +13,7 @@ class ConstTypeParser(GenericTypeParser):
|
||||
default_mappings = {
|
||||
"const": "default",
|
||||
"description": "description",
|
||||
"examples": "examples",
|
||||
}
|
||||
|
||||
def from_properties_impl(
|
||||
|
||||
@@ -41,4 +41,9 @@ class EnumTypeParser(GenericTypeParser):
|
||||
if "default" in parsed_properties and parsed_properties["default"] is not None:
|
||||
parsed_properties["default"] = enum_type(parsed_properties["default"])
|
||||
|
||||
if "examples" in parsed_properties:
|
||||
parsed_properties["examples"] = [
|
||||
enum_type(example) for example in parsed_properties["examples"]
|
||||
]
|
||||
|
||||
return enum_type, parsed_properties
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing_extensions import Unpack
|
||||
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
@@ -62,8 +63,38 @@ class StringTypeParser(GenericTypeParser):
|
||||
if format_type in self.format_pattern_mapping:
|
||||
mapped_properties["pattern"] = self.format_pattern_mapping[format_type]
|
||||
|
||||
print("A")
|
||||
if "examples" in mapped_properties:
|
||||
mapped_properties["examples"] = [
|
||||
self.__parse_example(example, format_type, mapped_type)
|
||||
for example in mapped_properties["examples"]
|
||||
]
|
||||
|
||||
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
|
||||
|
||||
def __parse_example(
|
||||
self, example: Any, format_type: str, mapped_type: type[Any]
|
||||
) -> Any:
|
||||
"""
|
||||
Parse example from JSON Schema format to python format
|
||||
:param example: Example Value
|
||||
:param format_type: Format Type
|
||||
:param mapped_type: Type to parse
|
||||
:return: Example parsed
|
||||
"""
|
||||
match format_type:
|
||||
case "date" | "time" | "date-time":
|
||||
return mapped_type.fromisoformat(example)
|
||||
case "duration":
|
||||
# TODO: Implement duration parser
|
||||
raise NotImplementedError
|
||||
case "ipv4" | "ipv6":
|
||||
return mapped_type(example)
|
||||
case "uuid":
|
||||
return mapped_type(example)
|
||||
case _:
|
||||
return example
|
||||
|
||||
@@ -42,3 +42,19 @@ class TestBoolTypeParser(TestCase):
|
||||
|
||||
with self.assertRaises(InvalidSchemaException):
|
||||
parser.from_properties_impl("placeholder", properties)
|
||||
|
||||
def test_bool_parser_with_examples(self):
|
||||
parser = BooleanTypeParser()
|
||||
|
||||
properties = {
|
||||
"type": "boolean",
|
||||
"examples": [True, False],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties_impl(
|
||||
"placeholder", properties
|
||||
)
|
||||
|
||||
self.assertEqual(type_parsing, bool)
|
||||
self.assertEqual(type_validator["default"], None)
|
||||
self.assertEqual(type_validator["examples"], [True, False])
|
||||
|
||||
@@ -12,7 +12,7 @@ class TestConstTypeParser(TestCase):
|
||||
parser = ConstTypeParser()
|
||||
|
||||
expected_const_value = "United States of America"
|
||||
properties = {"const": expected_const_value}
|
||||
properties = {"const": expected_const_value, "examples": [expected_const_value]}
|
||||
|
||||
parsed_type, parsed_properties = parser.from_properties_impl(
|
||||
"country", properties
|
||||
@@ -23,13 +23,14 @@ class TestConstTypeParser(TestCase):
|
||||
self.assertEqual(get_args(parsed_type), (expected_const_value,))
|
||||
|
||||
self.assertEqual(parsed_properties["default"], expected_const_value)
|
||||
self.assertEqual(parsed_properties["examples"], [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}
|
||||
properties = {"const": expected_const_value, "examples": [expected_const_value]}
|
||||
|
||||
parsed_type, parsed_properties = parser.from_properties_impl(
|
||||
"list_const", properties
|
||||
@@ -40,13 +41,14 @@ class TestConstTypeParser(TestCase):
|
||||
self.assertIn(list, get_args(parsed_type))
|
||||
|
||||
self.assertEqual(parsed_properties["default"], expected_const_value)
|
||||
self.assertEqual(parsed_properties["examples"], [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}
|
||||
properties = {"const": expected_const_value, "examples": [expected_const_value]}
|
||||
|
||||
parsed_type, parsed_properties = parser.from_properties_impl(
|
||||
"int_const", properties
|
||||
@@ -57,13 +59,14 @@ class TestConstTypeParser(TestCase):
|
||||
self.assertEqual(get_args(parsed_type), (expected_const_value,))
|
||||
|
||||
self.assertEqual(parsed_properties["default"], expected_const_value)
|
||||
self.assertEqual(parsed_properties["examples"], [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}
|
||||
properties = {"const": expected_const_value, "examples": [expected_const_value]}
|
||||
|
||||
parsed_type, parsed_properties = parser.from_properties_impl(
|
||||
"bool_const", properties
|
||||
@@ -74,6 +77,7 @@ class TestConstTypeParser(TestCase):
|
||||
self.assertEqual(get_args(parsed_type), (expected_const_value,))
|
||||
|
||||
self.assertEqual(parsed_properties["default"], expected_const_value)
|
||||
self.assertEqual(parsed_properties["examples"], [expected_const_value])
|
||||
|
||||
def test_const_type_parser_invalid_properties(self):
|
||||
parser = ConstTypeParser()
|
||||
|
||||
@@ -89,3 +89,27 @@ class TestEnumTypeParser(TestCase):
|
||||
|
||||
with self.assertRaises(InvalidSchemaException):
|
||||
parser.from_properties_impl("TestEnum", schema)
|
||||
|
||||
def test_enum_type_parser_creates_enum_with_examples(self):
|
||||
parser = EnumTypeParser()
|
||||
|
||||
schema = {
|
||||
"enum": ["value1", "value2", "value3"],
|
||||
"examples": ["value1", "value3"],
|
||||
}
|
||||
|
||||
parsed_type, parsed_properties = parser.from_properties_impl(
|
||||
"TestEnum",
|
||||
schema,
|
||||
)
|
||||
|
||||
self.assertIsInstance(parsed_type, type)
|
||||
self.assertTrue(issubclass(parsed_type, Enum))
|
||||
self.assertEqual(
|
||||
set(parsed_type.__members__.keys()), {"VALUE1", "VALUE2", "VALUE3"}
|
||||
)
|
||||
self.assertEqual(parsed_properties["default"], None)
|
||||
self.assertEqual(
|
||||
parsed_properties["examples"],
|
||||
[getattr(parsed_type, "VALUE1"), getattr(parsed_type, "VALUE3")],
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ class TestFloatTypeParser(TestCase):
|
||||
"maximum": 10.5,
|
||||
"minimum": 1.0,
|
||||
"multipleOf": 0.5,
|
||||
"examples": [1.5, 2.5],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
@@ -31,6 +32,7 @@ class TestFloatTypeParser(TestCase):
|
||||
self.assertEqual(type_validator["le"], 10.5)
|
||||
self.assertEqual(type_validator["ge"], 1.0)
|
||||
self.assertEqual(type_validator["multiple_of"], 0.5)
|
||||
self.assertEqual(type_validator["examples"], [1.5, 2.5])
|
||||
|
||||
def test_float_parser_with_default(self):
|
||||
parser = FloatTypeParser()
|
||||
|
||||
@@ -23,6 +23,7 @@ class TestIntTypeParser(TestCase):
|
||||
"maximum": 10,
|
||||
"minimum": 1,
|
||||
"multipleOf": 2,
|
||||
"examples": [2, 4],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
@@ -31,6 +32,7 @@ class TestIntTypeParser(TestCase):
|
||||
self.assertEqual(type_validator["le"], 10)
|
||||
self.assertEqual(type_validator["ge"], 1)
|
||||
self.assertEqual(type_validator["multiple_of"], 2)
|
||||
self.assertEqual(type_validator["examples"], [2, 4])
|
||||
|
||||
def test_int_parser_with_default(self):
|
||||
parser = IntTypeParser()
|
||||
|
||||
@@ -16,6 +16,22 @@ class TestNullTypeParser(TestCase):
|
||||
self.assertEqual(type_parsing, type(None))
|
||||
self.assertEqual(type_validator, {"default": None})
|
||||
|
||||
def test_null_parser_with_examples(self):
|
||||
parser = NullTypeParser()
|
||||
|
||||
properties = {
|
||||
"type": "null",
|
||||
"examples": [None],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties_impl(
|
||||
"placeholder", properties
|
||||
)
|
||||
|
||||
self.assertEqual(type_parsing, type(None))
|
||||
self.assertEqual(type_validator["default"], None)
|
||||
self.assertEqual(type_validator["examples"], [None])
|
||||
|
||||
def test_null_parser_with_invalid_default(self):
|
||||
parser = NullTypeParser()
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ from jambo.parser import StringTypeParser
|
||||
|
||||
from pydantic import AnyUrl, EmailStr
|
||||
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
from ipaddress import IPv4Address, IPv6Address, ip_address
|
||||
from unittest import TestCase
|
||||
from uuid import UUID
|
||||
|
||||
@@ -27,6 +27,7 @@ class TestStringTypeParser(TestCase):
|
||||
"maxLength": 10,
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-zA-Z]+$",
|
||||
"examples": ["test", "TEST"],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
@@ -35,6 +36,7 @@ class TestStringTypeParser(TestCase):
|
||||
self.assertEqual(type_validator["max_length"], 10)
|
||||
self.assertEqual(type_validator["min_length"], 1)
|
||||
self.assertEqual(type_validator["pattern"], "^[a-zA-Z]+$")
|
||||
self.assertEqual(type_validator["examples"], ["test", "TEST"])
|
||||
|
||||
def test_string_parser_with_default_value(self):
|
||||
parser = StringTypeParser()
|
||||
@@ -98,11 +100,13 @@ class TestStringTypeParser(TestCase):
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"examples": ["test@example.com"],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, EmailStr)
|
||||
self.assertEqual(type_validator["examples"], ["test@example.com"])
|
||||
|
||||
def test_string_parser_with_uri_format(self):
|
||||
parser = StringTypeParser()
|
||||
@@ -110,21 +114,27 @@ class TestStringTypeParser(TestCase):
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"examples": ["test://domain/resource"],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, AnyUrl)
|
||||
self.assertEqual(type_validator["examples"], ["test://domain/resource"])
|
||||
|
||||
def test_string_parser_with_ip_formats(self):
|
||||
parser = StringTypeParser()
|
||||
|
||||
formats = {"ipv4": IPv4Address, "ipv6": IPv6Address}
|
||||
examples = {"ipv4": "192.168.1.1", "ipv6": "::1"}
|
||||
|
||||
for ip_format, expected_type in formats.items():
|
||||
example = examples[ip_format]
|
||||
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": ip_format,
|
||||
"examples": [example],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties(
|
||||
@@ -132,6 +142,7 @@ class TestStringTypeParser(TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(type_parsing, expected_type)
|
||||
self.assertEqual(type_validator["examples"], [ip_address(example)])
|
||||
|
||||
def test_string_parser_with_uuid_format(self):
|
||||
parser = StringTypeParser()
|
||||
@@ -139,11 +150,15 @@ class TestStringTypeParser(TestCase):
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"examples": ["ab71aaf4-ab6e-43cd-a369-cebdd9f7a4c6"],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, UUID)
|
||||
self.assertEqual(
|
||||
type_validator["examples"], [UUID("ab71aaf4-ab6e-43cd-a369-cebdd9f7a4c6")]
|
||||
)
|
||||
|
||||
def test_string_parser_with_time_format(self):
|
||||
parser = StringTypeParser()
|
||||
@@ -151,19 +166,34 @@ class TestStringTypeParser(TestCase):
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "time",
|
||||
"examples": ["14:30:00", "09:15:30.500", "23:59:59Z", "10:00:00+02:00"],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, time)
|
||||
self.assertEqual(
|
||||
type_validator["examples"],
|
||||
[
|
||||
time(hour=14, minute=30, second=0),
|
||||
time(hour=9, minute=15, second=30, microsecond=500_000),
|
||||
time(hour=23, minute=59, second=59, tzinfo=timezone.utc),
|
||||
time(hour=10, minute=0, second=0, tzinfo=timezone(timedelta(hours=2))),
|
||||
],
|
||||
)
|
||||
|
||||
def test_string_parser_with_pattern_based_formats(self):
|
||||
parser = StringTypeParser()
|
||||
|
||||
for format_type in ["hostname"]:
|
||||
format_types = {
|
||||
"hostname": "hostname_example",
|
||||
}
|
||||
|
||||
for format_type, example_type in format_types.items():
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": format_type,
|
||||
"examples": [example_type],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties(
|
||||
@@ -175,6 +205,7 @@ class TestStringTypeParser(TestCase):
|
||||
self.assertEqual(
|
||||
type_validator["pattern"], parser.format_pattern_mapping[format_type]
|
||||
)
|
||||
self.assertEqual(type_validator["examples"], [example_type])
|
||||
|
||||
def test_string_parser_with_unsupported_format(self):
|
||||
parser = StringTypeParser()
|
||||
@@ -198,11 +229,20 @@ class TestStringTypeParser(TestCase):
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"examples": ["2025-11-17", "1999-12-31", "2000-01-01"],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, date)
|
||||
self.assertEqual(
|
||||
type_validator["examples"],
|
||||
[
|
||||
date(year=2025, month=11, day=17),
|
||||
date(year=1999, month=12, day=31),
|
||||
date(year=2000, month=1, day=1),
|
||||
],
|
||||
)
|
||||
|
||||
def test_string_parser_with_datetime_format(self):
|
||||
parser = StringTypeParser()
|
||||
@@ -210,11 +250,51 @@ class TestStringTypeParser(TestCase):
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"examples": [
|
||||
"2025-11-17T11:15:00",
|
||||
"2025-11-17T11:15:00Z",
|
||||
"2025-11-17T11:15:00+01:00",
|
||||
"2025-11-17T11:15:00.123456-05:00",
|
||||
],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, datetime)
|
||||
self.assertEqual(
|
||||
type_validator["examples"],
|
||||
[
|
||||
datetime(year=2025, month=11, day=17, hour=11, minute=15, second=0),
|
||||
datetime(
|
||||
year=2025,
|
||||
month=11,
|
||||
day=17,
|
||||
hour=11,
|
||||
minute=15,
|
||||
second=0,
|
||||
tzinfo=timezone.utc,
|
||||
),
|
||||
datetime(
|
||||
year=2025,
|
||||
month=11,
|
||||
day=17,
|
||||
hour=11,
|
||||
minute=15,
|
||||
second=0,
|
||||
tzinfo=timezone(timedelta(hours=1)),
|
||||
),
|
||||
datetime(
|
||||
year=2025,
|
||||
month=11,
|
||||
day=17,
|
||||
hour=11,
|
||||
minute=15,
|
||||
second=0,
|
||||
microsecond=123456,
|
||||
tzinfo=timezone(timedelta(hours=-5)),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def test_string_parser_with_timedelta_format(self):
|
||||
parser = StringTypeParser()
|
||||
@@ -222,8 +302,18 @@ class TestStringTypeParser(TestCase):
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "duration",
|
||||
"examples": ["P1Y2M3DT4H5M6S", "PT30M", "P7D", "PT0.5S"],
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, timedelta)
|
||||
self.assertEqual(
|
||||
type_validator["examples"],
|
||||
[
|
||||
timedelta(days=7),
|
||||
timedelta(minutes=30),
|
||||
timedelta(hours=4, minutes=5, seconds=6),
|
||||
timedelta(seconds=0.5),
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user