feat(examples): Add examples for primitive types

Refs: #52
This commit is contained in:
JCHacking
2025-11-17 23:41:16 +01:00
parent 81c149120e
commit 43ce95cc9a
11 changed files with 223 additions and 7 deletions

View File

@@ -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

View File

@@ -13,6 +13,7 @@ class ConstTypeParser(GenericTypeParser):
default_mappings = {
"const": "default",
"description": "description",
"examples": "examples",
}
def from_properties_impl(

View File

@@ -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

View File

@@ -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

View File

@@ -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])

View File

@@ -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()

View File

@@ -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")],
)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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),
],
)