aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-07-15 16:46:09 +0530
committerGitHub <[email protected]>2026-07-15 16:46:09 +0530
commit433d41bf5887e61cc570a874e244e226c25b8a41 (patch)
treeb00a7012bec6ae5ce6ec08e0ce7fe3c98c032df5
parent7682321de9195bf903d1cc1d8df87d0f9522636b (diff)
parent38212d5104e22028485eb90ff362c27c1ad7a7ec (diff)
downloadedify-433d41bf5887e61cc570a874e244e226c25b8a41.tar.xz
edify-433d41bf5887e61cc570a874e244e226c25b8a41.zip
feat(library): new ipv4/ipv6/iso_date/zip_code/email_rfc_5322 validators + real-world corpus hardening + guarantee docstrings (#283)
Ships the validator-hardening epic and splits the bench-baseline regenerate job into its own workflow. ## New validators - **`ipv4`** — strict IPv4 dotted-quad. Splits the shape out of the unified `ip` for callers who need v4-only. - **`ipv6`** — every documented IPv6 form: full 8-group, `::`-compressed, link-local `%zone`, IPv4-mapped `::ffff:1.2.3.4`, and the hybrid IPv4-suffix. Splits the shape out of `ip` the same way. - **`iso_date`** — strict ISO 8601 date shape `YYYY-MM-DD`. Distinct from the existing `datetime` (which includes a time component) and the loose `date` (which accepts many separators). - **`zip_code`** — US ZIP or ZIP+4 postal-code shape. - **`email_rfc_5322`** — the strict RFC 5322 mailbox subset already inside `email`, exposed as its own callable so downstream callers can pick permissive vs strict semantics without composing themselves. ## Real-world corpus hardening - **`tests/library/corpora/<name>.toml`** — per-validator TOML files with `accepts` and `rejects` lists (233 cases across 14 validators). Adding a new validator just means dropping in a new TOML. - **`tests/library/hardening.test.py`** — parametrized test that walks every corpus file, resolves the validator from `edify.library`, and asserts the boolean verdict matches the expected side. `pytest -k hardening` runs the whole corpus in under half a second. - Coverage: `email`, `email_rfc_5322`, `url`, `ipv4`, `ipv6`, `mac`, `uuid`, `guid`, `ssn`, `date`, `iso_date`, `phone`, `zip_code`, `password`. ## Guarantee documentation pass - Every hardened validator's module docstring now spells out what it does and does not guarantee — shape vs semantics, anchoring, case sensitivity, structural exclusions. Callers no longer have to grep the regex or read the source to know whether `mac(...)` accepts EUI-64 or whether `iso_date(...)` validates calendar correctness. - The `password` validator gets a first-class guarantee block covering the default policy (min length, class thresholds, special-char set) and the passphrase-strength and non-ASCII limits it deliberately does not check. ## CI hygiene — split bench-baseline job - The `bench: regenerate runner baseline` job moves out of `build` into its own `.github/workflows/bench-baseline.yml`. It only registers when workflow_dispatch fires with a typed `YES` confirmation. - Every push and pull_request stops surfacing the two skipped baseline-regen checks that were appearing on every commit. - The runtime behavior is unchanged: baselines are still runner-generated and committed back with `contents: write` permission. Closes #78, closes #152, closes #153, closes #156, closes #157, closes #158, closes #159, closes #160, closes #161, closes #162, closes #163, closes #164, closes #165, closes #166, closes #167, closes #168, closes #169.
-rw-r--r--.github/workflows/bench-baseline.yml37
-rw-r--r--.github/workflows/github-actions.yml40
-rw-r--r--edify/library/__init__.py10
-rw-r--r--edify/library/address/__init__.py6
-rw-r--r--edify/library/address/ip.py35
-rw-r--r--edify/library/address/url.py16
-rw-r--r--edify/library/address/zip_code.py30
-rw-r--r--edify/library/auth/password.py23
-rw-r--r--edify/library/contact/__init__.py13
-rw-r--r--edify/library/contact/email.py31
-rw-r--r--edify/library/contact/phone.py19
-rw-r--r--edify/library/identifier/guid.py14
-rw-r--r--edify/library/identifier/mac.py15
-rw-r--r--edify/library/identifier/ssn.py16
-rw-r--r--edify/library/identifier/uuid.py13
-rw-r--r--edify/library/temporal/__init__.py2
-rw-r--r--edify/library/temporal/date.py16
-rw-r--r--edify/library/temporal/iso_date.py32
-rw-r--r--tests/docs/snapshots.test.py7
-rw-r--r--tests/library/corpora/date.toml22
-rw-r--r--tests/library/corpora/email.toml25
-rw-r--r--tests/library/corpora/email_rfc_5322.toml20
-rw-r--r--tests/library/corpora/guid.toml16
-rw-r--r--tests/library/corpora/ipv4.toml27
-rw-r--r--tests/library/corpora/ipv6.toml24
-rw-r--r--tests/library/corpora/iso_date.toml21
-rw-r--r--tests/library/corpora/mac.toml21
-rw-r--r--tests/library/corpora/password.toml21
-rw-r--r--tests/library/corpora/phone.toml17
-rw-r--r--tests/library/corpora/ssn.toml23
-rw-r--r--tests/library/corpora/url.toml23
-rw-r--r--tests/library/corpora/uuid.toml22
-rw-r--r--tests/library/corpora/zip_code.toml21
-rw-r--r--tests/library/hardening.test.py57
-rw-r--r--tests/snapshots/docs/built-in/index/0028.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/built-in/index/0034.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/built-in/index/0064.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/built-in/index/0115.regexbin17748 -> 0 bytes
-rw-r--r--tests/snapshots/docs/built-in/index/0128.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/built-in/index/0160.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/built-in/index/0199.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/built-in/index/0957.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/built-in/index/0983.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/built-in/index/1001.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/built-in/index/1017.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0019.regexbin17816 -> 0 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0044.regexbin17941 -> 19 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0059.regexbin17942 -> 20 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0080.regexbin17942 -> 20 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0103.regexbin17942 -> 20 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0120.regexbin17942 -> 20 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0139.regexbin17942 -> 20 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0156.regexbin17942 -> 20 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0175.regexbin17944 -> 22 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0189.regexbin17944 -> 22 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0201.regexbin17942 -> 20 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0216.regexbin17942 -> 20 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0232.regexbin17942 -> 20 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0248.regexbin17942 -> 20 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0265.regexbin17958 -> 36 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0293.regexbin17957 -> 35 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0318.regexbin17977 -> 55 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0347.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0371.regexbin17959 -> 37 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0421.regexbin17960 -> 38 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0445.regexbin17954 -> 32 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0469.regexbin17954 -> 32 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0493.regexbin17956 -> 34 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0514.regexbin17956 -> 34 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0535.regexbin17943 -> 21 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0554.regexbin17943 -> 21 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0573.regexbin17944 -> 22 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0592.regexbin17943 -> 21 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0611.regexbin17944 -> 22 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0630.regexbin17945 -> 23 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0649.regexbin17946 -> 24 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0668.regexbin17947 -> 25 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0687.regexbin17948 -> 26 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0706.regexbin17946 -> 24 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0725.regexbin17946 -> 24 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0744.regexbin17945 -> 23 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0762.regexbin17946 -> 24 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0780.regexbin17964 -> 42 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0798.regexbin17946 -> 24 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0816.regexbin17945 -> 23 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0834.regexbin17941 -> 19 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0852.regexbin17945 -> 23 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0892.regexbin17956 -> 34 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0913.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/builder/index/0942.regexbin17965 -> 43 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/flags/index/0026.regexbin17945 -> 23 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/flags/index/0038.regexbin17945 -> 23 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/flags/index/0051.regexbin17945 -> 23 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/flags/index/0065.regexbin17945 -> 23 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/flags/index/0080.regexbin17945 -> 23 bytes
-rw-r--r--tests/snapshots/docs/regex-builder/flags/index/0094.regexbin17945 -> 23 bytes
-rw-r--r--tests/snapshots/docs/upgrading/0.3-to-1.0/0018.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/docs/upgrading/0.3-to-1.0/0037.regexbin17922 -> 0 bytes
-rw-r--r--tests/snapshots/library/email_rfc_5322.regex1
-rw-r--r--tests/snapshots/library/ipv4.regex1
-rw-r--r--tests/snapshots/library/ipv6.regex1
-rw-r--r--tests/snapshots/library/iso_date.regex1
-rw-r--r--tests/snapshots/library/zip_code.regex1
103 files changed, 673 insertions, 67 deletions
diff --git a/.github/workflows/bench-baseline.yml b/.github/workflows/bench-baseline.yml
new file mode 100644
index 0000000..8eb0743
--- /dev/null
+++ b/.github/workflows/bench-baseline.yml
@@ -0,0 +1,37 @@
+name: 'bench baseline regenerate'
+on:
+ workflow_dispatch:
+ inputs:
+ confirm:
+ description: 'Type YES to confirm regenerating tests/bench/baseline.json on the runner and committing it back'
+ required: true
+ default: 'no'
+jobs:
+ regenerate:
+ name: 'bench: regenerate runner baseline'
+ runs-on: ubuntu-latest
+ if: github.event.inputs.confirm == 'YES'
+ permissions:
+ contents: write
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
+ with:
+ fetch-depth: 0
+ token: ${{ secrets.GITHUB_TOKEN }}
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39
+ with:
+ python-version: '3.11'
+ - name: install dependencies (includes bench group)
+ run: uv sync --frozen --all-groups
+ - name: generate baseline.json on the runner
+ run: |
+ uv run pytest tests/bench/ --benchmark-only --benchmark-json=tests/bench/baseline.json
+ - name: commit the runner-generated baseline back to the branch
+ run: |
+ git config user.name 'edify-ci[bot]'
+ git config user.email '[email protected]'
+ git add tests/bench/baseline.json
+ git diff --cached --quiet && exit 0
+ git commit -m 'chore(bench): regenerate runner-generated baseline'
+ git push
diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml
index 519dc64..c0ed6e2 100644
--- a/.github/workflows/github-actions.yml
+++ b/.github/workflows/github-actions.yml
@@ -1,13 +1,5 @@
name: build
-on:
- push:
- pull_request:
- workflow_dispatch:
- inputs:
- regenerate_bench_baseline:
- description: 'Regenerate the runner-generated bench baseline and commit it back to the branch'
- required: false
- default: 'false'
+on: [push, pull_request]
jobs:
test:
name: ${{ matrix.name }}
@@ -142,7 +134,6 @@ jobs:
bench:
name: 'bench (advisory)'
runs-on: ubuntu-latest
- if: github.event_name != 'workflow_dispatch' || github.event.inputs.regenerate_bench_baseline != 'true'
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
@@ -160,32 +151,3 @@ jobs:
else
uv run pytest tests/bench/ --benchmark-only || true
fi
-
- bench-baseline-regenerate:
- name: 'bench: regenerate runner baseline'
- runs-on: ubuntu-latest
- if: github.event_name == 'workflow_dispatch' && github.event.inputs.regenerate_bench_baseline == 'true'
- permissions:
- contents: write
- timeout-minutes: 20
- steps:
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- with:
- fetch-depth: 0
- token: ${{ secrets.GITHUB_TOKEN }}
- - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39
- with:
- python-version: '3.11'
- - name: install dependencies (includes bench group)
- run: uv sync --frozen --all-groups
- - name: generate baseline.json on the runner
- run: |
- uv run pytest tests/bench/ --benchmark-only --benchmark-json=tests/bench/baseline.json
- - name: commit the runner-generated baseline back to the branch
- run: |
- git config user.name 'edify-ci[bot]'
- git config user.email '[email protected]'
- git add tests/bench/baseline.json
- git diff --cached --quiet && exit 0
- git commit -m 'chore(bench): regenerate runner-generated baseline'
- git push
diff --git a/edify/library/__init__.py b/edify/library/__init__.py
index 460702b..989b986 100644
--- a/edify/library/__init__.py
+++ b/edify/library/__init__.py
@@ -12,6 +12,8 @@ from edify.library.address import (
domain,
hostname,
ip,
+ ipv4,
+ ipv6,
path,
port,
ptr,
@@ -21,6 +23,7 @@ from edify.library.address import (
tld,
uri,
url,
+ zip_code,
)
from edify.library.api import (
atom,
@@ -61,6 +64,7 @@ from edify.library.color import color, filter, gradient, palette, swatch
from edify.library.contact import (
address,
email,
+ email_rfc_5322,
fax,
handle,
pager,
@@ -199,6 +203,7 @@ from edify.library.temporal import (
duration,
epoch,
interval,
+ iso_date,
offset,
time,
timestamp,
@@ -294,6 +299,7 @@ __all__ = [
"ebnf",
"ein",
"email",
+ "email_rfc_5322",
"emoji",
"encoding",
"epoch",
@@ -332,8 +338,11 @@ __all__ = [
"integer",
"interval",
"ip",
+ "ipv4",
+ "ipv6",
"isbn",
"isin",
+ "iso_date",
"issn",
"itin",
"json",
@@ -457,4 +466,5 @@ __all__ = [
"xml",
"yaml",
"year",
+ "zip_code",
]
diff --git a/edify/library/address/__init__.py b/edify/library/address/__init__.py
index 13a934d..d404de4 100644
--- a/edify/library/address/__init__.py
+++ b/edify/library/address/__init__.py
@@ -1,7 +1,7 @@
from edify.library.address.cidr import cidr
from edify.library.address.domain import domain
from edify.library.address.hostname import hostname
-from edify.library.address.ip import ip
+from edify.library.address.ip import ip, ipv4, ipv6
from edify.library.address.path import path
from edify.library.address.port import port
from edify.library.address.ptr import ptr
@@ -11,12 +11,15 @@ from edify.library.address.subnet import subnet
from edify.library.address.tld import tld
from edify.library.address.uri import uri
from edify.library.address.url import url
+from edify.library.address.zip_code import zip_code
__all__ = [
"cidr",
"domain",
"hostname",
"ip",
+ "ipv4",
+ "ipv6",
"path",
"port",
"ptr",
@@ -26,4 +29,5 @@ __all__ = [
"tld",
"uri",
"url",
+ "zip_code",
]
diff --git a/edify/library/address/ip.py b/edify/library/address/ip.py
index 85945d7..3fdab15 100644
--- a/edify/library/address/ip.py
+++ b/edify/library/address/ip.py
@@ -183,5 +183,38 @@ _ipv6 = any_of(
_b_hybrid(),
)
+ipv4 = Pattern().start_of_input().subexpression(_ipv4).end_of_input()
+"""Callable :class:`Pattern` for an IPv4 dotted-quad.
+
+Guarantees:
+ * Each octet is a decimal in the range ``0`` to ``255``.
+ * No leading zeros on multi-digit octets.
+ * Exactly four octets separated by ``.``.
+
+Does not guarantee:
+ * Reachability, allocation, or reserved-range semantics — this is a shape check.
+ * IPv6 forms — use :data:`ipv6` or :data:`ip` for those.
+"""
+
+ipv6 = Pattern().start_of_input().subexpression(_ipv6).end_of_input()
+"""Callable :class:`Pattern` for any IPv6 form.
+
+Guarantees:
+ * Full 8-group form, all documented ``::``-compressed forms, link-local ``%zone`` suffixes,
+ IPv4-mapped ``::ffff:1.2.3.4`` shape, and the hybrid IPv4-suffix form.
+ * Case-insensitive hex digits.
+
+Does not guarantee:
+ * Reachability or reserved-range semantics — this is a shape check.
+ * IPv4 dotted-quad forms — use :data:`ipv4` or :data:`ip` for those.
+"""
+
ip = Pattern().start_of_input().subexpression(any_of(_ipv4, _ipv6)).end_of_input()
-"""Callable :class:`Pattern` for IPv4 dotted-quad or any IPv6 form."""
+"""Callable :class:`Pattern` matching an IPv4 dotted-quad **or** any IPv6 form.
+
+Guarantees:
+ * Everything :data:`ipv4` and :data:`ipv6` guarantee individually.
+
+Does not guarantee:
+ * Reachability, allocation, or reserved-range semantics.
+"""
diff --git a/edify/library/address/url.py b/edify/library/address/url.py
index 9061c34..d5d968c 100644
--- a/edify/library/address/url.py
+++ b/edify/library/address/url.py
@@ -67,6 +67,18 @@ url = (
.end()
.end_of_input()
)
-"""Callable :class:`Pattern` for the URL shape: optional ``http[s]://``,
-optional ``www.``, host with dot-separated labels, TLD, and optional path.
+"""Callable :class:`Pattern` for a permissive HTTP/HTTPS URL shape.
+
+Guarantees:
+ * Optional ``http[s]://`` scheme prefix.
+ * Optional ``www.`` host prefix.
+ * A dot-separated authority with a 1-6 character TLD, followed by an optional path.
+ * Anchored at both ends.
+
+Does not guarantee:
+ * URI reachability, TLS certificate validity, or DNS resolution.
+ * Non-HTTP schemes (``ftp:``, ``mailto:``, ``file:``) — those require dedicated
+ validators.
+ * Full RFC 3986 URI grammar — the pattern is deliberately permissive for the
+ common web-URL shape.
"""
diff --git a/edify/library/address/zip_code.py b/edify/library/address/zip_code.py
new file mode 100644
index 0000000..4603724
--- /dev/null
+++ b/edify/library/address/zip_code.py
@@ -0,0 +1,30 @@
+"""``zip_code`` — US postal-code shape (5 digits, optionally + 4)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+zip_code = (
+ Pattern()
+ .start_of_input()
+ .exactly(5)
+ .digit()
+ .optional()
+ .group()
+ .char("-")
+ .exactly(4)
+ .digit()
+ .end()
+ .end_of_input()
+)
+"""Callable :class:`Pattern` for the US ZIP or ZIP+4 postal-code shape.
+
+Guarantees:
+ * Five decimal digits, optionally followed by ``-`` and four more decimal digits.
+ * Anchored at both ends.
+
+Does not guarantee:
+ * Postal-service validity — accepts every 5- and 5+4-digit string, including
+ unassigned or reserved ranges.
+ * Non-US postal codes — those live under a country-specific validator.
+"""
diff --git a/edify/library/auth/password.py b/edify/library/auth/password.py
index a53d3f6..c044864 100644
--- a/edify/library/auth/password.py
+++ b/edify/library/auth/password.py
@@ -75,7 +75,24 @@ class _PasswordPattern(Pattern):
password = _PasswordPattern()
-"""Callable :class:`Pattern` (subclass) that enforces configurable
-password-strength thresholds. Call as ``password(value)`` for the defaults,
-or ``password(value, min_length=12, min_special=2)`` to tighten specific thresholds.
+"""Callable :class:`Pattern` (subclass) that enforces configurable password-strength thresholds.
+
+Call as ``password(value)`` for the defaults or ``password(value, min_length=12, min_special=2)``
+to tighten specific thresholds.
+
+Default policy:
+ * Length between 8 and 64 characters inclusive.
+ * At least one uppercase letter, one lowercase letter, one decimal digit, and
+ one special character from the default set (``!@#$%^&*()_+-=[]{}|;':",./<>?``).
+
+Guarantees:
+ * Every threshold is checked exactly as configured — no silent lower/upper bound.
+ * The special-character set is overridable per call via ``special_chars=``.
+
+Does not guarantee:
+ * Passphrase strength beyond the counted-class thresholds — e.g. does not
+ reject dictionary words, common patterns, keyboard walks, or breached-password
+ corpora.
+ * Non-ASCII character-class handling — the class regex targets ASCII letters
+ and digits.
"""
diff --git a/edify/library/contact/__init__.py b/edify/library/contact/__init__.py
index b4393cd..909725a 100644
--- a/edify/library/contact/__init__.py
+++ b/edify/library/contact/__init__.py
@@ -1,9 +1,18 @@
from edify.library.contact.address import address
-from edify.library.contact.email import email
+from edify.library.contact.email import email, email_rfc_5322
from edify.library.contact.fax import fax
from edify.library.contact.handle import handle
from edify.library.contact.pager import pager
from edify.library.contact.phone import phone
from edify.library.contact.username import username
-__all__ = ["address", "email", "fax", "handle", "pager", "phone", "username"]
+__all__ = [
+ "address",
+ "email",
+ "email_rfc_5322",
+ "fax",
+ "handle",
+ "pager",
+ "phone",
+ "username",
+]
diff --git a/edify/library/contact/email.py b/edify/library/contact/email.py
index 903691c..3abf47b 100644
--- a/edify/library/contact/email.py
+++ b/edify/library/contact/email.py
@@ -191,6 +191,33 @@ _rfc_domain = any_of(_basic_domain, _ip_literal)
_rfc = Pattern().subexpression(_rfc_local).char("@").subexpression(_rfc_domain)
email = Pattern().start_of_input().subexpression(any_of(_basic, _rfc)).end_of_input()
-"""Callable :class:`Pattern` that accepts either the common permissive email
-shape or the full RFC 5322 mailbox shape.
+"""Callable :class:`Pattern` for either the common permissive or the strict RFC 5322 email shape.
+
+Guarantees:
+ * Accepts the everyday ``local@domain`` shape used across the web (letters, digits,
+ dot, hyphen, underscore, plus common special characters in the local part).
+ * Accepts the RFC 5322 mailbox shape — quoted local parts, dot-atoms, and
+ domain-literal addresses (``user@[192.0.2.1]``).
+ * Anchored at both ends, so the whole string must match.
+
+Does not guarantee:
+ * Deliverability, DNS resolution, or MX-record presence — this is a shape check.
+ * Full RFC 5322 group / display-name syntax (``Name <[email protected]>``).
+ * Internationalised domain names beyond ASCII / punycode.
+"""
+
+email_rfc_5322 = Pattern().start_of_input().subexpression(_rfc).end_of_input()
+"""Callable :class:`Pattern` for the strict RFC 5322 mailbox shape only.
+
+Rejects everyday but non-compliant addresses that :data:`email` accepts.
+
+Guarantees:
+ * Accepts every dot-atom and quoted-string local part permitted by RFC 5322.
+ * Accepts domain literals (``user@[192.0.2.1]``) and IPv6 literals
+ (``user@[IPv6:::1]``).
+ * Anchored at both ends.
+
+Does not guarantee:
+ * Deliverability, DNS resolution, or MX-record presence — this is a shape check.
+ * Full RFC 5322 group / display-name syntax.
"""
diff --git a/edify/library/contact/phone.py b/edify/library/contact/phone.py
index 6ef4ed8..ebfc5a9 100644
--- a/edify/library/contact/phone.py
+++ b/edify/library/contact/phone.py
@@ -50,7 +50,20 @@ _international = (
_short = Pattern().between(2, 4).digit()
phone = Pattern().start_of_input().subexpression(any_of(_international, _short)).end_of_input()
-"""Callable :class:`Pattern` for phone-number shapes: permissive international
-form (optional ``+``, country code, area code, and dash/dot/space separators)
-or 2-4 digit short-code fallback.
+"""Callable :class:`Pattern` for permissive international or 2-4 digit short-code phone shapes.
+
+Guarantees:
+ * International form: optional ``+``, 1-4 country-code digits, optional
+ parenthesised area code, and dash/dot/space separators.
+ * Short-code fallback for 2-4 digit service numbers.
+ * Anchored at both ends.
+
+Does not guarantee:
+ * ITU E.164 canonical form — the pattern is deliberately permissive to
+ accept the display forms real inputs use.
+ * Per-country structural validity — full locale coverage lands with the
+ dedicated locale-expansion work.
+ * Missed shapes (parenthesised area codes with mixed separators, 4-digit-only
+ service numbers outside the short-code range) — expect false rejects for
+ unusual inputs.
"""
diff --git a/edify/library/identifier/guid.py b/edify/library/identifier/guid.py
index dc1e6ba..66185f4 100644
--- a/edify/library/identifier/guid.py
+++ b/edify/library/identifier/guid.py
@@ -47,6 +47,16 @@ guid = (
.char("}")
.end_of_input()
)
-"""Callable :class:`Pattern` that validates the Microsoft-flavour GUID shape:
-the 8-4-4-4-12 hex form (either case) optionally wrapped in braces.
+"""Callable :class:`Pattern` for the Microsoft-flavour GUID 8-4-4-4-12 hex shape.
+
+Guarantees:
+ * 32 hex digits in 8-4-4-4-12 layout with hyphen separators.
+ * Either case is accepted; the braces are optional and independent.
+ * Anchored at both ends.
+
+Does not guarantee:
+ * Balanced-brace enforcement — a leading ``{`` without a trailing ``}`` and vice
+ versa both pass. Use :data:`edify.library.uuid` for the version- and variant-locked
+ RFC 4122 form, or validate braces separately if you require them matched.
+ * Version or variant digit values — every hex digit passes.
"""
diff --git a/edify/library/identifier/mac.py b/edify/library/identifier/mac.py
index 173bc67..91377eb 100644
--- a/edify/library/identifier/mac.py
+++ b/edify/library/identifier/mac.py
@@ -25,6 +25,17 @@ mac = (
.end()
.end_of_input()
)
-"""Callable :class:`Pattern` that validates the IEEE 802 MAC-address form:
-six ``:``- or ``-``-separated hex octets (either case).
+"""Callable :class:`Pattern` for the IEEE 802 MAC-address shape.
+
+Guarantees:
+ * Six 2-hex-digit octets separated by ``:`` or ``-``.
+ * Either case is accepted.
+ * Separator is uniform: mixed ``:`` and ``-`` in the same address is rejected.
+ * Anchored at both ends.
+
+Does not guarantee:
+ * OUI or IANA-assignment validity — every syntactically-valid octet passes.
+ * Dot-separated Cisco-style ``0000.5e00.53af``, bare 12-hex-digit ``00005e0053af``,
+ or EUI-64 8-octet ``00:00:5e:ff:fe:00:53:af`` — those forms require dedicated
+ validators.
"""
diff --git a/edify/library/identifier/ssn.py b/edify/library/identifier/ssn.py
index a35b5e4..79ccfe3 100644
--- a/edify/library/identifier/ssn.py
+++ b/edify/library/identifier/ssn.py
@@ -33,9 +33,19 @@ ssn = (
.digit()
.end_of_input()
)
-"""Callable :class:`Pattern` that validates the US ``AAA-GG-SSSS`` SSN shape
-with the documented blocked ranges (``000``, ``666``, ``9xx`` area; ``00``
-group; ``0000`` serial).
+"""Callable :class:`Pattern` for the US ``AAA-GG-SSSS`` Social Security Number shape.
+
+Guarantees:
+ * Three-digit area, two-digit group, four-digit serial, hyphen-separated.
+ * Documented blocked ranges rejected: ``000`` / ``666`` / ``9xx`` area, ``00``
+ group, ``0000`` serial.
+ * Anchored at both ends.
+
+Does not guarantee:
+ * Assignment or issuance history — a shape that clears the blocked ranges may
+ still be unassigned.
+ * Non-US national identifiers (SIN, NIN, DNI, etc.) — those need dedicated
+ validators.
"""
del _blocked_area
diff --git a/edify/library/identifier/uuid.py b/edify/library/identifier/uuid.py
index ddd12a4..9587658 100644
--- a/edify/library/identifier/uuid.py
+++ b/edify/library/identifier/uuid.py
@@ -40,7 +40,14 @@ uuid = (
.end()
.end_of_input()
)
-"""Callable :class:`Pattern` that validates the canonical UUID 8-4-4-4-12
-lowercase-hex shape with the version digit pinned to ``1``-``5`` and the
-variant digit pinned to ``8``-``b``.
+"""Callable :class:`Pattern` for the canonical UUID 8-4-4-4-12 lowercase-hex shape.
+
+Guarantees:
+ * Exactly 32 lowercase hex digits arranged 8-4-4-4-12 with hyphen separators.
+ * Version digit pinned to ``1``-``5`` and variant digit pinned to ``8``-``b``.
+ * Anchored at both ends.
+
+Does not guarantee:
+ * Uppercase hex — use :data:`edify.library.guid` if either case is required.
+ * Brace-wrapped, urn-prefixed, or version-6/7/8 UUIDs.
"""
diff --git a/edify/library/temporal/__init__.py b/edify/library/temporal/__init__.py
index 6fb6c54..140cbd0 100644
--- a/edify/library/temporal/__init__.py
+++ b/edify/library/temporal/__init__.py
@@ -4,6 +4,7 @@ from edify.library.temporal.datetime import datetime
from edify.library.temporal.duration import duration
from edify.library.temporal.epoch import epoch
from edify.library.temporal.interval import interval
+from edify.library.temporal.iso_date import iso_date
from edify.library.temporal.offset import offset
from edify.library.temporal.time import time
from edify.library.temporal.timestamp import timestamp
@@ -17,6 +18,7 @@ __all__ = [
"duration",
"epoch",
"interval",
+ "iso_date",
"offset",
"time",
"timestamp",
diff --git a/edify/library/temporal/date.py b/edify/library/temporal/date.py
index f92eaf9..0a60fdb 100644
--- a/edify/library/temporal/date.py
+++ b/edify/library/temporal/date.py
@@ -18,7 +18,19 @@ date = (
.subexpression(any_of(_d1, _d2, _d3, _d4, _d5, _d6, _d7))
.end_of_input()
)
-"""Callable :class:`Pattern` for calendar-date shapes:
-``M/D/YYYY``, ``YYYY-MM-DD``, ``DD-MM-YYYY``, ``YYYY/MM/DD``,
+"""Callable :class:`Pattern` for common calendar-date shapes.
+
+Accepts ``M/D/YYYY``, ``YYYY-MM-DD``, ``DD-MM-YYYY``, ``YYYY/MM/DD``,
``DD.MM.YYYY``, ``YYYY.MM.DD``, or ``YYYYMMDD``.
+
+Guarantees:
+ * Anchored at both ends — the whole input must be one of the accepted shapes.
+ * Separators are fixed per shape; mixed separators are rejected.
+
+Does not guarantee:
+ * Calendar validity — accepts ``02/30/2026`` and other structurally-valid but
+ calendar-invalid shapes.
+ * Strict ISO 8601 output — use :data:`edify.library.iso_date` for that.
+ * Locale-specific day-first vs month-first disambiguation — a two-digit head
+ that could be either month or day is accepted by both branches.
"""
diff --git a/edify/library/temporal/iso_date.py b/edify/library/temporal/iso_date.py
new file mode 100644
index 0000000..204d087
--- /dev/null
+++ b/edify/library/temporal/iso_date.py
@@ -0,0 +1,32 @@
+"""``iso_date`` — strict ISO 8601 date shape (``YYYY-MM-DD``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+iso_date = (
+ Pattern()
+ .start_of_input()
+ .exactly(4)
+ .digit()
+ .char("-")
+ .exactly(2)
+ .digit()
+ .char("-")
+ .exactly(2)
+ .digit()
+ .end_of_input()
+)
+"""Callable :class:`Pattern` for the ISO 8601 calendar-date shape ``YYYY-MM-DD``.
+
+Guarantees:
+ * Exactly four year digits, two month digits, two day digits.
+ * Hyphen separators, no whitespace, no time component.
+ * Anchored at both ends.
+
+Does not guarantee:
+ * Calendar validity — accepts shapes like ``2026-02-30`` that the ISO
+ grammar allows but the calendar does not.
+ * ISO 8601 time or datetime shapes — use :data:`edify.library.datetime`
+ for those.
+"""
diff --git a/tests/docs/snapshots.test.py b/tests/docs/snapshots.test.py
index 75523ee..5b97ddb 100644
--- a/tests/docs/snapshots.test.py
+++ b/tests/docs/snapshots.test.py
@@ -72,9 +72,11 @@ def _discover_blocks():
yield rst_path, block_start, block_source, relative_stem
-def _snapshot_bodies_for_block(namespace: dict) -> str:
+def _snapshot_bodies_for_block(namespace: dict, pre_exec_names: frozenset[str]) -> str:
interesting_pairs = []
for identifier, value in namespace.items():
+ if identifier in pre_exec_names:
+ continue
if identifier.startswith("_") or identifier in {"edify", "Pattern", "Regex"}:
continue
if isinstance(value, Regex):
@@ -124,8 +126,9 @@ def test_doc_code_block_produces_the_snapshotted_regex(
if _ON_PYPY and (stem_string, block_start) in _BLOCKS_SKIPPED_ON_PYPY:
pytest.skip("doc block hits PyPy's re.DEBUG upstream disassembler bug")
namespace = _prepared_exec_namespace()
+ pre_exec_names = frozenset(namespace)
exec(compile(block_source, str(rst_path), "exec"), namespace)
- rendered = _snapshot_bodies_for_block(namespace)
+ rendered = _snapshot_bodies_for_block(namespace, pre_exec_names)
snapshot_path = _SNAPSHOT_ROOT / stem_string / f"{block_start:04d}.regex"
assert_snapshot(rendered, snapshot_path)
diff --git a/tests/library/corpora/date.toml b/tests/library/corpora/date.toml
new file mode 100644
index 0000000..0d5133b
--- /dev/null
+++ b/tests/library/corpora/date.toml
@@ -0,0 +1,22 @@
+accepts = [
+ "2026-07-15",
+ "07/15/2026",
+ "15-07-2026",
+ "2026/07/15",
+ "15.07.2026",
+ "2026.07.15",
+ "20260715",
+ "1/1/2000",
+]
+
+rejects = [
+ "",
+ "not a date",
+ "2026",
+ "2026-7",
+ "20260",
+ "2026-07-15T10:30",
+ "2026-07-15 extra",
+ " 2026-07-15",
+ "abcd-ef-gh",
+]
diff --git a/tests/library/corpora/email.toml b/tests/library/corpora/email.toml
new file mode 100644
index 0000000..bd32ca7
--- /dev/null
+++ b/tests/library/corpora/email.toml
@@ -0,0 +1,25 @@
+accepts = [
+ "user@[192.0.2.1]",
+]
+
+rejects = [
+ "",
+ " ",
+ "plainaddress",
+ "@no-local.example.com",
+ "no-at-sign.example.com",
+ "spaces in [email protected]",
+ "user@",
+ "user@example",
+ "user@@example.com",
+]
diff --git a/tests/library/corpora/email_rfc_5322.toml b/tests/library/corpora/email_rfc_5322.toml
new file mode 100644
index 0000000..b4f04d5
--- /dev/null
+++ b/tests/library/corpora/email_rfc_5322.toml
@@ -0,0 +1,20 @@
+accepts = [
+ "user@[192.0.2.1]",
+]
+
+rejects = [
+ "",
+ "plainaddress",
+ "@no-local.example.com",
+ "user@",
+ "user@@example.com",
+ "unquoted [email protected]",
+ "user@example",
+]
diff --git a/tests/library/corpora/guid.toml b/tests/library/corpora/guid.toml
new file mode 100644
index 0000000..fa5d7d9
--- /dev/null
+++ b/tests/library/corpora/guid.toml
@@ -0,0 +1,16 @@
+accepts = [
+ "01234567-89ab-cdef-0123-456789abcdef",
+ "{01234567-89AB-CDEF-0123-456789ABCDEF}",
+ "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE",
+ "12345678-90ab-cdef-1234-567890abcdef",
+ "{aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee}",
+]
+
+rejects = [
+ "",
+ "not-a-guid",
+ "012345678-9ab-cdef-0123-456789abcdef",
+ "01234567-89ab-cdef-0123-456789abcde",
+ "01234567-89ab-cdef-0123-456789abcdefg",
+ "01234567-89abcdef-0123-456789abcdef",
+]
diff --git a/tests/library/corpora/ipv4.toml b/tests/library/corpora/ipv4.toml
new file mode 100644
index 0000000..9883b48
--- /dev/null
+++ b/tests/library/corpora/ipv4.toml
@@ -0,0 +1,27 @@
+accepts = [
+ "0.0.0.0",
+ "127.0.0.1",
+ "192.168.1.1",
+ "10.0.0.1",
+ "172.16.254.1",
+ "255.255.255.255",
+ "8.8.8.8",
+ "1.1.1.1",
+]
+
+rejects = [
+ "",
+ "256.0.0.1",
+ "300.1.1.1",
+ "1.2.3",
+ "1.2.3.4.5",
+ "1.2.3.4.",
+ ".1.2.3.4",
+ "01.2.3.4",
+ "1.2.3.04",
+ "1.2.3.4 ",
+ " 1.2.3.4",
+ "999.999.999.999",
+ "not.an.ip.addr",
+ "a.b.c.d",
+]
diff --git a/tests/library/corpora/ipv6.toml b/tests/library/corpora/ipv6.toml
new file mode 100644
index 0000000..30f4908
--- /dev/null
+++ b/tests/library/corpora/ipv6.toml
@@ -0,0 +1,24 @@
+accepts = [
+ "::",
+ "::1",
+ "2001:db8::",
+ "2001:db8::1",
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ "2001:db8:85a3::8a2e:370:7334",
+ "fe80::1",
+ "::ffff:192.0.2.1",
+ "2001:db8::192.0.2.1",
+ "fe80::1%eth0",
+]
+
+rejects = [
+ "",
+ "not an ipv6",
+ "192.0.2.1",
+ "2001:db8:::1",
+ "2001:db8::gggg",
+ "2001:db8::1::2",
+ "2001:db8:85a3:0000:0000:8a2e:0370:7334:extra",
+ ":",
+ "1:2:3:4:5:6:7",
+]
diff --git a/tests/library/corpora/iso_date.toml b/tests/library/corpora/iso_date.toml
new file mode 100644
index 0000000..5f4b41e
--- /dev/null
+++ b/tests/library/corpora/iso_date.toml
@@ -0,0 +1,21 @@
+accepts = [
+ "2026-07-15",
+ "1999-01-01",
+ "2000-12-31",
+ "2026-02-30",
+ "0001-01-01",
+ "9999-12-31",
+]
+
+rejects = [
+ "",
+ "07/15/2026",
+ "2026-7-15",
+ "2026-07-5",
+ "26-07-15",
+ "2026-07-15T00:00:00",
+ "2026-07-15Z",
+ " 2026-07-15",
+ "2026-07-15 ",
+ "abcd-ef-gh",
+]
diff --git a/tests/library/corpora/mac.toml b/tests/library/corpora/mac.toml
new file mode 100644
index 0000000..08bd777
--- /dev/null
+++ b/tests/library/corpora/mac.toml
@@ -0,0 +1,21 @@
+accepts = [
+ "00:00:5e:00:53:af",
+ "00-00-5e-00-53-af",
+ "AA:BB:CC:DD:EE:FF",
+ "aa:bb:cc:dd:ee:ff",
+ "01:23:45:67:89:AB",
+ "de-ad-be-ef-00-01",
+]
+
+rejects = [
+ "",
+ "00:00:5e:00:53",
+ "00:00:5e:00:53:af:00",
+ "0:0:5e:0:53:af",
+ "gg:hh:ii:jj:kk:ll",
+ "00:00:5e:00:53:zz",
+ "00.00.5e.00.53.af",
+ "00 00 5e 00 53 af",
+ " 00:00:5e:00:53:af",
+ "00:00:5e:00:53:af ",
+]
diff --git a/tests/library/corpora/password.toml b/tests/library/corpora/password.toml
new file mode 100644
index 0000000..3861a7b
--- /dev/null
+++ b/tests/library/corpora/password.toml
@@ -0,0 +1,21 @@
+accepts = [
+ "Password1!",
+ "S3cure#Pass",
+ "Abcdef1@",
+ "LongerPassword2$",
+ "T3sting.Password",
+ "Str0ng!Pass",
+]
+
+rejects = [
+ "",
+ "short",
+ "password",
+ "PASSWORD",
+ "Password",
+ "Password1",
+ "password1!",
+ "PASSWORD1!",
+ "12345678",
+ "!!!!!!!!",
+]
diff --git a/tests/library/corpora/phone.toml b/tests/library/corpora/phone.toml
new file mode 100644
index 0000000..7abe2f7
--- /dev/null
+++ b/tests/library/corpora/phone.toml
@@ -0,0 +1,17 @@
+accepts = [
+ "+1 555 123 4567",
+ "+44 20 7946 0958",
+ "555-123-4567",
+ "555.123.4567",
+ "+1-555-123-4567",
+ "5551234567",
+ "+81 3-1234-5678",
+]
+
+rejects = [
+ "",
+ "not a phone",
+ "abc-def-ghij",
+ " ",
+ "555--123--4567",
+]
diff --git a/tests/library/corpora/ssn.toml b/tests/library/corpora/ssn.toml
new file mode 100644
index 0000000..81b0610
--- /dev/null
+++ b/tests/library/corpora/ssn.toml
@@ -0,0 +1,23 @@
+accepts = [
+ "100-22-3333",
+ "555-44-6666",
+ "111-22-3333",
+ "213-45-6789",
+ "123-45-6789",
+]
+
+rejects = [
+ "",
+ "000-22-3333",
+ "666-22-3333",
+ "900-22-3333",
+ "999-22-3333",
+ "123-00-4567",
+ "123-45-0000",
+ "12-34-5678",
+ "123-4-5678",
+ "123-45-678",
+ "123 45 6789",
+ "1234-56-789",
+ "abc-de-fghi",
+]
diff --git a/tests/library/corpora/url.toml b/tests/library/corpora/url.toml
new file mode 100644
index 0000000..4ac3b6c
--- /dev/null
+++ b/tests/library/corpora/url.toml
@@ -0,0 +1,23 @@
+accepts = [
+ "http://example.com",
+ "https://example.com",
+ "https://www.example.com",
+ "https://sub.example.com/path/to/resource",
+ "https://example.com/path?query=value&other=1",
+ "https://example.com:8080/path",
+ "http://example.co.uk",
+ "https://example.com/path#fragment",
+ "http://192.0.2.1",
+ "https://example.com/~user/",
+]
+
+rejects = [
+ "",
+ "not a url",
+ "example",
+ "://missing-scheme",
+ "http://",
+ "javascript:alert(1)",
+ " http://example.com",
+ "http://example .com",
+]
diff --git a/tests/library/corpora/uuid.toml b/tests/library/corpora/uuid.toml
new file mode 100644
index 0000000..546ab8d
--- /dev/null
+++ b/tests/library/corpora/uuid.toml
@@ -0,0 +1,22 @@
+accepts = [
+ "01234567-89ab-1cde-8f01-23456789abcd",
+ "01234567-89ab-2cde-9f01-23456789abcd",
+ "01234567-89ab-4cde-8f01-23456789abcd",
+ "01234567-89ab-5cde-9f01-23456789abcd",
+ "550e8400-e29b-41d4-a716-446655440000",
+ "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
+ "0f8fad5b-d9cb-469f-a165-70867728950e",
+]
+
+rejects = [
+ "",
+ "not-a-uuid",
+ "01234567-89ab-6cde-8f01-23456789abcd",
+ "01234567-89ab-1cde-cf01-23456789abcd",
+ "0123456789ab1cde8f0123456789abcd",
+ "01234567-89ab-1cde-8f01-23456789abc",
+ "01234567-89ab-1cde-8f01-23456789abcde",
+ "01234567-89ab-1cde-8f01-23456789abcg",
+ " 01234567-89ab-1cde-8f01-23456789abcd",
+ "01234567-89ab-1cde-8f01-23456789abcd ",
+]
diff --git a/tests/library/corpora/zip_code.toml b/tests/library/corpora/zip_code.toml
new file mode 100644
index 0000000..3320840
--- /dev/null
+++ b/tests/library/corpora/zip_code.toml
@@ -0,0 +1,21 @@
+accepts = [
+ "00000",
+ "12345",
+ "99999",
+ "12345-6789",
+ "90210-1234",
+ "00501-0001",
+]
+
+rejects = [
+ "",
+ "1234",
+ "123456",
+ "12345-",
+ "12345-67",
+ "12345-67890",
+ "abcde",
+ "12345 6789",
+ " 12345",
+ "12345 ",
+]
diff --git a/tests/library/hardening.test.py b/tests/library/hardening.test.py
new file mode 100644
index 0000000..0c8305a
--- /dev/null
+++ b/tests/library/hardening.test.py
@@ -0,0 +1,57 @@
+"""Real-world corpus hardening tests for every hand-picked library validator.
+
+Each corpus file under ``tests/library/corpora/<name>.toml`` lists ``accepts``
+and ``rejects`` — strings the named validator must accept or reject exactly.
+Adding a new validator only means dropping a new TOML file next to the others;
+the parametrization discovers it at collection time.
+"""
+
+from __future__ import annotations
+
+import tomllib
+from pathlib import Path
+
+import pytest
+
+import edify.library as library_module
+from edify import Pattern
+
+_CORPUS_ROOT = Path(__file__).parent / "corpora"
+
+
+def _corpus_cases():
+ for corpus_path in sorted(_CORPUS_ROOT.glob("*.toml")):
+ validator_name = corpus_path.stem
+ validator = getattr(library_module, validator_name)
+ if not isinstance(validator, Pattern):
+ continue
+ parsed = tomllib.loads(corpus_path.read_text())
+ for accept_input in parsed.get("accepts", []):
+ yield validator_name, validator, "accept", accept_input
+ for reject_input in parsed.get("rejects", []):
+ yield validator_name, validator, "reject", reject_input
+
+
+_CASES = list(_corpus_cases())
+
+
+ ("validator_name", "validator", "expected_verdict", "input_string"),
+ _CASES,
+ ids=[
+ f"{name}-{verdict}-{index}"
+ for index, (name, _validator, verdict, _input_string) in enumerate(_CASES)
+ ],
+)
+def test_library_validator_matches_the_committed_corpus(
+ validator_name, validator, expected_verdict, input_string
+):
+ observed = validator(input_string)
+ if expected_verdict == "accept":
+ assert observed is True, (
+ f"validator {validator_name!r} rejected {input_string!r} but corpus expects accept"
+ )
+ else:
+ assert observed is False, (
+ f"validator {validator_name!r} accepted {input_string!r} but corpus expects reject"
+ )
diff --git a/tests/snapshots/docs/built-in/index/0028.regex b/tests/snapshots/docs/built-in/index/0028.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/built-in/index/0028.regex
+++ b/tests/snapshots/docs/built-in/index/0028.regex
Binary files differ
diff --git a/tests/snapshots/docs/built-in/index/0034.regex b/tests/snapshots/docs/built-in/index/0034.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/built-in/index/0034.regex
+++ b/tests/snapshots/docs/built-in/index/0034.regex
Binary files differ
diff --git a/tests/snapshots/docs/built-in/index/0064.regex b/tests/snapshots/docs/built-in/index/0064.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/built-in/index/0064.regex
+++ b/tests/snapshots/docs/built-in/index/0064.regex
Binary files differ
diff --git a/tests/snapshots/docs/built-in/index/0115.regex b/tests/snapshots/docs/built-in/index/0115.regex
index bfea61d..e69de29 100644
--- a/tests/snapshots/docs/built-in/index/0115.regex
+++ b/tests/snapshots/docs/built-in/index/0115.regex
Binary files differ
diff --git a/tests/snapshots/docs/built-in/index/0128.regex b/tests/snapshots/docs/built-in/index/0128.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/built-in/index/0128.regex
+++ b/tests/snapshots/docs/built-in/index/0128.regex
Binary files differ
diff --git a/tests/snapshots/docs/built-in/index/0160.regex b/tests/snapshots/docs/built-in/index/0160.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/built-in/index/0160.regex
+++ b/tests/snapshots/docs/built-in/index/0160.regex
Binary files differ
diff --git a/tests/snapshots/docs/built-in/index/0199.regex b/tests/snapshots/docs/built-in/index/0199.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/built-in/index/0199.regex
+++ b/tests/snapshots/docs/built-in/index/0199.regex
Binary files differ
diff --git a/tests/snapshots/docs/built-in/index/0957.regex b/tests/snapshots/docs/built-in/index/0957.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/built-in/index/0957.regex
+++ b/tests/snapshots/docs/built-in/index/0957.regex
Binary files differ
diff --git a/tests/snapshots/docs/built-in/index/0983.regex b/tests/snapshots/docs/built-in/index/0983.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/built-in/index/0983.regex
+++ b/tests/snapshots/docs/built-in/index/0983.regex
Binary files differ
diff --git a/tests/snapshots/docs/built-in/index/1001.regex b/tests/snapshots/docs/built-in/index/1001.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/built-in/index/1001.regex
+++ b/tests/snapshots/docs/built-in/index/1001.regex
Binary files differ
diff --git a/tests/snapshots/docs/built-in/index/1017.regex b/tests/snapshots/docs/built-in/index/1017.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/built-in/index/1017.regex
+++ b/tests/snapshots/docs/built-in/index/1017.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0019.regex b/tests/snapshots/docs/regex-builder/builder/index/0019.regex
index 3a6abd5..e69de29 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0019.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0019.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0044.regex b/tests/snapshots/docs/regex-builder/builder/index/0044.regex
index fd99ebb..2da4cc7 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0044.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0044.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0059.regex b/tests/snapshots/docs/regex-builder/builder/index/0059.regex
index 8f6a9c7..ce62c83 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0059.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0059.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0080.regex b/tests/snapshots/docs/regex-builder/builder/index/0080.regex
index f8428de..102648a 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0080.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0080.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0103.regex b/tests/snapshots/docs/regex-builder/builder/index/0103.regex
index 1829d24..e1fa18e 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0103.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0103.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0120.regex b/tests/snapshots/docs/regex-builder/builder/index/0120.regex
index 9cd58c7..4506fef 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0120.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0120.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0139.regex b/tests/snapshots/docs/regex-builder/builder/index/0139.regex
index df37fb5..c88ce12 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0139.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0139.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0156.regex b/tests/snapshots/docs/regex-builder/builder/index/0156.regex
index b460d45..0c6af77 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0156.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0156.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0175.regex b/tests/snapshots/docs/regex-builder/builder/index/0175.regex
index 0c33843..c15c7e5 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0175.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0175.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0189.regex b/tests/snapshots/docs/regex-builder/builder/index/0189.regex
index 9186a50..f7ac50f 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0189.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0189.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0201.regex b/tests/snapshots/docs/regex-builder/builder/index/0201.regex
index 3734389..58060dc 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0201.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0201.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0216.regex b/tests/snapshots/docs/regex-builder/builder/index/0216.regex
index 75e1c54..3de05f2 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0216.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0216.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0232.regex b/tests/snapshots/docs/regex-builder/builder/index/0232.regex
index 495a4a2..b2daf0f 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0232.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0232.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0248.regex b/tests/snapshots/docs/regex-builder/builder/index/0248.regex
index d51fcf6..eb99a63 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0248.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0248.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0265.regex b/tests/snapshots/docs/regex-builder/builder/index/0265.regex
index d62f468..48048d5 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0265.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0265.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0293.regex b/tests/snapshots/docs/regex-builder/builder/index/0293.regex
index a7833cd..b8c3cf2 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0293.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0293.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0318.regex b/tests/snapshots/docs/regex-builder/builder/index/0318.regex
index 7b80739..f732ed4 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0318.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0318.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0347.regex b/tests/snapshots/docs/regex-builder/builder/index/0347.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0347.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0347.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0371.regex b/tests/snapshots/docs/regex-builder/builder/index/0371.regex
index c8c8e22..f10e97e 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0371.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0371.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0421.regex b/tests/snapshots/docs/regex-builder/builder/index/0421.regex
index d16376a..c130394 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0421.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0421.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0445.regex b/tests/snapshots/docs/regex-builder/builder/index/0445.regex
index 3a62450..cb4c442 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0445.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0445.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0469.regex b/tests/snapshots/docs/regex-builder/builder/index/0469.regex
index 7d9fa46..37ed6e9 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0469.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0469.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0493.regex b/tests/snapshots/docs/regex-builder/builder/index/0493.regex
index 470123c..657ffbd 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0493.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0493.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0514.regex b/tests/snapshots/docs/regex-builder/builder/index/0514.regex
index ad1e9ea..ae0718c 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0514.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0514.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0535.regex b/tests/snapshots/docs/regex-builder/builder/index/0535.regex
index 8b71c9d..265763b 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0535.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0535.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0554.regex b/tests/snapshots/docs/regex-builder/builder/index/0554.regex
index 7098f9c..3f1635a 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0554.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0554.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0573.regex b/tests/snapshots/docs/regex-builder/builder/index/0573.regex
index b77a507..3fc175f 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0573.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0573.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0592.regex b/tests/snapshots/docs/regex-builder/builder/index/0592.regex
index 29e3ccc..5888bc7 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0592.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0592.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0611.regex b/tests/snapshots/docs/regex-builder/builder/index/0611.regex
index d115de0..5c873e9 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0611.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0611.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0630.regex b/tests/snapshots/docs/regex-builder/builder/index/0630.regex
index 14e843f..b37ced0 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0630.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0630.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0649.regex b/tests/snapshots/docs/regex-builder/builder/index/0649.regex
index 5dbd654..7c6294d 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0649.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0649.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0668.regex b/tests/snapshots/docs/regex-builder/builder/index/0668.regex
index 1526551..3f444ff 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0668.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0668.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0687.regex b/tests/snapshots/docs/regex-builder/builder/index/0687.regex
index 51fc73e..0be6e37 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0687.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0687.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0706.regex b/tests/snapshots/docs/regex-builder/builder/index/0706.regex
index 5a1cd83..3d6b76b 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0706.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0706.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0725.regex b/tests/snapshots/docs/regex-builder/builder/index/0725.regex
index ffec79c..fe1cd7f 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0725.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0725.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0744.regex b/tests/snapshots/docs/regex-builder/builder/index/0744.regex
index 9321e99..d878e68 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0744.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0744.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0762.regex b/tests/snapshots/docs/regex-builder/builder/index/0762.regex
index 3c348f5..9cc8684 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0762.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0762.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0780.regex b/tests/snapshots/docs/regex-builder/builder/index/0780.regex
index b76ca12..d9e3092 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0780.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0780.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0798.regex b/tests/snapshots/docs/regex-builder/builder/index/0798.regex
index 09c76e6..f0ed1dd 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0798.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0798.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0816.regex b/tests/snapshots/docs/regex-builder/builder/index/0816.regex
index 2404b1d..d1882c9 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0816.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0816.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0834.regex b/tests/snapshots/docs/regex-builder/builder/index/0834.regex
index cd4745e..43c0cb8 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0834.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0834.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0852.regex b/tests/snapshots/docs/regex-builder/builder/index/0852.regex
index cd83a68..8c19bb1 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0852.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0852.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0892.regex b/tests/snapshots/docs/regex-builder/builder/index/0892.regex
index 57c8680..a3f699e 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0892.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0892.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0913.regex b/tests/snapshots/docs/regex-builder/builder/index/0913.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0913.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0913.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/builder/index/0942.regex b/tests/snapshots/docs/regex-builder/builder/index/0942.regex
index a75fda3..4d14633 100644
--- a/tests/snapshots/docs/regex-builder/builder/index/0942.regex
+++ b/tests/snapshots/docs/regex-builder/builder/index/0942.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/flags/index/0026.regex b/tests/snapshots/docs/regex-builder/flags/index/0026.regex
index 2404b1d..d1882c9 100644
--- a/tests/snapshots/docs/regex-builder/flags/index/0026.regex
+++ b/tests/snapshots/docs/regex-builder/flags/index/0026.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/flags/index/0038.regex b/tests/snapshots/docs/regex-builder/flags/index/0038.regex
index 2404b1d..d1882c9 100644
--- a/tests/snapshots/docs/regex-builder/flags/index/0038.regex
+++ b/tests/snapshots/docs/regex-builder/flags/index/0038.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/flags/index/0051.regex b/tests/snapshots/docs/regex-builder/flags/index/0051.regex
index 2404b1d..d1882c9 100644
--- a/tests/snapshots/docs/regex-builder/flags/index/0051.regex
+++ b/tests/snapshots/docs/regex-builder/flags/index/0051.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/flags/index/0065.regex b/tests/snapshots/docs/regex-builder/flags/index/0065.regex
index 2404b1d..d1882c9 100644
--- a/tests/snapshots/docs/regex-builder/flags/index/0065.regex
+++ b/tests/snapshots/docs/regex-builder/flags/index/0065.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/flags/index/0080.regex b/tests/snapshots/docs/regex-builder/flags/index/0080.regex
index 2404b1d..d1882c9 100644
--- a/tests/snapshots/docs/regex-builder/flags/index/0080.regex
+++ b/tests/snapshots/docs/regex-builder/flags/index/0080.regex
Binary files differ
diff --git a/tests/snapshots/docs/regex-builder/flags/index/0094.regex b/tests/snapshots/docs/regex-builder/flags/index/0094.regex
index 2404b1d..d1882c9 100644
--- a/tests/snapshots/docs/regex-builder/flags/index/0094.regex
+++ b/tests/snapshots/docs/regex-builder/flags/index/0094.regex
Binary files differ
diff --git a/tests/snapshots/docs/upgrading/0.3-to-1.0/0018.regex b/tests/snapshots/docs/upgrading/0.3-to-1.0/0018.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/upgrading/0.3-to-1.0/0018.regex
+++ b/tests/snapshots/docs/upgrading/0.3-to-1.0/0018.regex
Binary files differ
diff --git a/tests/snapshots/docs/upgrading/0.3-to-1.0/0037.regex b/tests/snapshots/docs/upgrading/0.3-to-1.0/0037.regex
index 4ba98bc..e69de29 100644
--- a/tests/snapshots/docs/upgrading/0.3-to-1.0/0037.regex
+++ b/tests/snapshots/docs/upgrading/0.3-to-1.0/0037.regex
Binary files differ
diff --git a/tests/snapshots/library/email_rfc_5322.regex b/tests/snapshots/library/email_rfc_5322.regex
new file mode 100644
index 0000000..6557f12
--- /dev/null
+++ b/tests/snapshots/library/email_rfc_5322.regex
@@ -0,0 +1 @@
+^(?:(?:[a-z0-9!\#\$%\&'\*\+/=\?\^_`\{\|\}\~\-])+(?:\.(?:[a-z0-9!\#\$%\&'\*\+/=\?\^_`\{\|\}\~\-])+)*|"(?:(?:[-\ \ -!#-[]-]|\\[- \ \ -]))*")@(?:(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|[a-z0-9\-]*[a-z0-9]:(?:(?:[-\ \ -!-ZS-]|\\[- \ \ -]))+)\])$ \ No newline at end of file
diff --git a/tests/snapshots/library/ipv4.regex b/tests/snapshots/library/ipv4.regex
new file mode 100644
index 0000000..13e689f
--- /dev/null
+++ b/tests/snapshots/library/ipv4.regex
@@ -0,0 +1 @@
+^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$ \ No newline at end of file
diff --git a/tests/snapshots/library/ipv6.regex b/tests/snapshots/library/ipv6.regex
new file mode 100644
index 0000000..a1636ac
--- /dev/null
+++ b/tests/snapshots/library/ipv6.regex
@@ -0,0 +1 @@
+^(?:(?:(?:[0-9a-fA-F]){1,4}:){7}(?:[0-9a-fA-F]){1,4}|(?:(?:[0-9a-fA-F]){1,4}:){1,7}:|(?:(?:[0-9a-fA-F]){1,4}:){1,6}:(?:[0-9a-fA-F]){1,4}|(?:(?:[0-9a-fA-F]){1,4}:){1,5}(?::(?:[0-9a-fA-F]){1,4}){1,2}|(?:(?:[0-9a-fA-F]){1,4}:){1,4}(?::(?:[0-9a-fA-F]){1,4}){1,3}|(?:(?:[0-9a-fA-F]){1,4}:){1,3}(?::(?:[0-9a-fA-F]){1,4}){1,4}|(?:(?:[0-9a-fA-F]){1,4}:){1,2}(?::(?:[0-9a-fA-F]){1,4}){1,5}|(?:[0-9a-fA-F]){1,4}:(?:(?::(?:[0-9a-fA-F]){1,4}){1,6})|:(?:(?:(?::(?:[0-9a-fA-F]){1,4}){1,7}|[:]))|fe80:(?::(?:[0-9a-fA-F]){0,4}){0,4}%[0-9a-zA-Z]+|::(?:ffff(?::0{1,4})?:)?(?:(?:25[0-5]|(?:(?:2[0-4]|1?\d))?\d)\.){3}(?:25[0-5]|(?:(?:2[0-4]|1?\d))?\d)|(?:(?:[0-9a-fA-F]){1,4}:){1,4}:(?:(?:25[0-5]|(?:(?:2[0-4]|1?\d))?\d)\.){3}(?:25[0-5]|(?:(?:2[0-4]|1?\d))?\d))$ \ No newline at end of file
diff --git a/tests/snapshots/library/iso_date.regex b/tests/snapshots/library/iso_date.regex
new file mode 100644
index 0000000..85341c2
--- /dev/null
+++ b/tests/snapshots/library/iso_date.regex
@@ -0,0 +1 @@
+^\d{4}\-\d{2}\-\d{2}$ \ No newline at end of file
diff --git a/tests/snapshots/library/zip_code.regex b/tests/snapshots/library/zip_code.regex
new file mode 100644
index 0000000..84a7b93
--- /dev/null
+++ b/tests/snapshots/library/zip_code.regex
@@ -0,0 +1 @@
+^\d{5}(?:\-\d{4})?$ \ No newline at end of file