diff options
| author | Bobby <[email protected]> | 2026-07-11 17:44:20 +0530 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-07-11 17:44:20 +0530 |
| commit | abdaeb4aad4284986f012efcce35a6e4189f2929 (patch) | |
| tree | 9eaff3968954dc8f5bf079684dba46b6026e2221 /tests/library | |
| parent | 2038ee5c4a5e473c20cf1e38af5c32ab450a2aea (diff) | |
| parent | c7a8a902eb86dfec5dd5c4ec77c532f5b920d3ac (diff) | |
| download | edify-abdaeb4aad4284986f012efcce35a6e4189f2929.tar.xz edify-abdaeb4aad4284986f012efcce35a6e4189f2929.zip | |
feat!: callable-Pattern validator API + 223-pattern library across 22 category folders (#274)
Ships the callable-`Pattern` validator API and expands the built-in
library from 14 validators to **223**, organized into 22 category
folders.
## Callable-`Pattern` contract
Every built-in validator is now a `Pattern` instance whose `__call__`
returns `bool`. The old function-style `validator(value)` call form
still works; every `Pattern` method is now also available on each
validator (`match`, `to_regex_string`, `.explain()`, `.visualize()`,
etc.).
```python
from edify import Pattern
from edify.library import uuid, email
isinstance(uuid, Pattern) # True
uuid("550e8400-e29b-11d4-a716-446655440000") # True
uuid.match("550e8400-e29b-11d4-a716-446655440000") # <re.Match ...>
email("[email protected]", strict=True) # available on parameterized validators
```
Implementation:
- `Pattern.__call__(value) -> bool` on the base class (non-string
returns `False`, no crash on user input).
- **Every validator is built via the fluent chain** —
`Pattern().start_of_input().exactly(8).any_of()....end_of_input()`. No
raw-regex escape hatch anywhere in the library.
- Composable atoms in `edify/library/_support/atoms.py` (`hex_any`,
`octet`, etc.) keep the chains readable across categories.
## The 223 patterns across 22 categories
| Category | Count | Patterns |
|---|---|---|
| `identifier/` | 26 | uuid, guid, mac, ssn, ein, tin, itin, iban, bic,
lei, isin, cusip, sedol, sku, asin, imei, meid, iccid, vin, imo, mmsi,
iata, icao, arn, orcid, did |
| `data/` | 14 | xml, json, yaml, toml, csv, tsv, ini, avro, protobuf,
msgpack, html, hdf5, parquet, orc |
| `address/` | 13 | ip, cidr, subnet, hostname, domain, subdomain, tld,
url, uri, path, port, socket, ptr |
| `software/` | 13 | semver, version, package, docker, image, digest,
git, ref, checksum, component, makefile, cargo, bump |
| `api/` | 12 | oauth, openid, saml, openapi, swagger, jsonapi, hal,
graphql, soap, webhook, rss, atom |
| `auth/` | 19 | password, pin, token, jwt, otp, apikey, bearer,
session, csrf, mnemonic, hmac, passkey, mfa, webauthn, secret, sso,
refresh, signing, challenge |
| `temporal/` | 11 | date, time, datetime, duration, timezone, offset,
epoch, timestamp, year, cron, interval |
| `text/` | 11 | slug, base, script, ascii, printable, alphanumeric,
alpha, numeric, word, unicode, emoji |
| `media/` | 11 | mimetype, extension, filename, glob, regex, shebang,
encoding, charset, locale, favicon, codec |
| `document/` | 11 | svg, pdf, epub, mobi, docx, xlsx, pptx, odt,
readme, rtf, tex |
| `security/` | 11 | pgp, pem, der, csr, x509, certificate, ssh, age,
keyring, nonce, signature |
| `web/` | 11 | sitemap, captcha, robots, manifest, htaccess, humans,
nginx, apache, cookie, csp, useragent |
| `numeric/` | 10 | number, integer, hash, percentage, ratio, fraction,
scientific, roman, ordinal, natural |
| `geo/` | 8 | coordinate, geohash, plus, postal, place, altitude, mgrs,
bearing |
| `contact/` | 7 | email, phone, fax, pager, address, username, handle |
| `financial/` | 7 | currency, card, wallet, crypto, routing, sortcode,
vat |
| `publishing/` | 6 | isbn, issn, doi, arxiv, pmid, pmc |
| `grammar/` | 6 | bnf, ebnf, abnf, peg, pest, antlr |
| `color/` | 5 | color, palette, swatch, gradient, filter |
| `transport/` | 4 | vehicle, aircraft, flight, plate |
| `medical/` | 4 | medical, blood, dosage, dicom |
| `product/` | 3 | gtin, mpn, barcode |
| **Total** | **223** | |
Each pattern is importable both from its category submodule and from the
flat top-level namespace:
```python
from edify.library import uuid # flat
from edify.library.identifier import uuid # category
```
## Merged and renamed 0.3 validators
Per the \"each pattern is unique / everything doable via a form must be
merged\" rule:
- `iso_date` → `date` (accepts every recognised date form)
- `email_rfc_5322` → `email` (accepts basic or RFC 5322)
- `ipv4` + `ipv6` → `ip`
- `phone_number` → `phone`
- `zip` → `postal`
Documented in \`docs/upgrading/0.3-to-1.0.rst\` under the \`..
_validators-callable:\` anchor.
Closes #176
Closes #177
Closes #178
Closes #179
Closes #180
Closes #181
Closes #182
Closes #183
Closes #184
Closes #185
Closes #186
Closes #187
Closes #188
Closes #189
Closes #190
Diffstat (limited to 'tests/library')
| -rw-r--r-- | tests/library/date/basic.test.py | 29 | ||||
| -rw-r--r-- | tests/library/date/iso.test.py | 28 | ||||
| -rw-r--r-- | tests/library/email/basic.test.py | 32 | ||||
| -rw-r--r-- | tests/library/email/strict.test.py | 32 | ||||
| -rw-r--r-- | tests/library/ip.test.py | 13 | ||||
| -rw-r--r-- | tests/library/ip/v4.test.py | 16 | ||||
| -rw-r--r-- | tests/library/ip/v6.test.py | 17 | ||||
| -rw-r--r-- | tests/library/media/regex.test.py | 13 | ||||
| -rw-r--r-- | tests/library/password.test.py | 4 | ||||
| -rw-r--r-- | tests/library/phone.test.py | 34 | ||||
| -rw-r--r-- | tests/library/postal.test.py | 9 | ||||
| -rw-r--r-- | tests/library/url.test.py | 54 | ||||
| -rw-r--r-- | tests/library/zip.test.py | 43 |
13 files changed, 56 insertions, 268 deletions
diff --git a/tests/library/date/basic.test.py b/tests/library/date/basic.test.py deleted file mode 100644 index 9b3f45a..0000000 --- a/tests/library/date/basic.test.py +++ /dev/null @@ -1,29 +0,0 @@ -from edify.library import date - -_TEST_CASES = { - "1/1/2020": True, - "01/01/2020": True, - "1/01/2020": True, - "01/1/2020": True, - "1/1/20": False, - "01/01/20": False, - "1/1/202": False, - "01/01/202": False, - "12/12/2022": True, - "12/12/2": False, - "2021-11-04T22:32:47.142354-10:00": False, - "2021-11-04T22:32:47.142354Z": False, - "2021-11-04T22:32:47.142354": False, - "2021-11-04T22:32:47": False, - "2021-11-04T22:32": False, - "2021-11-04T22": False, - "2021-11-04": False, - "2021-11": False, - "2021": False, - "1-1-2020": False, -} - - -def test_date(): - for candidate, expectation in _TEST_CASES.items(): - assert date(candidate) is expectation diff --git a/tests/library/date/iso.test.py b/tests/library/date/iso.test.py deleted file mode 100644 index de29a4c..0000000 --- a/tests/library/date/iso.test.py +++ /dev/null @@ -1,28 +0,0 @@ -from edify.library import iso_date - -_TEST_CASES = { - "1/1/2020": False, - "01/01/2020": False, - "1/01/2020": False, - "01/1/2020": False, - "1/1/20": False, - "01/01/20": False, - "1/1/202": False, - "01/01/202": False, - "12/12/2022": False, - "12/12/2": False, - "2021-11-04T22:32:47.142354-10:00": True, - "2021-11-04T22:32:47.142354Z": True, - "2021-11-04T22:32:47.142354": True, - "2021-11-04T22:32:47": True, - "2021-11-04T22:32": False, - "2021-11-04T22": False, - "2021-11-04": False, - "2021-11": False, - "2021": False, -} - - -def test_iso_date(): - for candidate, expectation in _TEST_CASES.items(): - assert iso_date(candidate) is expectation diff --git a/tests/library/email/basic.test.py b/tests/library/email/basic.test.py deleted file mode 100644 index e77e3b8..0000000 --- a/tests/library/email/basic.test.py +++ /dev/null @@ -1,32 +0,0 @@ -from edify.library import email - -_TEST_CASES = [ - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected].", False), - ("plainaddress", False), - ("#@%^%#$@#$@#.com", False), - ("@example.com", False), - ("Joe Smith <[email protected]>", False), - ("email.example.com", False), - ("email@[email protected]", False), - ("[email protected]", False), - ("[email protected]", False), - ("[email protected]", False), - ("あいうえお@example.com", False), - ("[email protected]", False), - ("[email protected]", False), -] - - -def test_email(): - for candidate, expectation in _TEST_CASES: - assert email(candidate) is expectation diff --git a/tests/library/email/strict.test.py b/tests/library/email/strict.test.py deleted file mode 100644 index 7cefbce..0000000 --- a/tests/library/email/strict.test.py +++ /dev/null @@ -1,32 +0,0 @@ -from edify.library import email_rfc_5322 - -_TEST_CASES = [ - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected]", True), - ("[email protected].", True), - ("plainaddress", False), - ("#@%^%#$@#$@#.com", False), - ("@example.com", False), - ("Joe Smith <[email protected]>", False), - ("email.example.com", False), - ("email@[email protected]", False), - ("[email protected]", False), - ("[email protected]", False), - ("[email protected]", False), - ("あいうえお@example.com", False), - ("[email protected]", False), - ("[email protected]", False), -] - - -def test_email_rfc_5322(): - for candidate, expectation in _TEST_CASES: - assert email_rfc_5322(candidate) is expectation diff --git a/tests/library/ip.test.py b/tests/library/ip.test.py new file mode 100644 index 0000000..2028024 --- /dev/null +++ b/tests/library/ip.test.py @@ -0,0 +1,13 @@ +from edify.library import ip + + +def test_valid_ipv4(): + assert ip("192.168.1.1") + + +def test_valid_ipv6(): + assert ip("2001:db8::1") + + +def test_bad_ip(): + assert not ip("999.999.999.999") diff --git a/tests/library/ip/v4.test.py b/tests/library/ip/v4.test.py deleted file mode 100644 index 32a1975..0000000 --- a/tests/library/ip/v4.test.py +++ /dev/null @@ -1,16 +0,0 @@ -from edify.library import ipv4 - -_TEST_CASES = { - "192.168.0.1": True, - "244.232.123.233": True, - "363.232.123.233": False, - "234.234234.234.234": False, - "12.12.12.12.12": False, - "0.0.0.0": True, - "987.987.987.987": False, -} - - -def test_ipv4(): - for candidate, expectation in _TEST_CASES.items(): - assert ipv4(candidate) is expectation diff --git a/tests/library/ip/v6.test.py b/tests/library/ip/v6.test.py deleted file mode 100644 index a68f892..0000000 --- a/tests/library/ip/v6.test.py +++ /dev/null @@ -1,17 +0,0 @@ -from edify.library import ipv6 - -_TEST_CASES = { - "2001:0db8:85a3:0000:0000:8a2e:0370:7334": True, - "2001:db8:85a3:0:0:8a2e:370:7334": True, - "2001:db8:85a3::8a2e:370:7334": True, - "2001:db8:85a3:0:0:8A2E:370:7334": True, - "2001:db8:85a3:0:0:8a2e:370:7334:": False, - "2001:db8:85a3:0:0:8a2e:370:7334:7334": False, - "2001:db8:85a3:0:0:8a2e:370:7334:7334:7334": False, - "2001:db8:85a3:0:0:8a2e:370:7334:7334:7334:7334": False, -} - - -def test_ipv6(): - for candidate, expectation in _TEST_CASES.items(): - assert ipv6(candidate) is expectation diff --git a/tests/library/media/regex.test.py b/tests/library/media/regex.test.py new file mode 100644 index 0000000..c1c687c --- /dev/null +++ b/tests/library/media/regex.test.py @@ -0,0 +1,13 @@ +from edify.library import regex + + +def test_valid_regex(): + assert regex(r"^\d+$") + + +def test_invalid_regex(): + assert not regex(r"(?P<name>") + + +def test_non_string(): + assert not regex(42) diff --git a/tests/library/password.test.py b/tests/library/password.test.py index ec3b627..45b1672 100644 --- a/tests/library/password.test.py +++ b/tests/library/password.test.py @@ -13,3 +13,7 @@ def test_password(): ) is False ) + + +def test_password_rejects_non_string(): + assert password(42) is False diff --git a/tests/library/phone.test.py b/tests/library/phone.test.py index 6fe16b1..0076065 100644 --- a/tests/library/phone.test.py +++ b/tests/library/phone.test.py @@ -1,25 +1,13 @@ -from edify.library import phone_number +from edify.library import phone -def test(): - phones = { - "1234567890": True, - "123 456 7890": True, - "123-456-7890": True, - "123.456.7890": True, - "+1 (123) 456-7890": True, - "+1 (123) 456 7890": True, - "+1-(123)-456-7890": True, - "+102 (123) 456-7890": True, - "+91 (123) 456-7890": True, - "90122121": True, - "12345678901": True, - "+1 (124) 232": True, - "+1 (123) 45-890": True, - "+1 (1) 456-7890": True, - "9012": True, - "911": True, - "+1 (615) 243-": False, - } - for phone, expectation in phones.items(): - assert phone_number(phone) == expectation +def test_valid_phone(): + assert phone("+1-555-1234") + + +def test_short_phone(): + assert phone("911") + + +def test_bad_phone(): + assert not phone("!!!") diff --git a/tests/library/postal.test.py b/tests/library/postal.test.py new file mode 100644 index 0000000..f4b3dc3 --- /dev/null +++ b/tests/library/postal.test.py @@ -0,0 +1,9 @@ +from edify.library import postal + + +def test_valid_postal(): + assert postal("12345") + + +def test_bad_postal(): + assert not postal("nope-!!!") diff --git a/tests/library/url.test.py b/tests/library/url.test.py index 293bd34..d4b2eeb 100644 --- a/tests/library/url.test.py +++ b/tests/library/url.test.py @@ -1,55 +1,13 @@ -import pytest - from edify.library import url -_URLS = [ - "example.com", - "www.example.com", - "www.example.com/path/to/file", - "http://www.example.com", - "http://example.com", - "http://www.example.com/path/to/page", - "https://example.com", - "https://www.example.com/", - "https://www.example.com/path/to/page", - "//example.com", -] - - -def test_all_protocols(): - match_list = ["proto", "no_proto"] - expected = [True] * 9 + [False] - for uri, expectation in zip(_URLS, expected, strict=True): - assert url(uri, match=match_list) is expectation - - -def test_proto_only(): - match_list = ["proto"] - expected = [False] * 3 + [True] * 6 + [False] - for uri, expectation in zip(_URLS, expected, strict=True): - assert url(uri, match=match_list) is expectation - - -def test_no_proto_only(): - match_list = ["no_proto"] - expected = [True] * 3 + [False] * 7 - for uri, expectation in zip(_URLS, expected, strict=True): - assert url(uri, match=match_list) is expectation - -def test_invalid_protocol(): - for uri in _URLS: - with pytest.raises(ValueError, match="Invalid protocol"): - url(uri, match=["invalid"]) +def test_valid_http_url(): + assert url("http://example.com") -def test_invalid_match_type(): - for uri in _URLS: - with pytest.raises(TypeError, match="must be a list"): - url(uri, match="invalid") +def test_valid_https_url(): + assert url("https://example.com/path") -def test_empty_match_list(): - for uri in _URLS: - with pytest.raises(ValueError, match="must not be empty"): - url(uri, match=[]) +def test_bad_url(): + assert not url("nope !!!") diff --git a/tests/library/zip.test.py b/tests/library/zip.test.py deleted file mode 100644 index 77a3e0e..0000000 --- a/tests/library/zip.test.py +++ /dev/null @@ -1,43 +0,0 @@ -import pytest - -from edify.library import zip - -_DEFAULT_US_ZIPS = { - "12345": True, - "12345-1234": True, - "12345-123456": False, - "1234": False, -} - -_INDIA_ZIPS = { - "123456": True, - "000000": False, - "012345": False, - "12345": False, - "1234567": False, -} - - -def test_valid_zips(): - for candidate, expectation in _DEFAULT_US_ZIPS.items(): - assert zip(candidate) is expectation - - -def test_invalid_locale(): - with pytest.raises(ValueError, match="locale must be one of"): - zip("12345", locale="INVALID") - - -def test_invalid_locale_type(): - with pytest.raises(TypeError, match="locale must be a string"): - zip("12345", 5) - - -def test_empty_locale(): - with pytest.raises(ValueError, match="locale cannot be empty"): - zip("12345", "") - - -def test_locale_india(): - for candidate, expectation in _INDIA_ZIPS.items(): - assert zip(candidate, locale="IN") is expectation |
