import re
from django.core.exceptions import ValidationError
[docs]
class AddressValidator:
"""Utility class for validating and normalizing address components"""
# US state abbreviations
[docs]
US_STATES = {
"AL",
"AK",
"AZ",
"AR",
"CA",
"CO",
"CT",
"DE",
"FL",
"GA",
"HI",
"ID",
"IL",
"IN",
"IA",
"KS",
"KY",
"LA",
"ME",
"MD",
"MA",
"MI",
"MN",
"MS",
"MO",
"MT",
"NE",
"NV",
"NH",
"NJ",
"NM",
"NY",
"NC",
"ND",
"OH",
"OK",
"OR",
"PA",
"RI",
"SC",
"SD",
"TN",
"TX",
"UT",
"VT",
"VA",
"WA",
"WV",
"WI",
"WY",
"DC", # District of Columbia
}
# ZIP code patterns
[docs]
ZIP_CODE_PATTERN = re.compile(r"^\d{5}(-\d{4})?$")
@classmethod
[docs]
def validate_state(cls, state: str) -> str:
"""Validate and normalize US state abbreviation"""
if not state:
raise ValidationError("State is required")
normalized_state = state.strip().upper()
if normalized_state not in cls.US_STATES:
raise ValidationError(f"Invalid state code: {state}")
return normalized_state
@classmethod
[docs]
def validate_zip_code(cls, zip_code: str) -> str:
"""Validate and normalize ZIP code format"""
if not zip_code:
raise ValidationError("ZIP code is required")
normalized_zip = zip_code.strip()
if not cls.ZIP_CODE_PATTERN.match(normalized_zip):
raise ValidationError("ZIP code must be in format 12345 or 12345-6789")
return normalized_zip
@classmethod
[docs]
def validate_street_address(cls, street_address: str) -> str:
"""Validate and normalize street address"""
if not street_address:
raise ValidationError("Street address is required")
normalized_address = street_address.strip()
if len(normalized_address) < 5:
raise ValidationError("Street address must be at least 5 characters")
if len(normalized_address) > 255:
raise ValidationError("Street address must be less than 255 characters")
return normalized_address
@classmethod
[docs]
def validate_city(cls, city: str) -> str:
"""Validate and normalize city name"""
if not city:
raise ValidationError("City is required")
normalized_city = city.strip().title()
if len(normalized_city) < 2:
raise ValidationError("City name must be at least 2 characters")
if len(normalized_city) > 100:
raise ValidationError("City name must be less than 100 characters")
# Basic city name validation (letters, spaces, hyphens, apostrophes)
if not re.match(r"^[a-zA-Z\s\-'\.]+$", normalized_city):
raise ValidationError("City name contains invalid characters")
return normalized_city
@classmethod
[docs]
def validate_complete_address(
cls,
street_address: str,
city: str,
state: str,
zip_code: str,
) -> dict[str, str]:
"""Validate a complete address and return normalized components"""
return {
"street_address": cls.validate_street_address(street_address),
"city": cls.validate_city(city),
"state": cls.validate_state(state),
"zip_code": cls.validate_zip_code(zip_code),
}
@classmethod