-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ 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
- Loading branch information
1 parent
c7db9d7
commit 55a01b2
Showing
3 changed files
with
156 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
""" | ||
The `domain_str` module provides the `DomainStr` data type. | ||
This class depends on the `pydantic` package and implements custom validation for domain string format. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
import re | ||
from typing import Any, Mapping | ||
|
||
from pydantic import GetCoreSchemaHandler | ||
from pydantic_core import PydanticCustomError, core_schema | ||
|
||
|
||
class DomainStr(str): | ||
""" | ||
A string subclass with custom validation for domain string format. | ||
""" | ||
|
||
@classmethod | ||
def validate(cls, __input_value: Any, _: Any) -> str: | ||
""" | ||
Validate a domain name from the provided value. | ||
Args: | ||
__input_value: The value to be validated. | ||
_: The source type to be converted. | ||
Returns: | ||
str: The parsed domain name. | ||
""" | ||
return cls._validate(__input_value) | ||
|
||
@classmethod | ||
def _validate(cls, v: Any) -> DomainStr: | ||
if not isinstance(v, str): | ||
raise PydanticCustomError('domain_type', 'Value must be a string') | ||
|
||
v = v.strip().lower() | ||
if len(v) < 1 or len(v) > 253: | ||
raise PydanticCustomError('domain_length', 'Domain must be between 1 and 253 characters') | ||
|
||
pattern = r'^([a-z0-9-]+(\.[a-z0-9-]+)+)$' | ||
if not re.match(pattern, v): | ||
raise PydanticCustomError('domain_format', 'Invalid domain format') | ||
|
||
return cls(v) | ||
|
||
@classmethod | ||
def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema: | ||
return core_schema.with_info_before_validator_function( | ||
cls.validate, | ||
core_schema.str_schema(), | ||
) | ||
|
||
@classmethod | ||
def __get_pydantic_json_schema__( | ||
cls, schema: core_schema.CoreSchema, handler: GetCoreSchemaHandler | ||
) -> Mapping[str, Any]: | ||
return handler(schema) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
from typing import Any | ||
|
||
import pytest | ||
from pydantic import BaseModel, ValidationError | ||
|
||
from pydantic_extra_types.domain import DomainStr | ||
|
||
|
||
class MyModel(BaseModel): | ||
domain: DomainStr | ||
|
||
|
||
valid_domains = [ | ||
'example.com', | ||
'sub.example.com', | ||
'sub-domain.example-site.co.uk', | ||
'a.com', | ||
'x.com', | ||
'1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.com', # Multiple subdomains | ||
] | ||
|
||
invalid_domains = [ | ||
'', # Empty string | ||
'example', # Missing TLD | ||
'.com', # Missing domain name | ||
'example.', # Trailing dot | ||
'exam ple.com', # Space in domain | ||
'exa_mple.com', # Underscore in domain | ||
'example.com.', # Trailing dot | ||
] | ||
|
||
very_long_domains = [ | ||
'a' * 249 + '.com', # Just under the limit | ||
'a' * 250 + '.com', # At the limit | ||
'a' * 251 + '.com', # Just over the limit | ||
'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', | ||
] | ||
|
||
invalid_domain_types = [1, 2, 1.1, 2.1, False, [], {}, None] | ||
|
||
|
||
@pytest.mark.parametrize('domain', valid_domains) | ||
def test_valid_domains(domain: str): | ||
try: | ||
MyModel.model_validate({'domain': domain}) | ||
assert len(domain) < 254 and len(domain) > 0 | ||
except ValidationError: | ||
assert len(domain) > 254 or len(domain) == 0 | ||
|
||
|
||
@pytest.mark.parametrize('domain', invalid_domains) | ||
def test_invalid_domains(domain: str): | ||
try: | ||
MyModel.model_validate({'domain': domain}) | ||
raise Exception( | ||
f"This test case has only samples that should raise a ValidationError. This domain '{domain}' did not raise such an exception." | ||
) | ||
except ValidationError: | ||
# An error is expected on this test | ||
pass | ||
|
||
|
||
@pytest.mark.parametrize('domain', very_long_domains) | ||
def test_very_long_domains(domain: str): | ||
try: | ||
MyModel.model_validate({'domain': domain}) | ||
assert len(domain) < 254 and len(domain) > 0 | ||
except ValidationError: | ||
# An error is expected on this test | ||
pass | ||
|
||
|
||
@pytest.mark.parametrize('domain', invalid_domain_types) | ||
def test_invalid_domain_types(domain: Any): | ||
with pytest.raises(ValidationError, match='Value must be a string'): | ||
MyModel(domain=domain) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters