Skip to content
This repository was archived by the owner on Aug 19, 2025. It is now read-only.

Commit 202bb14

Browse files
authored
add email field (#111)
1 parent 851b60f commit 202bb14

6 files changed

Lines changed: 53 additions & 0 deletions

File tree

tests/test_fields.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Date,
1414
DateTime,
1515
Decimal,
16+
Email,
1617
Float,
1718
Integer,
1819
Number,
@@ -835,6 +836,16 @@ def test_validation_error_is_hashable():
835836
hash(error)
836837

837838

839+
def test_email():
840+
validator = Email()
841+
value, error = validator.validate_or_error("info@example.com")
842+
assert value == "info@example.com"
843+
844+
validator = Email()
845+
value, error = validator.validate_or_error("example.com")
846+
assert error == ValidationError(text="Must be a valid email format.", code="format")
847+
848+
838849
def test_password():
839850
validator = Password()
840851
value, _ = validator.validate_or_error("secret")

tests/test_forms.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
choices=[("abc", "Abc"), ("def", "Def"), ("ghi", "Ghi")]
1414
),
1515
"extra": typesystem.Boolean(default=True, read_only=True),
16+
"email": typesystem.Email(),
1617
"password": typesystem.Password(),
1718
}
1819
)
@@ -30,6 +31,7 @@ def test_form_rendering():
3031
assert html.count('<input type="text" ') == 1
3132
assert html.count("<textarea ") == 1
3233
assert html.count("<select ") == 1
34+
assert html.count('<input type="email" ') == 1
3335
assert html.count('<input type="password" ') == 1
3436

3537

tests/test_schemas.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,3 +500,11 @@ def test_definitions_to_json_schema():
500500
},
501501
}
502502
}
503+
504+
505+
def test_schema_email_serialization():
506+
user = typesystem.Schema(fields={"email": typesystem.Email()})
507+
508+
item = {"email": "team@encode.io"}
509+
data = user.serialize(item)
510+
assert data == {"email": "team@encode.io"}

typesystem/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Date,
99
DateTime,
1010
Decimal,
11+
Email,
1112
Field,
1213
Float,
1314
Integer,
@@ -35,6 +36,7 @@
3536
"Date",
3637
"DateTime",
3738
"Decimal",
39+
"Email",
3840
"Integer",
3941
"Jinja2Forms",
4042
"Field",

typesystem/fields.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"time": formats.TimeFormat(),
1515
"datetime": formats.DateTimeFormat(),
1616
"uuid": formats.UUIDFormat(),
17+
"email": formats.EmailFormat(),
1718
}
1819

1920

@@ -769,6 +770,11 @@ def __init__(self, **kwargs: typing.Any) -> None:
769770
super().__init__(format="uuid", **kwargs)
770771

771772

773+
class Email(String):
774+
def __init__(self, **kwargs: typing.Any) -> None:
775+
super().__init__(format="email", **kwargs)
776+
777+
772778
class Password(String):
773779
def __init__(self, **kwargs: typing.Any) -> None:
774780
super().__init__(format="password", **kwargs)

typesystem/formats.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@
2323
r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
2424
)
2525

26+
EMAIL_REGEX = re.compile(
27+
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"
28+
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*'
29+
r")@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,63}(?<!-)\.?$",
30+
re.IGNORECASE,
31+
)
32+
2633

2734
class BaseFormat:
2835
errors: typing.Dict[str, str] = {}
@@ -169,3 +176,20 @@ def validate(self, value: typing.Any) -> uuid.UUID:
169176

170177
def serialize(self, obj: typing.Any) -> str:
171178
return str(obj)
179+
180+
181+
class EmailFormat(BaseFormat):
182+
errors = {"format": "Must be a valid email format."}
183+
184+
def is_native_type(self, value: typing.Any) -> bool:
185+
return False
186+
187+
def validate(self, value: typing.Any) -> uuid.UUID:
188+
match = EMAIL_REGEX.match(value)
189+
if not match:
190+
raise self.validation_error("format")
191+
192+
return value
193+
194+
def serialize(self, obj: typing.Any) -> str:
195+
return str(obj)

0 commit comments

Comments
 (0)