From 23dfb3f899f2949fed3368afa96851e4062dd924 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 29 Jun 2026 15:24:18 +0000 Subject: [PATCH] =?UTF-8?q?Add=20UK-postcode=20format=20validity=20to=20th?= =?UTF-8?q?e=20Postcode=20value=20object=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pure, offline regex check (no network) so a frozen value object stays pure. Co-Authored-By: Claude Opus 4.8 (1M context) --- domain/postcode.py | 11 +++++++++++ tests/domain/test_postcode.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/domain/postcode.py b/domain/postcode.py index 8e4e7c797..06135555f 100644 --- a/domain/postcode.py +++ b/domain/postcode.py @@ -1,7 +1,13 @@ from __future__ import annotations +import re from dataclasses import dataclass +# UK postcode format, matched against the canonical (no-space, upper) value. +# A pure structural check — it does not confirm the postcode actually exists +# (that would need a network lookup, which a value object must not do). +_UK_POSTCODE_RE = re.compile(r"[A-Z]{1,2}\d[A-Z\d]?\d[A-Z]{2}") + @dataclass(frozen=True) class Postcode: @@ -13,3 +19,8 @@ class Postcode: def __str__(self) -> str: return self.value + + def is_valid(self) -> bool: + """Whether the canonical value is a well-formed UK postcode (format + only — no existence check, so it is pure and offline).""" + return _UK_POSTCODE_RE.fullmatch(self.value) is not None diff --git a/tests/domain/test_postcode.py b/tests/domain/test_postcode.py index f7ce90150..ebed1e2d4 100644 --- a/tests/domain/test_postcode.py +++ b/tests/domain/test_postcode.py @@ -57,3 +57,18 @@ def test_postcode_is_frozen() -> None: # act / assert with pytest.raises(dataclasses.FrozenInstanceError): postcode.value = "OTHER" # type: ignore[misc] + + +@pytest.mark.parametrize( + "raw", + ["sw1a 1aa", "M1 1AA", "b33 8th", "cr2 6xh", "dn55 1pt", "ec1a 1bb", "w1a 0ax"], +) +def test_valid_uk_postcode_formats_are_valid(raw: str) -> None: + # act / assert — a pure format check on the canonical value, no network. + assert Postcode(raw).is_valid() is True + + +@pytest.mark.parametrize("raw", ["", " ", "NONSENSE", "12345", "SW1A 1A", "ZZ"]) +def test_malformed_or_empty_postcodes_are_invalid(raw: str) -> None: + # act / assert + assert Postcode(raw).is_valid() is False