diff --git a/tests/test_fields.py b/tests/test_fields.py index d391eda..91a1d47 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -13,6 +13,7 @@ Date, DateTime, Decimal, + Email, Float, Integer, Number, @@ -835,6 +836,16 @@ def test_validation_error_is_hashable(): hash(error) +def test_email(): + validator = Email() + value, error = validator.validate_or_error("info@example.com") + assert value == "info@example.com" + + validator = Email() + value, error = validator.validate_or_error("example.com") + assert error == ValidationError(text="Must be a valid email format.", code="format") + + def test_password(): validator = Password() value, _ = validator.validate_or_error("secret") diff --git a/tests/test_forms.py b/tests/test_forms.py index 33fa264..e93f010 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -13,6 +13,7 @@ choices=[("abc", "Abc"), ("def", "Def"), ("ghi", "Ghi")] ), "extra": typesystem.Boolean(default=True, read_only=True), + "email": typesystem.Email(), "password": typesystem.Password(), } ) @@ -30,6 +31,7 @@ def test_form_rendering(): assert html.count(' None: super().__init__(format="uuid", **kwargs) +class Email(String): + def __init__(self, **kwargs: typing.Any) -> None: + super().__init__(format="email", **kwargs) + + class Password(String): def __init__(self, **kwargs: typing.Any) -> None: super().__init__(format="password", **kwargs) diff --git a/typesystem/formats.py b/typesystem/formats.py index 9f56ca3..ed8c5cc 100644 --- a/typesystem/formats.py +++ b/typesystem/formats.py @@ -23,6 +23,13 @@ r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}" ) +EMAIL_REGEX = re.compile( + r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" + r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*' + r")@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,63}(? uuid.UUID: def serialize(self, obj: typing.Any) -> str: return str(obj) + + +class EmailFormat(BaseFormat): + errors = {"format": "Must be a valid email format."} + + def is_native_type(self, value: typing.Any) -> bool: + return False + + def validate(self, value: typing.Any) -> uuid.UUID: + match = EMAIL_REGEX.match(value) + if not match: + raise self.validation_error("format") + + return value + + def serialize(self, obj: typing.Any) -> str: + return str(obj)