Skip to content

Commit

Permalink
✨ Add support for domain name string type (#212)
Browse files Browse the repository at this point in the history
* 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
matter1-git authored Sep 6, 2024
1 parent c7db9d7 commit 55a01b2
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 12 deletions.
61 changes: 61 additions & 0 deletions pydantic_extra_types/domain.py
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)
76 changes: 76 additions & 0 deletions tests/test_domain.py
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)
31 changes: 19 additions & 12 deletions tests/test_json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,11 @@
import pydantic_extra_types
from pydantic_extra_types.color import Color
from pydantic_extra_types.coordinate import Coordinate, Latitude, Longitude
from pydantic_extra_types.country import (
CountryAlpha2,
CountryAlpha3,
CountryNumericCode,
CountryShortName,
)
from pydantic_extra_types.country import CountryAlpha2, CountryAlpha3, CountryNumericCode, CountryShortName
from pydantic_extra_types.currency_code import ISO4217, Currency
from pydantic_extra_types.domain import DomainStr
from pydantic_extra_types.isbn import ISBN
from pydantic_extra_types.language_code import (
ISO639_3,
ISO639_5,
LanguageAlpha2,
LanguageName,
)
from pydantic_extra_types.language_code import ISO639_3, ISO639_5, LanguageAlpha2, LanguageName
from pydantic_extra_types.mac_address import MacAddress
from pydantic_extra_types.payment import PaymentCardNumber
from pydantic_extra_types.pendulum_dt import DateTime
Expand Down Expand Up @@ -451,6 +442,22 @@
],
},
),
(
DomainStr,
{
'title': 'Model',
'type': 'object',
'properties': {
'x': {
'title': 'X',
'type': 'string',
},
},
'required': [
'x',
],
},
),
],
)
def test_json_schema(cls, expected):
Expand Down

0 comments on commit 55a01b2

Please sign in to comment.