feat(parser): first‑class support for JSON string.format #12
@@ -1,5 +1,9 @@
|
||||
from jambo.parser._type_parser import GenericTypeParser
|
||||
|
||||
from pydantic import EmailStr, HttpUrl, IPvAnyAddress
|
||||
|
||||
from datetime import date, datetime, time
|
||||
|
||||
|
||||
class StringTypeParser(GenericTypeParser):
|
||||
mapped_type = str
|
||||
@@ -10,13 +14,42 @@ class StringTypeParser(GenericTypeParser):
|
||||
"maxLength": "max_length",
|
||||
"minLength": "min_length",
|
||||
"pattern": "pattern",
|
||||
"format": "format",
|
||||
}
|
||||
|
||||
format_type_mapping = {
|
||||
"email": EmailStr,
|
||||
"uri": HttpUrl,
|
||||
"ipv4": IPvAnyAddress,
|
||||
"ipv6": IPvAnyAddress,
|
||||
"hostname": str,
|
||||
"date": date,
|
||||
"time": time,
|
||||
"date-time": datetime,
|
||||
}
|
||||
|
||||
format_pattern_mapping = {
|
||||
"hostname": r"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$",
|
||||
}
|
||||
|
||||
def from_properties(self, name, properties, required=False):
|
||||
mapped_properties = self.mappings_properties_builder(properties, required)
|
||||
|
||||
format_type = properties.get("format")
|
||||
if format_type:
|
||||
if format_type in self.format_type_mapping:
|
||||
mapped_type = self.format_type_mapping[format_type]
|
||||
if format_type in self.format_pattern_mapping:
|
||||
mapped_properties["pattern"] = self.format_pattern_mapping[
|
||||
format_type
|
||||
]
|
||||
else:
|
||||
raise ValueError(f"Unsupported string format: {format_type}")
|
||||
else:
|
||||
mapped_type = str
|
||||
|
||||
default_value = properties.get("default")
|
||||
if default_value is not None:
|
||||
self.validate_default(str, mapped_properties, default_value)
|
||||
self.validate_default(mapped_type, mapped_properties, default_value)
|
||||
|
||||
return str, mapped_properties
|
||||
return mapped_type, mapped_properties
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from jambo.parser import StringTypeParser
|
||||
|
||||
from pydantic import EmailStr, HttpUrl, IPvAnyAddress
|
||||
|
||||
from datetime import date, datetime, time
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
@@ -85,3 +88,112 @@ class TestStringTypeParser(TestCase):
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
parser.from_properties("placeholder", properties)
|
||||
|
||||
def test_string_parser_with_email_format(self):
|
||||
parser = StringTypeParser()
|
||||
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, EmailStr)
|
||||
|
||||
def test_string_parser_with_uri_format(self):
|
||||
parser = StringTypeParser()
|
||||
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, HttpUrl)
|
||||
|
||||
def test_string_parser_with_ip_formats(self):
|
||||
parser = StringTypeParser()
|
||||
|
||||
for ip_format in ["ipv4", "ipv6"]:
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": ip_format,
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties(
|
||||
"placeholder", properties
|
||||
)
|
||||
|
||||
self.assertEqual(type_parsing, IPvAnyAddress)
|
||||
|
||||
def test_string_parser_with_time_format(self):
|
||||
parser = StringTypeParser()
|
||||
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "time",
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, time)
|
||||
|
||||
def test_string_parser_with_pattern_based_formats(self):
|
||||
parser = StringTypeParser()
|
||||
|
||||
for format_type in ["hostname"]:
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": format_type,
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties(
|
||||
"placeholder", properties
|
||||
)
|
||||
|
||||
self.assertEqual(type_parsing, str)
|
||||
self.assertIn("pattern", type_validator)
|
||||
self.assertEqual(
|
||||
type_validator["pattern"], parser.format_pattern_mapping[format_type]
|
||||
)
|
||||
|
||||
def test_string_parser_with_unsupported_format(self):
|
||||
parser = StringTypeParser()
|
||||
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "unsupported-format",
|
||||
}
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(
|
||||
str(context.exception), "Unsupported string format: unsupported-format"
|
||||
)
|
||||
|
||||
def test_string_parser_with_date_format(self):
|
||||
parser = StringTypeParser()
|
||||
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, date)
|
||||
|
||||
def test_string_parser_with_datetime_format(self):
|
||||
parser = StringTypeParser()
|
||||
|
||||
properties = {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
}
|
||||
|
||||
type_parsing, type_validator = parser.from_properties("placeholder", properties)
|
||||
|
||||
self.assertEqual(type_parsing, datetime)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from jambo import SchemaConverter
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
from unittest import TestCase
|
||||
|
||||
|
||||
@@ -397,3 +398,99 @@ class TestSchemaConverter(TestCase):
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
Model(id=11)
|
||||
|
||||
def test_string_format_email(self):
|
||||
schema = {
|
||||
"title": "EmailTest",
|
||||
"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):
|
||||
model(email="invalid-email")
|
||||
|
||||
def test_string_format_uri(self):
|
||||
schema = {
|
||||
"title": "UriTest",
|
||||
"type": "object",
|
||||
"properties": {"website": {"type": "string", "format": "uri"}},
|
||||
}
|
||||
model = SchemaConverter.build(schema)
|
||||
self.assertEqual(
|
||||
model(website="https://example.com").website, HttpUrl("https://example.com")
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
model(website="invalid-uri")
|
||||
|
||||
def test_string_format_ipv4(self):
|
||||
schema = {
|
||||
"title": "IPv4Test",
|
||||
"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):
|
||||
model(ip="256.256.256.256")
|
||||
|
||||
def test_string_format_ipv6(self):
|
||||
schema = {
|
||||
"title": "IPv6Test",
|
||||
"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):
|
||||
model(ip="invalid-ipv6")
|
||||
|
||||
def test_string_format_hostname(self):
|
||||
schema = {
|
||||
"title": "HostnameTest",
|
||||
"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):
|
||||
model(hostname="invalid..hostname")
|
||||
|
||||
def test_string_format_datetime(self):
|
||||
schema = {
|
||||
"title": "DateTimeTest",
|
||||
"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):
|
||||
model(timestamp="invalid-datetime")
|
||||
|
||||
def test_string_format_time(self):
|
||||
schema = {
|
||||
"title": "TimeTest",
|
||||
"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):
|
||||
model(time="25:00:00")
|
||||
|
||||
def test_string_format_unsupported(self):
|
||||
schema = {
|
||||
"title": "InvalidFormat",
|
||||
"type": "object",
|
||||
"properties": {"field": {"type": "string", "format": "unsupported"}},
|
||||
}
|
||||
with self.assertRaises(ValueError):
|
||||
SchemaConverter.build(schema)
|
||||
|
||||
Reference in New Issue
Block a user