Skip to content

Commit

Permalink
add URL field (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
aminalaee authored Oct 13, 2021
1 parent 7b7ebec commit ea96a68
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ Validates IPv4 and IPv6 addresses.

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

### URL

Similar to `String` and takes the same arguments.
Represented in HTML forms as a `<input type="url">`.

## Boolean data types

### Boolean
Expand Down
11 changes: 11 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from typesystem.base import Message, ValidationError
from typesystem.fields import (
URL,
UUID,
Array,
Boolean,
Expand Down Expand Up @@ -878,3 +879,13 @@ def test_ipaddress():
"2001:0dz8:85a3:0000:0000:8a2e:0370:7334"
)
assert error == ValidationError(text="Must be a valid IP format.", code="format")


def test_url():
validator = URL()
value, error = validator.validate_or_error("https://example.com")
assert value == "https://example.com"

validator = URL()
value, error = validator.validate_or_error("example")
assert error == ValidationError(text="Must be a real URL.", code="invalid")
2 changes: 2 additions & 0 deletions tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"extra": typesystem.Boolean(default=True, read_only=True),
"email": typesystem.Email(),
"password": typesystem.Password(),
"url": typesystem.URL(),
}
)

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


def test_password_rendering():
Expand Down
11 changes: 11 additions & 0 deletions tests/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,3 +530,14 @@ def test_schema_ipaddress_serialization():
data = schema.serialize(item)
assert data["src"] == "127.0.0.1"
assert data["dst"] is None


def test_schema_url_serialization():
schema = typesystem.Schema(
fields={"url": typesystem.URL(), "website": typesystem.URL()}
)

item = {"url": "https://google.com", "website": None}
data = schema.serialize(item)
assert data["url"] == "https://google.com"
assert data["website"] is None
2 changes: 2 additions & 0 deletions typesystem/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typesystem.base import Message, ParseError, Position, ValidationError
from typesystem.fields import (
URL,
UUID,
Any,
Array,
Expand Down Expand Up @@ -51,6 +52,7 @@
"Text",
"Time",
"Union",
"URL",
"UUID",
# Schemas
"Schema",
Expand Down
6 changes: 6 additions & 0 deletions typesystem/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"uuid": formats.UUIDFormat(),
"email": formats.EmailFormat(),
"ipaddress": formats.IPAddressFormat(),
"url": formats.URLFormat(),
}


Expand Down Expand Up @@ -784,3 +785,8 @@ def __init__(self, **kwargs: typing.Any) -> None:
class IPAddress(String):
def __init__(self, **kwargs: typing.Any) -> None:
super().__init__(format="ipaddress", **kwargs)


class URL(String):
def __init__(self, **kwargs: typing.Any) -> None:
super().__init__(format="url", **kwargs)
21 changes: 21 additions & 0 deletions typesystem/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re
import typing
import uuid
from urllib.parse import urlparse

from typesystem.base import ValidationError

Expand Down Expand Up @@ -244,3 +245,23 @@ def serialize(
assert isinstance(obj, (ipaddress.IPv4Address, ipaddress.IPv6Address))

return str(obj)


class URLFormat(BaseFormat):
errors = {"invalid": "Must be a real URL."}

def is_native_type(self, value: typing.Any) -> bool:
return False

def validate(self, value: typing.Any) -> str:
url = urlparse(value)
if not all([url.scheme, url.netloc]):
raise self.validation_error("invalid")

return str(value)

def serialize(self, obj: typing.Optional[str]) -> typing.Optional[str]:
if obj is None:
return None

return obj

0 comments on commit ea96a68

Please sign in to comment.