Skip to content

Commit 55a01b2

Browse files
authored
✨ Add support for domain name string type (#212)
* Domain name string type * fix linting issues * fix lint by replacing double quotes with single quotes * fix linting issues (final) * add support for python 3.8 * fix incompatible imports * add proposed changes and test cases * add 100% coverage and remove prints from tests * add proper type validation for DomainStr and type tests
1 parent c7db9d7 commit 55a01b2

File tree

3 files changed

+156
-12
lines changed

3 files changed

+156
-12
lines changed

pydantic_extra_types/domain.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
The `domain_str` module provides the `DomainStr` data type.
3+
This class depends on the `pydantic` package and implements custom validation for domain string format.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import re
9+
from typing import Any, Mapping
10+
11+
from pydantic import GetCoreSchemaHandler
12+
from pydantic_core import PydanticCustomError, core_schema
13+
14+
15+
class DomainStr(str):
16+
"""
17+
A string subclass with custom validation for domain string format.
18+
"""
19+
20+
@classmethod
21+
def validate(cls, __input_value: Any, _: Any) -> str:
22+
"""
23+
Validate a domain name from the provided value.
24+
25+
Args:
26+
__input_value: The value to be validated.
27+
_: The source type to be converted.
28+
29+
Returns:
30+
str: The parsed domain name.
31+
32+
"""
33+
return cls._validate(__input_value)
34+
35+
@classmethod
36+
def _validate(cls, v: Any) -> DomainStr:
37+
if not isinstance(v, str):
38+
raise PydanticCustomError('domain_type', 'Value must be a string')
39+
40+
v = v.strip().lower()
41+
if len(v) < 1 or len(v) > 253:
42+
raise PydanticCustomError('domain_length', 'Domain must be between 1 and 253 characters')
43+
44+
pattern = r'^([a-z0-9-]+(\.[a-z0-9-]+)+)$'
45+
if not re.match(pattern, v):
46+
raise PydanticCustomError('domain_format', 'Invalid domain format')
47+
48+
return cls(v)
49+
50+
@classmethod
51+
def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
52+
return core_schema.with_info_before_validator_function(
53+
cls.validate,
54+
core_schema.str_schema(),
55+
)
56+
57+
@classmethod
58+
def __get_pydantic_json_schema__(
59+
cls, schema: core_schema.CoreSchema, handler: GetCoreSchemaHandler
60+
) -> Mapping[str, Any]:
61+
return handler(schema)

tests/test_domain.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from typing import Any
2+
3+
import pytest
4+
from pydantic import BaseModel, ValidationError
5+
6+
from pydantic_extra_types.domain import DomainStr
7+
8+
9+
class MyModel(BaseModel):
10+
domain: DomainStr
11+
12+
13+
valid_domains = [
14+
'example.com',
15+
'sub.example.com',
16+
'sub-domain.example-site.co.uk',
17+
'a.com',
18+
'x.com',
19+
'1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.com', # Multiple subdomains
20+
]
21+
22+
invalid_domains = [
23+
'', # Empty string
24+
'example', # Missing TLD
25+
'.com', # Missing domain name
26+
'example.', # Trailing dot
27+
'exam ple.com', # Space in domain
28+
'exa_mple.com', # Underscore in domain
29+
'example.com.', # Trailing dot
30+
]
31+
32+
very_long_domains = [
33+
'a' * 249 + '.com', # Just under the limit
34+
'a' * 250 + '.com', # At the limit
35+
'a' * 251 + '.com', # Just over the limit
36+
'sub1.sub2.sub3.sub4.sub5.sub6.sub7.sub8.sub9.sub10.sub11.sub12.sub13.sub14.sub15.sub16.sub17.sub18.sub19.sub20.sub21.sub22.sub23.sub24.sub25.sub26.sub27.sub28.sub29.sub30.sub31.sub32.sub33.extremely-long-domain-name-example-to-test-the-253-character-limit.com',
37+
]
38+
39+
invalid_domain_types = [1, 2, 1.1, 2.1, False, [], {}, None]
40+
41+
42+
@pytest.mark.parametrize('domain', valid_domains)
43+
def test_valid_domains(domain: str):
44+
try:
45+
MyModel.model_validate({'domain': domain})
46+
assert len(domain) < 254 and len(domain) > 0
47+
except ValidationError:
48+
assert len(domain) > 254 or len(domain) == 0
49+
50+
51+
@pytest.mark.parametrize('domain', invalid_domains)
52+
def test_invalid_domains(domain: str):
53+
try:
54+
MyModel.model_validate({'domain': domain})
55+
raise Exception(
56+
f"This test case has only samples that should raise a ValidationError. This domain '{domain}' did not raise such an exception."
57+
)
58+
except ValidationError:
59+
# An error is expected on this test
60+
pass
61+
62+
63+
@pytest.mark.parametrize('domain', very_long_domains)
64+
def test_very_long_domains(domain: str):
65+
try:
66+
MyModel.model_validate({'domain': domain})
67+
assert len(domain) < 254 and len(domain) > 0
68+
except ValidationError:
69+
# An error is expected on this test
70+
pass
71+
72+
73+
@pytest.mark.parametrize('domain', invalid_domain_types)
74+
def test_invalid_domain_types(domain: Any):
75+
with pytest.raises(ValidationError, match='Value must be a string'):
76+
MyModel(domain=domain)

tests/test_json_schema.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,11 @@
1313
import pydantic_extra_types
1414
from pydantic_extra_types.color import Color
1515
from pydantic_extra_types.coordinate import Coordinate, Latitude, Longitude
16-
from pydantic_extra_types.country import (
17-
CountryAlpha2,
18-
CountryAlpha3,
19-
CountryNumericCode,
20-
CountryShortName,
21-
)
16+
from pydantic_extra_types.country import CountryAlpha2, CountryAlpha3, CountryNumericCode, CountryShortName
2217
from pydantic_extra_types.currency_code import ISO4217, Currency
18+
from pydantic_extra_types.domain import DomainStr
2319
from pydantic_extra_types.isbn import ISBN
24-
from pydantic_extra_types.language_code import (
25-
ISO639_3,
26-
ISO639_5,
27-
LanguageAlpha2,
28-
LanguageName,
29-
)
20+
from pydantic_extra_types.language_code import ISO639_3, ISO639_5, LanguageAlpha2, LanguageName
3021
from pydantic_extra_types.mac_address import MacAddress
3122
from pydantic_extra_types.payment import PaymentCardNumber
3223
from pydantic_extra_types.pendulum_dt import DateTime
@@ -451,6 +442,22 @@
451442
],
452443
},
453444
),
445+
(
446+
DomainStr,
447+
{
448+
'title': 'Model',
449+
'type': 'object',
450+
'properties': {
451+
'x': {
452+
'title': 'X',
453+
'type': 'string',
454+
},
455+
},
456+
'required': [
457+
'x',
458+
],
459+
},
460+
),
454461
],
455462
)
456463
def test_json_schema(cls, expected):

0 commit comments

Comments
 (0)