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

Commit ea96a68

Browse files
authored
add URL field (#117)
1 parent 7b7ebec commit ea96a68

7 files changed

Lines changed: 58 additions & 0 deletions

File tree

docs/fields.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ Validates IPv4 and IPv6 addresses.
9494

9595
Returns `ipaddress.IPv4Address` or `ipaddress.IPv6Address` based on input.
9696

97+
### URL
98+
99+
Similar to `String` and takes the same arguments.
100+
Represented in HTML forms as a `<input type="url">`.
101+
97102
## Boolean data types
98103

99104
### Boolean

tests/test_fields.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from typesystem.base import Message, ValidationError
88
from typesystem.fields import (
9+
URL,
910
UUID,
1011
Array,
1112
Boolean,
@@ -878,3 +879,13 @@ def test_ipaddress():
878879
"2001:0dz8:85a3:0000:0000:8a2e:0370:7334"
879880
)
880881
assert error == ValidationError(text="Must be a valid IP format.", code="format")
882+
883+
884+
def test_url():
885+
validator = URL()
886+
value, error = validator.validate_or_error("https://example.com")
887+
assert value == "https://example.com"
888+
889+
validator = URL()
890+
value, error = validator.validate_or_error("example")
891+
assert error == ValidationError(text="Must be a real URL.", code="invalid")

tests/test_forms.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"extra": typesystem.Boolean(default=True, read_only=True),
1616
"email": typesystem.Email(),
1717
"password": typesystem.Password(),
18+
"url": typesystem.URL(),
1819
}
1920
)
2021

@@ -33,6 +34,7 @@ def test_form_rendering():
3334
assert html.count("<select ") == 1
3435
assert html.count('<input type="email" ') == 1
3536
assert html.count('<input type="password" ') == 1
37+
assert html.count('<input type="url" ') == 1
3638

3739

3840
def test_password_rendering():

tests/test_schemas.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,3 +530,14 @@ def test_schema_ipaddress_serialization():
530530
data = schema.serialize(item)
531531
assert data["src"] == "127.0.0.1"
532532
assert data["dst"] is None
533+
534+
535+
def test_schema_url_serialization():
536+
schema = typesystem.Schema(
537+
fields={"url": typesystem.URL(), "website": typesystem.URL()}
538+
)
539+
540+
item = {"url": "https://google.com", "website": None}
541+
data = schema.serialize(item)
542+
assert data["url"] == "https://google.com"
543+
assert data["website"] is None

typesystem/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typesystem.base import Message, ParseError, Position, ValidationError
22
from typesystem.fields import (
3+
URL,
34
UUID,
45
Any,
56
Array,
@@ -51,6 +52,7 @@
5152
"Text",
5253
"Time",
5354
"Union",
55+
"URL",
5456
"UUID",
5557
# Schemas
5658
"Schema",

typesystem/fields.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"uuid": formats.UUIDFormat(),
1717
"email": formats.EmailFormat(),
1818
"ipaddress": formats.IPAddressFormat(),
19+
"url": formats.URLFormat(),
1920
}
2021

2122

@@ -784,3 +785,8 @@ def __init__(self, **kwargs: typing.Any) -> None:
784785
class IPAddress(String):
785786
def __init__(self, **kwargs: typing.Any) -> None:
786787
super().__init__(format="ipaddress", **kwargs)
788+
789+
790+
class URL(String):
791+
def __init__(self, **kwargs: typing.Any) -> None:
792+
super().__init__(format="url", **kwargs)

typesystem/formats.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import re
44
import typing
55
import uuid
6+
from urllib.parse import urlparse
67

78
from typesystem.base import ValidationError
89

@@ -244,3 +245,23 @@ def serialize(
244245
assert isinstance(obj, (ipaddress.IPv4Address, ipaddress.IPv6Address))
245246

246247
return str(obj)
248+
249+
250+
class URLFormat(BaseFormat):
251+
errors = {"invalid": "Must be a real URL."}
252+
253+
def is_native_type(self, value: typing.Any) -> bool:
254+
return False
255+
256+
def validate(self, value: typing.Any) -> str:
257+
url = urlparse(value)
258+
if not all([url.scheme, url.netloc]):
259+
raise self.validation_error("invalid")
260+
261+
return str(value)
262+
263+
def serialize(self, obj: typing.Optional[str]) -> typing.Optional[str]:
264+
if obj is None:
265+
return None
266+
267+
return obj

0 commit comments

Comments
 (0)