aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornatsuoto <[email protected]>2026-07-14 18:51:18 +0530
committernatsuoto <[email protected]>2026-07-14 18:51:18 +0530
commit3ebe0d7b2b61a908255a45118f4d07ef805a0e01 (patch)
tree27bb8bc42bf11521ac0534e6fcd7c08abc6405a3
parentf3efea9b9ec120c876e7b1b4e4b3e785e7c594d6 (diff)
downloadedify-3ebe0d7b2b61a908255a45118f4d07ef805a0e01.tar.xz
edify-3ebe0d7b2b61a908255a45118f4d07ef805a0e01.zip
feat(atoms): expand from 13 to 83 general-purpose composable fragments
-rw-r--r--edify/atoms/__init__.py140
-rw-r--r--edify/atoms/alnum.py8
-rw-r--r--edify/atoms/ascii.py8
-rw-r--r--edify/atoms/base32.py10
-rw-r--r--edify/atoms/base58.py19
-rw-r--r--edify/atoms/base64.py20
-rw-r--r--edify/atoms/base64url.py18
-rw-r--r--edify/atoms/bic.py27
-rw-r--r--edify/atoms/binnum.py8
-rw-r--r--edify/atoms/boolean.py29
-rw-r--r--edify/atoms/braces.py8
-rw-r--r--edify/atoms/brackets.py8
-rw-r--r--edify/atoms/cidr.py9
-rw-r--r--edify/atoms/clock12.py27
-rw-r--r--edify/atoms/creditcard.py8
-rw-r--r--edify/atoms/currency.py8
-rw-r--r--edify/atoms/day.py12
-rw-r--r--edify/atoms/decimal.py8
-rw-r--r--edify/atoms/duration.py47
-rw-r--r--edify/atoms/email.py29
-rw-r--r--edify/atoms/epoch.py8
-rw-r--r--edify/atoms/extension.py8
-rw-r--r--edify/atoms/filename.py8
-rw-r--r--edify/atoms/filepath.py30
-rw-r--r--edify/atoms/floatnum.py28
-rw-r--r--edify/atoms/guid.py25
-rw-r--r--edify/atoms/hexnum.py9
-rw-r--r--edify/atoms/hexstring.py9
-rw-r--r--edify/atoms/httpmethod.py18
-rw-r--r--edify/atoms/httpstatus.py8
-rw-r--r--edify/atoms/iban.py19
-rw-r--r--edify/atoms/integer.py8
-rw-r--r--edify/atoms/ipv4.py9
-rw-r--r--edify/atoms/ipv6.py21
-rw-r--r--edify/atoms/isodatetime.py45
-rw-r--r--edify/atoms/letter.py8
-rw-r--r--edify/atoms/line.py8
-rw-r--r--edify/atoms/lower.py8
-rw-r--r--edify/atoms/mac.py19
-rw-r--r--edify/atoms/md5.py9
-rw-r--r--edify/atoms/mimetype.py25
-rw-r--r--edify/atoms/money.py13
-rw-r--r--edify/atoms/month.py11
-rw-r--r--edify/atoms/natural.py8
-rw-r--r--edify/atoms/objectid.py9
-rw-r--r--edify/atoms/octnum.py8
-rw-r--r--edify/atoms/oid.py8
-rw-r--r--edify/atoms/parens.py8
-rw-r--r--edify/atoms/percent.py19
-rw-r--r--edify/atoms/printable.py8
-rw-r--r--edify/atoms/protocol.py18
-rw-r--r--edify/atoms/quoted.py8
-rw-r--r--edify/atoms/ratio.py8
-rw-r--r--edify/atoms/rgbcolor.py27
-rw-r--r--edify/atoms/scheme.py20
-rw-r--r--edify/atoms/scientific.py25
-rw-r--r--edify/atoms/sha256.py9
-rw-r--r--edify/atoms/signed.py8
-rw-r--r--edify/atoms/space.py8
-rw-r--r--edify/atoms/timezone.py11
-rw-r--r--edify/atoms/tld.py8
-rw-r--r--edify/atoms/truefalse.py15
-rw-r--r--edify/atoms/ulid.py20
-rw-r--r--edify/atoms/unsigned.py8
-rw-r--r--edify/atoms/upper.py8
-rw-r--r--edify/atoms/uri.py23
-rw-r--r--edify/atoms/url.py16
-rw-r--r--edify/atoms/weekday.py23
-rw-r--r--edify/atoms/word.py8
-rw-r--r--edify/atoms/year.py8
-rw-r--r--edify/atoms/yesno.py19
-rw-r--r--tests/atoms/alnum.test.py26
-rw-r--r--tests/atoms/ascii.test.py26
-rw-r--r--tests/atoms/base32.test.py26
-rw-r--r--tests/atoms/base58.test.py26
-rw-r--r--tests/atoms/base64.test.py26
-rw-r--r--tests/atoms/base64url.test.py26
-rw-r--r--tests/atoms/bic.test.py26
-rw-r--r--tests/atoms/binnum.test.py26
-rw-r--r--tests/atoms/boolean.test.py26
-rw-r--r--tests/atoms/braces.test.py26
-rw-r--r--tests/atoms/brackets.test.py26
-rw-r--r--tests/atoms/cidr.test.py26
-rw-r--r--tests/atoms/clock12.test.py26
-rw-r--r--tests/atoms/creditcard.test.py26
-rw-r--r--tests/atoms/currency.test.py26
-rw-r--r--tests/atoms/day.test.py26
-rw-r--r--tests/atoms/decimal.test.py26
-rw-r--r--tests/atoms/duration.test.py26
-rw-r--r--tests/atoms/email.test.py26
-rw-r--r--tests/atoms/epoch.test.py26
-rw-r--r--tests/atoms/extension.test.py26
-rw-r--r--tests/atoms/filename.test.py26
-rw-r--r--tests/atoms/filepath.test.py26
-rw-r--r--tests/atoms/floatnum.test.py26
-rw-r--r--tests/atoms/guid.test.py26
-rw-r--r--tests/atoms/hexnum.test.py26
-rw-r--r--tests/atoms/hexstring.test.py26
-rw-r--r--tests/atoms/httpmethod.test.py26
-rw-r--r--tests/atoms/httpstatus.test.py26
-rw-r--r--tests/atoms/iban.test.py26
-rw-r--r--tests/atoms/integer.test.py26
-rw-r--r--tests/atoms/ipv4.test.py26
-rw-r--r--tests/atoms/ipv6.test.py26
-rw-r--r--tests/atoms/isodatetime.test.py26
-rw-r--r--tests/atoms/letter.test.py26
-rw-r--r--tests/atoms/line.test.py26
-rw-r--r--tests/atoms/lower.test.py26
-rw-r--r--tests/atoms/mac.test.py26
-rw-r--r--tests/atoms/md5.test.py26
-rw-r--r--tests/atoms/mimetype.test.py26
-rw-r--r--tests/atoms/money.test.py26
-rw-r--r--tests/atoms/month.test.py26
-rw-r--r--tests/atoms/natural.test.py26
-rw-r--r--tests/atoms/objectid.test.py26
-rw-r--r--tests/atoms/octnum.test.py26
-rw-r--r--tests/atoms/oid.test.py26
-rw-r--r--tests/atoms/parens.test.py26
-rw-r--r--tests/atoms/percent.test.py26
-rw-r--r--tests/atoms/printable.test.py26
-rw-r--r--tests/atoms/protocol.test.py26
-rw-r--r--tests/atoms/quoted.test.py26
-rw-r--r--tests/atoms/ratio.test.py26
-rw-r--r--tests/atoms/rgbcolor.test.py26
-rw-r--r--tests/atoms/scheme.test.py26
-rw-r--r--tests/atoms/scientific.test.py26
-rw-r--r--tests/atoms/sha256.test.py26
-rw-r--r--tests/atoms/signed.test.py26
-rw-r--r--tests/atoms/space.test.py26
-rw-r--r--tests/atoms/timezone.test.py26
-rw-r--r--tests/atoms/tld.test.py26
-rw-r--r--tests/atoms/truefalse.test.py26
-rw-r--r--tests/atoms/ulid.test.py26
-rw-r--r--tests/atoms/unsigned.test.py26
-rw-r--r--tests/atoms/upper.test.py26
-rw-r--r--tests/atoms/uri.test.py26
-rw-r--r--tests/atoms/url.test.py26
-rw-r--r--tests/atoms/weekday.test.py26
-rw-r--r--tests/atoms/word.test.py26
-rw-r--r--tests/atoms/year.test.py26
-rw-r--r--tests/atoms/yesno.test.py26
141 files changed, 2991 insertions, 0 deletions
diff --git a/edify/atoms/__init__.py b/edify/atoms/__init__.py
index 17e1c67..066a56e 100644
--- a/edify/atoms/__init__.py
+++ b/edify/atoms/__init__.py
@@ -5,32 +5,172 @@ Each atom is an unanchored fragment meant to be spliced into a builder via
directly from ``edify.atoms``.
"""
+from edify.atoms.alnum import alnum
+from edify.atoms.ascii import ascii
+from edify.atoms.base32 import base32
+from edify.atoms.base58 import base58
+from edify.atoms.base64 import base64
+from edify.atoms.base64url import base64url
+from edify.atoms.bic import bic
+from edify.atoms.binnum import binnum
+from edify.atoms.boolean import boolean
+from edify.atoms.braces import braces
+from edify.atoms.brackets import brackets
+from edify.atoms.cidr import cidr
from edify.atoms.clock import clock
+from edify.atoms.clock12 import clock12
+from edify.atoms.creditcard import creditcard
+from edify.atoms.currency import currency
+from edify.atoms.day import day
+from edify.atoms.decimal import decimal
+from edify.atoms.duration import duration
+from edify.atoms.email import email
+from edify.atoms.epoch import epoch
+from edify.atoms.extension import extension
+from edify.atoms.filename import filename
+from edify.atoms.filepath import filepath
+from edify.atoms.floatnum import floatnum
+from edify.atoms.guid import guid
from edify.atoms.hexcolor import hexcolor
+from edify.atoms.hexnum import hexnum
+from edify.atoms.hexstring import hexstring
from edify.atoms.hostname import hostname
+from edify.atoms.httpmethod import httpmethod
+from edify.atoms.httpstatus import httpstatus
+from edify.atoms.iban import iban
+from edify.atoms.integer import integer
+from edify.atoms.ipv4 import ipv4
+from edify.atoms.ipv6 import ipv6
from edify.atoms.isodate import isodate
+from edify.atoms.isodatetime import isodatetime
from edify.atoms.label import label
+from edify.atoms.letter import letter
+from edify.atoms.line import line
from edify.atoms.localpart import localpart
+from edify.atoms.lower import lower
+from edify.atoms.mac import mac
+from edify.atoms.md5 import md5
+from edify.atoms.mimetype import mimetype
+from edify.atoms.money import money
+from edify.atoms.month import month
+from edify.atoms.natural import natural
from edify.atoms.nibble import nibble
+from edify.atoms.objectid import objectid
from edify.atoms.octet import octet
+from edify.atoms.octnum import octnum
+from edify.atoms.oid import oid
+from edify.atoms.parens import parens
+from edify.atoms.percent import percent
from edify.atoms.port import port
+from edify.atoms.printable import printable
+from edify.atoms.protocol import protocol
+from edify.atoms.quoted import quoted
+from edify.atoms.ratio import ratio
+from edify.atoms.rgbcolor import rgbcolor
+from edify.atoms.scheme import scheme
+from edify.atoms.scientific import scientific
from edify.atoms.semver import semver
+from edify.atoms.sha256 import sha256
+from edify.atoms.signed import signed
from edify.atoms.slug import slug
+from edify.atoms.space import space
+from edify.atoms.timezone import timezone
+from edify.atoms.tld import tld
+from edify.atoms.truefalse import truefalse
+from edify.atoms.ulid import ulid
+from edify.atoms.unsigned import unsigned
+from edify.atoms.upper import upper
+from edify.atoms.uri import uri
+from edify.atoms.url import url
from edify.atoms.username import username
from edify.atoms.uuid import uuid
+from edify.atoms.weekday import weekday
+from edify.atoms.word import word
+from edify.atoms.year import year
+from edify.atoms.yesno import yesno
__all__ = [
+ "alnum",
+ "ascii",
+ "base32",
+ "base58",
+ "base64",
+ "base64url",
+ "bic",
+ "binnum",
+ "boolean",
+ "braces",
+ "brackets",
+ "cidr",
"clock",
+ "clock12",
+ "creditcard",
+ "currency",
+ "day",
+ "decimal",
+ "duration",
+ "email",
+ "epoch",
+ "extension",
+ "filename",
+ "filepath",
+ "floatnum",
+ "guid",
"hexcolor",
+ "hexnum",
+ "hexstring",
"hostname",
+ "httpmethod",
+ "httpstatus",
+ "iban",
+ "integer",
+ "ipv4",
+ "ipv6",
"isodate",
+ "isodatetime",
"label",
+ "letter",
+ "line",
"localpart",
+ "lower",
+ "mac",
+ "md5",
+ "mimetype",
+ "money",
+ "month",
+ "natural",
"nibble",
+ "objectid",
"octet",
+ "octnum",
+ "oid",
+ "parens",
+ "percent",
"port",
+ "printable",
+ "protocol",
+ "quoted",
+ "ratio",
+ "rgbcolor",
+ "scheme",
+ "scientific",
"semver",
+ "sha256",
+ "signed",
"slug",
+ "space",
+ "timezone",
+ "tld",
+ "truefalse",
+ "ulid",
+ "unsigned",
+ "upper",
+ "uri",
+ "url",
"username",
"uuid",
+ "weekday",
+ "word",
+ "year",
+ "yesno",
]
diff --git a/edify/atoms/alnum.py b/edify/atoms/alnum.py
new file mode 100644
index 0000000..5fa09b1
--- /dev/null
+++ b/edify/atoms/alnum.py
@@ -0,0 +1,8 @@
+"""``alnum`` — a single alphanumeric ASCII character."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+alnum = Pattern().alphanumeric()
+"""Composable :class:`Pattern` fragment for one alphanumeric char ``[a-zA-Z0-9]``."""
diff --git a/edify/atoms/ascii.py b/edify/atoms/ascii.py
new file mode 100644
index 0000000..0089891
--- /dev/null
+++ b/edify/atoms/ascii.py
@@ -0,0 +1,8 @@
+"""``ascii`` — a single ASCII character (``0x00``-``0x7F``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+ascii = Pattern().range("\x00", "\x7f")
+"""Composable :class:`Pattern` fragment for any ASCII code point."""
diff --git a/edify/atoms/base32.py b/edify/atoms/base32.py
new file mode 100644
index 0000000..87de908
--- /dev/null
+++ b/edify/atoms/base32.py
@@ -0,0 +1,10 @@
+"""``base32`` — base32-encoded string (RFC 4648, with optional padding)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+base32 = (
+ Pattern().one_or_more().any_of().range("A", "Z").range("2", "7").end().zero_or_more().char("=")
+)
+"""Composable :class:`Pattern` fragment for a base32 string."""
diff --git a/edify/atoms/base58.py b/edify/atoms/base58.py
new file mode 100644
index 0000000..f52d8ce
--- /dev/null
+++ b/edify/atoms/base58.py
@@ -0,0 +1,19 @@
+"""``base58`` — Bitcoin-style base58 string."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+base58 = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .range("1", "9")
+ .range("A", "H")
+ .range("J", "N")
+ .range("P", "Z")
+ .range("a", "k")
+ .range("m", "z")
+ .end()
+)
+"""Composable :class:`Pattern` fragment for a base58 string."""
diff --git a/edify/atoms/base64.py b/edify/atoms/base64.py
new file mode 100644
index 0000000..05415cb
--- /dev/null
+++ b/edify/atoms/base64.py
@@ -0,0 +1,20 @@
+"""``base64`` — base64-encoded string (with optional padding)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+base64 = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .range("A", "Z")
+ .range("a", "z")
+ .range("0", "9")
+ .char("+")
+ .char("/")
+ .end()
+ .zero_or_more()
+ .char("=")
+)
+"""Composable :class:`Pattern` fragment for a standard base64 string."""
diff --git a/edify/atoms/base64url.py b/edify/atoms/base64url.py
new file mode 100644
index 0000000..e8f0830
--- /dev/null
+++ b/edify/atoms/base64url.py
@@ -0,0 +1,18 @@
+"""``base64url`` — URL-safe base64 string (``+`` and ``/`` replaced with ``_`` and ``-``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+base64url = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .range("A", "Z")
+ .range("a", "z")
+ .range("0", "9")
+ .char("_")
+ .char("-")
+ .end()
+)
+"""Composable :class:`Pattern` fragment for a URL-safe base64 string."""
diff --git a/edify/atoms/bic.py b/edify/atoms/bic.py
new file mode 100644
index 0000000..02f08c1
--- /dev/null
+++ b/edify/atoms/bic.py
@@ -0,0 +1,27 @@
+"""``bic`` — Bank Identifier Code / SWIFT code shape."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+bic = (
+ Pattern()
+ .exactly(4)
+ .uppercase()
+ .exactly(2)
+ .uppercase()
+ .exactly(2)
+ .any_of()
+ .range("A", "Z")
+ .range("0", "9")
+ .end()
+ .optional()
+ .group()
+ .exactly(3)
+ .any_of()
+ .range("A", "Z")
+ .range("0", "9")
+ .end()
+ .end()
+)
+"""Composable :class:`Pattern` fragment for a BIC/SWIFT code."""
diff --git a/edify/atoms/binnum.py b/edify/atoms/binnum.py
new file mode 100644
index 0000000..b98d10d
--- /dev/null
+++ b/edify/atoms/binnum.py
@@ -0,0 +1,8 @@
+"""``binnum`` — binary integer literal (``0b`` or ``0B`` prefix)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+binnum = Pattern().char("0").any_of_chars("bB").one_or_more().any_of_chars("01")
+"""Composable :class:`Pattern` fragment for a ``0bNN``-shaped binary literal."""
diff --git a/edify/atoms/boolean.py b/edify/atoms/boolean.py
new file mode 100644
index 0000000..b6a6ff4
--- /dev/null
+++ b/edify/atoms/boolean.py
@@ -0,0 +1,29 @@
+"""``boolean`` — any common boolean spelling (true/false/yes/no/on/off/1/0)."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+
+boolean = any_of(
+ Pattern().string("true"),
+ Pattern().string("false"),
+ Pattern().string("True"),
+ Pattern().string("False"),
+ Pattern().string("TRUE"),
+ Pattern().string("FALSE"),
+ Pattern().string("yes"),
+ Pattern().string("no"),
+ Pattern().string("Yes"),
+ Pattern().string("No"),
+ Pattern().string("YES"),
+ Pattern().string("NO"),
+ Pattern().string("on"),
+ Pattern().string("off"),
+ Pattern().string("On"),
+ Pattern().string("Off"),
+ Pattern().string("ON"),
+ Pattern().string("OFF"),
+ Pattern().string("1"),
+ Pattern().string("0"),
+)
+"""Composable :class:`Pattern` fragment for any common boolean spelling."""
diff --git a/edify/atoms/braces.py b/edify/atoms/braces.py
new file mode 100644
index 0000000..5e15662
--- /dev/null
+++ b/edify/atoms/braces.py
@@ -0,0 +1,8 @@
+"""``braces`` — a curly-brace group with no nested braces."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+braces = Pattern().char("{").zero_or_more().anything_but_chars("}").char("}")
+"""Composable :class:`Pattern` fragment for a ``{...}`` group."""
diff --git a/edify/atoms/brackets.py b/edify/atoms/brackets.py
new file mode 100644
index 0000000..26abd99
--- /dev/null
+++ b/edify/atoms/brackets.py
@@ -0,0 +1,8 @@
+"""``brackets`` — a square-bracket group with no nested brackets."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+brackets = Pattern().char("[").zero_or_more().anything_but_chars("]").char("]")
+"""Composable :class:`Pattern` fragment for a ``[...]`` group."""
diff --git a/edify/atoms/cidr.py b/edify/atoms/cidr.py
new file mode 100644
index 0000000..4f84feb
--- /dev/null
+++ b/edify/atoms/cidr.py
@@ -0,0 +1,9 @@
+"""``cidr`` — IPv4 CIDR notation (``address/prefix``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+from edify.atoms.ipv4 import ipv4
+
+cidr = Pattern().use(ipv4).char("/").between(1, 2).digit()
+"""Composable :class:`Pattern` fragment for IPv4 CIDR notation."""
diff --git a/edify/atoms/clock12.py b/edify/atoms/clock12.py
new file mode 100644
index 0000000..fcd5bd3
--- /dev/null
+++ b/edify/atoms/clock12.py
@@ -0,0 +1,27 @@
+"""``clock12`` — 12-hour clock time with AM/PM."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+clock12 = (
+ Pattern()
+ .any_of()
+ .subexpression(Pattern().char("1").range("0", "2"))
+ .subexpression(Pattern().optional().char("0").range("1", "9"))
+ .end()
+ .char(":")
+ .range("0", "5")
+ .digit()
+ .optional()
+ .group()
+ .char(":")
+ .range("0", "5")
+ .digit()
+ .end()
+ .optional()
+ .whitespace_char()
+ .any_of_chars("AaPp")
+ .any_of_chars("Mm")
+)
+"""Composable :class:`Pattern` fragment for a 12-hour clock time with AM/PM."""
diff --git a/edify/atoms/creditcard.py b/edify/atoms/creditcard.py
new file mode 100644
index 0000000..821b766
--- /dev/null
+++ b/edify/atoms/creditcard.py
@@ -0,0 +1,8 @@
+"""``creditcard`` — 13-to-19 digit credit-card number (no Luhn check)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+creditcard = Pattern().between(13, 19).digit()
+"""Composable :class:`Pattern` fragment for a credit-card number shape."""
diff --git a/edify/atoms/currency.py b/edify/atoms/currency.py
new file mode 100644
index 0000000..bdd28b3
--- /dev/null
+++ b/edify/atoms/currency.py
@@ -0,0 +1,8 @@
+"""``currency`` — three-letter ISO 4217 currency code."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+currency = Pattern().exactly(3).uppercase()
+"""Composable :class:`Pattern` fragment for an ISO 4217 currency code."""
diff --git a/edify/atoms/day.py b/edify/atoms/day.py
new file mode 100644
index 0000000..4cb56a2
--- /dev/null
+++ b/edify/atoms/day.py
@@ -0,0 +1,12 @@
+"""``day`` — two-digit day-of-month (``01``-``31``)."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+
+day = any_of(
+ Pattern().char("0").range("1", "9"),
+ Pattern().range("1", "2").digit(),
+ Pattern().char("3").range("0", "1"),
+)
+"""Composable :class:`Pattern` fragment for a two-digit day-of-month."""
diff --git a/edify/atoms/decimal.py b/edify/atoms/decimal.py
new file mode 100644
index 0000000..7124fcb
--- /dev/null
+++ b/edify/atoms/decimal.py
@@ -0,0 +1,8 @@
+"""``decimal`` — decimal number with a fractional part (``N.M``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+decimal = Pattern().one_or_more().digit().char(".").one_or_more().digit()
+"""Composable :class:`Pattern` fragment for a decimal number ``digits.digits``."""
diff --git a/edify/atoms/duration.py b/edify/atoms/duration.py
new file mode 100644
index 0000000..04505ce
--- /dev/null
+++ b/edify/atoms/duration.py
@@ -0,0 +1,47 @@
+"""``duration`` — ISO 8601 duration shape (``PnYnMnDTnHnMnS``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+
+def _num_with_letter(letter: str) -> Pattern:
+ return (
+ Pattern()
+ .optional()
+ .group()
+ .one_or_more()
+ .digit()
+ .optional()
+ .group()
+ .char(".")
+ .one_or_more()
+ .digit()
+ .end()
+ .char(letter)
+ .end()
+ )
+
+
+duration = (
+ Pattern()
+ .char("P")
+ .assert_ahead()
+ .any_char()
+ .end()
+ .subexpression(_num_with_letter("Y"))
+ .subexpression(_num_with_letter("M"))
+ .subexpression(_num_with_letter("W"))
+ .subexpression(_num_with_letter("D"))
+ .optional()
+ .group()
+ .char("T")
+ .assert_ahead()
+ .digit()
+ .end()
+ .subexpression(_num_with_letter("H"))
+ .subexpression(_num_with_letter("M"))
+ .subexpression(_num_with_letter("S"))
+ .end()
+)
+"""Composable :class:`Pattern` fragment for an ISO 8601 duration."""
diff --git a/edify/atoms/email.py b/edify/atoms/email.py
new file mode 100644
index 0000000..ce40ed4
--- /dev/null
+++ b/edify/atoms/email.py
@@ -0,0 +1,29 @@
+"""``email`` — permissive email address shape."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+email = (
+ Pattern()
+ .one_or_more()
+ .any_of()
+ .range("a", "z")
+ .range("A", "Z")
+ .range("0", "9")
+ .char(".")
+ .char("_")
+ .char("+")
+ .char("-")
+ .end()
+ .char("@")
+ .one_or_more()
+ .any_of()
+ .range("a", "z")
+ .range("A", "Z")
+ .range("0", "9")
+ .char(".")
+ .char("-")
+ .end()
+)
+"""Composable :class:`Pattern` fragment for a permissive email address."""
diff --git a/edify/atoms/epoch.py b/edify/atoms/epoch.py
new file mode 100644
index 0000000..87293ac
--- /dev/null
+++ b/edify/atoms/epoch.py
@@ -0,0 +1,8 @@
+"""``epoch`` — Unix epoch seconds (10-digit timestamp)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+epoch = Pattern().exactly(10).digit()
+"""Composable :class:`Pattern` fragment for a 10-digit Unix epoch timestamp."""
diff --git a/edify/atoms/extension.py b/edify/atoms/extension.py
new file mode 100644
index 0000000..888d5a2
--- /dev/null
+++ b/edify/atoms/extension.py
@@ -0,0 +1,8 @@
+"""``extension`` — a file extension (``.ext``, alphanumeric)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+extension = Pattern().char(".").one_or_more().alphanumeric()
+"""Composable :class:`Pattern` fragment for a file extension."""
diff --git a/edify/atoms/filename.py b/edify/atoms/filename.py
new file mode 100644
index 0000000..515acd5
--- /dev/null
+++ b/edify/atoms/filename.py
@@ -0,0 +1,8 @@
+"""``filename`` — a filename with no path separators or reserved characters."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+filename = Pattern().one_or_more().anything_but_chars('/\\<>:"|?*\x00')
+"""Composable :class:`Pattern` fragment for a filename with no path separators."""
diff --git a/edify/atoms/filepath.py b/edify/atoms/filepath.py
new file mode 100644
index 0000000..23540f5
--- /dev/null
+++ b/edify/atoms/filepath.py
@@ -0,0 +1,30 @@
+"""``filepath`` — a POSIX or Windows file path shape."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+
+_posix = (
+ Pattern()
+ .optional()
+ .char("/")
+ .one_or_more()
+ .group()
+ .anything_but_chars("/\x00")
+ .optional()
+ .char("/")
+ .end()
+)
+_windows = (
+ Pattern()
+ .letter()
+ .string(":")
+ .one_or_more()
+ .group()
+ .char("\\")
+ .anything_but_chars('/\\<>:"|?*\x00\n')
+ .end()
+)
+
+filepath = any_of(_posix, _windows)
+"""Composable :class:`Pattern` fragment for a filesystem path."""
diff --git a/edify/atoms/floatnum.py b/edify/atoms/floatnum.py
new file mode 100644
index 0000000..3f30f12
--- /dev/null
+++ b/edify/atoms/floatnum.py
@@ -0,0 +1,28 @@
+"""``floatnum`` — general floating-point number (integer, decimal, or scientific)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+floatnum = (
+ Pattern()
+ .optional()
+ .any_of_chars("+-")
+ .one_or_more()
+ .digit()
+ .optional()
+ .group()
+ .char(".")
+ .one_or_more()
+ .digit()
+ .end()
+ .optional()
+ .group()
+ .any_of_chars("eE")
+ .optional()
+ .any_of_chars("+-")
+ .one_or_more()
+ .digit()
+ .end()
+)
+"""Composable :class:`Pattern` fragment for a general floating-point number."""
diff --git a/edify/atoms/guid.py b/edify/atoms/guid.py
new file mode 100644
index 0000000..e13ed7b
--- /dev/null
+++ b/edify/atoms/guid.py
@@ -0,0 +1,25 @@
+"""``guid`` — any-version UUID/GUID (8-4-4-4-12 hex)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+from edify.atoms.nibble import nibble
+
+guid = (
+ Pattern()
+ .exactly(8)
+ .use(nibble)
+ .char("-")
+ .exactly(4)
+ .use(nibble)
+ .char("-")
+ .exactly(4)
+ .use(nibble)
+ .char("-")
+ .exactly(4)
+ .use(nibble)
+ .char("-")
+ .exactly(12)
+ .use(nibble)
+)
+"""Composable :class:`Pattern` fragment for an any-version UUID/GUID."""
diff --git a/edify/atoms/hexnum.py b/edify/atoms/hexnum.py
new file mode 100644
index 0000000..acc8f79
--- /dev/null
+++ b/edify/atoms/hexnum.py
@@ -0,0 +1,9 @@
+"""``hexnum`` — hexadecimal integer literal (``0x`` or ``0X`` prefix)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+from edify.atoms.nibble import nibble
+
+hexnum = Pattern().char("0").any_of_chars("xX").one_or_more().use(nibble)
+"""Composable :class:`Pattern` fragment for a ``0xNN``-shaped hexadecimal literal."""
diff --git a/edify/atoms/hexstring.py b/edify/atoms/hexstring.py
new file mode 100644
index 0000000..4892554
--- /dev/null
+++ b/edify/atoms/hexstring.py
@@ -0,0 +1,9 @@
+"""``hexstring`` — one or more hexadecimal characters."""
+
+from __future__ import annotations
+
+from edify import Pattern
+from edify.atoms.nibble import nibble
+
+hexstring = Pattern().one_or_more().use(nibble)
+"""Composable :class:`Pattern` fragment for a hex character string."""
diff --git a/edify/atoms/httpmethod.py b/edify/atoms/httpmethod.py
new file mode 100644
index 0000000..13f1fb7
--- /dev/null
+++ b/edify/atoms/httpmethod.py
@@ -0,0 +1,18 @@
+"""``httpmethod`` — HTTP request-method name."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+
+httpmethod = any_of(
+ Pattern().string("OPTIONS"),
+ Pattern().string("GET"),
+ Pattern().string("HEAD"),
+ Pattern().string("POST"),
+ Pattern().string("PUT"),
+ Pattern().string("DELETE"),
+ Pattern().string("TRACE"),
+ Pattern().string("CONNECT"),
+ Pattern().string("PATCH"),
+)
+"""Composable :class:`Pattern` fragment for an HTTP request method."""
diff --git a/edify/atoms/httpstatus.py b/edify/atoms/httpstatus.py
new file mode 100644
index 0000000..94dd9bc
--- /dev/null
+++ b/edify/atoms/httpstatus.py
@@ -0,0 +1,8 @@
+"""``httpstatus`` — three-digit HTTP status code (1xx-5xx)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+httpstatus = Pattern().range("1", "5").exactly(2).digit()
+"""Composable :class:`Pattern` fragment for a three-digit HTTP status code."""
diff --git a/edify/atoms/iban.py b/edify/atoms/iban.py
new file mode 100644
index 0000000..82e7542
--- /dev/null
+++ b/edify/atoms/iban.py
@@ -0,0 +1,19 @@
+"""``iban`` — International Bank Account Number shape."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+iban = (
+ Pattern()
+ .exactly(2)
+ .uppercase()
+ .exactly(2)
+ .digit()
+ .between(11, 30)
+ .any_of()
+ .range("A", "Z")
+ .range("0", "9")
+ .end()
+)
+"""Composable :class:`Pattern` fragment for an IBAN."""
diff --git a/edify/atoms/integer.py b/edify/atoms/integer.py
new file mode 100644
index 0000000..4fd0fbc
--- /dev/null
+++ b/edify/atoms/integer.py
@@ -0,0 +1,8 @@
+"""``integer`` — signed decimal integer."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+integer = Pattern().optional().any_of_chars("+-").one_or_more().digit()
+"""Composable :class:`Pattern` fragment for a signed decimal integer."""
diff --git a/edify/atoms/ipv4.py b/edify/atoms/ipv4.py
new file mode 100644
index 0000000..af0597a
--- /dev/null
+++ b/edify/atoms/ipv4.py
@@ -0,0 +1,9 @@
+"""``ipv4`` — dotted-quad IPv4 address."""
+
+from __future__ import annotations
+
+from edify import Pattern
+from edify.atoms.octet import octet
+
+ipv4 = Pattern().use(octet).exactly(3).group().char(".").use(octet).end()
+"""Composable :class:`Pattern` fragment for a dotted-quad IPv4 address."""
diff --git a/edify/atoms/ipv6.py b/edify/atoms/ipv6.py
new file mode 100644
index 0000000..f0bf52e
--- /dev/null
+++ b/edify/atoms/ipv6.py
@@ -0,0 +1,21 @@
+"""``ipv6`` — permissive IPv6 address (full, abbreviated, or ``::``)."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+from edify.atoms.nibble import nibble
+
+ipv6 = any_of(
+ Pattern()
+ .exactly(7)
+ .group()
+ .between(1, 4)
+ .use(nibble)
+ .char(":")
+ .end()
+ .between(1, 4)
+ .use(nibble),
+ Pattern().between(1, 7).group().between(1, 4).use(nibble).char(":").end().char(":"),
+ Pattern().string("::"),
+)
+"""Composable :class:`Pattern` fragment for a permissive IPv6 address."""
diff --git a/edify/atoms/isodatetime.py b/edify/atoms/isodatetime.py
new file mode 100644
index 0000000..54a79d3
--- /dev/null
+++ b/edify/atoms/isodatetime.py
@@ -0,0 +1,45 @@
+"""``isodatetime`` — ISO 8601 combined date-time shape."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+isodatetime = (
+ Pattern()
+ .exactly(4)
+ .digit()
+ .char("-")
+ .exactly(2)
+ .digit()
+ .char("-")
+ .exactly(2)
+ .digit()
+ .any_of_chars("Tt ")
+ .exactly(2)
+ .digit()
+ .char(":")
+ .exactly(2)
+ .digit()
+ .optional()
+ .group()
+ .char(":")
+ .exactly(2)
+ .digit()
+ .optional()
+ .group()
+ .char(".")
+ .one_or_more()
+ .digit()
+ .end()
+ .end()
+ .optional()
+ .group()
+ .any_of()
+ .any_of_chars("Zz")
+ .subexpression(
+ Pattern().any_of_chars("+-").exactly(2).digit().optional().char(":").exactly(2).digit()
+ )
+ .end()
+ .end()
+)
+"""Composable :class:`Pattern` fragment for an ISO 8601 combined date-time."""
diff --git a/edify/atoms/letter.py b/edify/atoms/letter.py
new file mode 100644
index 0000000..305cb74
--- /dev/null
+++ b/edify/atoms/letter.py
@@ -0,0 +1,8 @@
+"""``letter`` — a single ASCII letter."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+letter = Pattern().letter()
+"""Composable :class:`Pattern` fragment for one ASCII letter ``[a-zA-Z]``."""
diff --git a/edify/atoms/line.py b/edify/atoms/line.py
new file mode 100644
index 0000000..8ac767b
--- /dev/null
+++ b/edify/atoms/line.py
@@ -0,0 +1,8 @@
+"""``line`` — one non-empty line of text (no CR/LF)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+line = Pattern().one_or_more().anything_but_chars("\r\n")
+"""Composable :class:`Pattern` fragment for one line of text."""
diff --git a/edify/atoms/lower.py b/edify/atoms/lower.py
new file mode 100644
index 0000000..03e9a9c
--- /dev/null
+++ b/edify/atoms/lower.py
@@ -0,0 +1,8 @@
+"""``lower`` — a single lowercase ASCII letter."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+lower = Pattern().lowercase()
+"""Composable :class:`Pattern` fragment for one lowercase letter ``[a-z]``."""
diff --git a/edify/atoms/mac.py b/edify/atoms/mac.py
new file mode 100644
index 0000000..d018b79
--- /dev/null
+++ b/edify/atoms/mac.py
@@ -0,0 +1,19 @@
+"""``mac`` — MAC address (six hex pairs separated by ``:`` or ``-``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+from edify.atoms.nibble import nibble
+
+mac = (
+ Pattern()
+ .exactly(2)
+ .use(nibble)
+ .exactly(5)
+ .group()
+ .any_of_chars(":-")
+ .exactly(2)
+ .use(nibble)
+ .end()
+)
+"""Composable :class:`Pattern` fragment for a MAC address."""
diff --git a/edify/atoms/md5.py b/edify/atoms/md5.py
new file mode 100644
index 0000000..60f0ae8
--- /dev/null
+++ b/edify/atoms/md5.py
@@ -0,0 +1,9 @@
+"""``md5`` — 32-character hexadecimal MD5 digest."""
+
+from __future__ import annotations
+
+from edify import Pattern
+from edify.atoms.nibble import nibble
+
+md5 = Pattern().exactly(32).use(nibble)
+"""Composable :class:`Pattern` fragment for an MD5 hex digest."""
diff --git a/edify/atoms/mimetype.py b/edify/atoms/mimetype.py
new file mode 100644
index 0000000..5581e04
--- /dev/null
+++ b/edify/atoms/mimetype.py
@@ -0,0 +1,25 @@
+"""``mimetype`` — ``type/subtype`` MIME identifier."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+
+def _token() -> Pattern:
+ return (
+ Pattern()
+ .letter()
+ .zero_or_more()
+ .any_of()
+ .range("a", "z")
+ .range("A", "Z")
+ .range("0", "9")
+ .char("+")
+ .char("-")
+ .char(".")
+ .end()
+ )
+
+
+mimetype = Pattern().use(_token()).char("/").use(_token())
+"""Composable :class:`Pattern` fragment for a ``type/subtype`` MIME identifier."""
diff --git a/edify/atoms/money.py b/edify/atoms/money.py
new file mode 100644
index 0000000..991aefb
--- /dev/null
+++ b/edify/atoms/money.py
@@ -0,0 +1,13 @@
+"""``money`` — currency code combined with an amount (either order)."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+
+_amount = Pattern().one_or_more().digit().optional().group().char(".").one_or_more().digit().end()
+
+money = any_of(
+ Pattern().exactly(3).uppercase().optional().whitespace_char().use(_amount),
+ Pattern().use(_amount).optional().whitespace_char().exactly(3).uppercase(),
+)
+"""Composable :class:`Pattern` fragment for a currency + amount value."""
diff --git a/edify/atoms/month.py b/edify/atoms/month.py
new file mode 100644
index 0000000..95123a7
--- /dev/null
+++ b/edify/atoms/month.py
@@ -0,0 +1,11 @@
+"""``month`` — two-digit calendar month (``01``-``12``)."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+
+month = any_of(
+ Pattern().char("0").range("1", "9"),
+ Pattern().char("1").range("0", "2"),
+)
+"""Composable :class:`Pattern` fragment for a two-digit calendar month."""
diff --git a/edify/atoms/natural.py b/edify/atoms/natural.py
new file mode 100644
index 0000000..e2ee5b7
--- /dev/null
+++ b/edify/atoms/natural.py
@@ -0,0 +1,8 @@
+"""``natural`` — natural number (positive integer, no leading zero)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+natural = Pattern().range("1", "9").zero_or_more().digit()
+"""Composable :class:`Pattern` fragment for a positive integer without a leading zero."""
diff --git a/edify/atoms/objectid.py b/edify/atoms/objectid.py
new file mode 100644
index 0000000..807a42d
--- /dev/null
+++ b/edify/atoms/objectid.py
@@ -0,0 +1,9 @@
+"""``objectid`` — 24-character hex ObjectId."""
+
+from __future__ import annotations
+
+from edify import Pattern
+from edify.atoms.nibble import nibble
+
+objectid = Pattern().exactly(24).use(nibble)
+"""Composable :class:`Pattern` fragment for a 24-hex-character ObjectId."""
diff --git a/edify/atoms/octnum.py b/edify/atoms/octnum.py
new file mode 100644
index 0000000..ff100ed
--- /dev/null
+++ b/edify/atoms/octnum.py
@@ -0,0 +1,8 @@
+"""``octnum`` — octal integer literal (``0o`` or ``0O`` prefix)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+octnum = Pattern().char("0").any_of_chars("oO").one_or_more().range("0", "7")
+"""Composable :class:`Pattern` fragment for a ``0oNN``-shaped octal literal."""
diff --git a/edify/atoms/oid.py b/edify/atoms/oid.py
new file mode 100644
index 0000000..c50847d
--- /dev/null
+++ b/edify/atoms/oid.py
@@ -0,0 +1,8 @@
+"""``oid`` — dotted numeric object identifier."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+oid = Pattern().one_or_more().digit().one_or_more().group().char(".").one_or_more().digit().end()
+"""Composable :class:`Pattern` fragment for a dotted numeric OID."""
diff --git a/edify/atoms/parens.py b/edify/atoms/parens.py
new file mode 100644
index 0000000..1fc3c18
--- /dev/null
+++ b/edify/atoms/parens.py
@@ -0,0 +1,8 @@
+"""``parens`` — a parenthesised group with no nested parentheses."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+parens = Pattern().char("(").zero_or_more().anything_but_chars(")").char(")")
+"""Composable :class:`Pattern` fragment for a ``(...)`` group."""
diff --git a/edify/atoms/percent.py b/edify/atoms/percent.py
new file mode 100644
index 0000000..1f85e07
--- /dev/null
+++ b/edify/atoms/percent.py
@@ -0,0 +1,19 @@
+"""``percent`` — percentage value (``N[.N]%``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+percent = (
+ Pattern()
+ .one_or_more()
+ .digit()
+ .optional()
+ .group()
+ .char(".")
+ .one_or_more()
+ .digit()
+ .end()
+ .char("%")
+)
+"""Composable :class:`Pattern` fragment for a percentage value."""
diff --git a/edify/atoms/printable.py b/edify/atoms/printable.py
new file mode 100644
index 0000000..a76caeb
--- /dev/null
+++ b/edify/atoms/printable.py
@@ -0,0 +1,8 @@
+"""``printable`` — a single printable ASCII character (``0x20``-``0x7E``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+printable = Pattern().range("\x20", "\x7e")
+"""Composable :class:`Pattern` fragment for one printable-ASCII character."""
diff --git a/edify/atoms/protocol.py b/edify/atoms/protocol.py
new file mode 100644
index 0000000..ccfaa22
--- /dev/null
+++ b/edify/atoms/protocol.py
@@ -0,0 +1,18 @@
+"""``protocol`` — common network protocol name."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+
+protocol = any_of(
+ Pattern().string("https"),
+ Pattern().string("http"),
+ Pattern().string("ftps"),
+ Pattern().string("ftp"),
+ Pattern().string("wss"),
+ Pattern().string("ws"),
+ Pattern().string("ssh"),
+ Pattern().string("git"),
+ Pattern().string("file"),
+)
+"""Composable :class:`Pattern` fragment for a common protocol name."""
diff --git a/edify/atoms/quoted.py b/edify/atoms/quoted.py
new file mode 100644
index 0000000..a90e566
--- /dev/null
+++ b/edify/atoms/quoted.py
@@ -0,0 +1,8 @@
+"""``quoted`` — a double-quoted string with no embedded quotes."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+quoted = Pattern().char('"').zero_or_more().anything_but_chars('"').char('"')
+"""Composable :class:`Pattern` fragment for a ``"..."`` string."""
diff --git a/edify/atoms/ratio.py b/edify/atoms/ratio.py
new file mode 100644
index 0000000..a7cd032
--- /dev/null
+++ b/edify/atoms/ratio.py
@@ -0,0 +1,8 @@
+"""``ratio`` — colon-separated ratio (``N:M``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+ratio = Pattern().one_or_more().digit().char(":").one_or_more().digit()
+"""Composable :class:`Pattern` fragment for a ratio value."""
diff --git a/edify/atoms/rgbcolor.py b/edify/atoms/rgbcolor.py
new file mode 100644
index 0000000..804220f
--- /dev/null
+++ b/edify/atoms/rgbcolor.py
@@ -0,0 +1,27 @@
+"""``rgbcolor`` — CSS ``rgb()`` / ``rgba()`` colour value."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+rgbcolor = (
+ Pattern()
+ .string("rgb")
+ .optional()
+ .char("a")
+ .char("(")
+ .zero_or_more()
+ .whitespace_char()
+ .one_or_more()
+ .digit()
+ .zero_or_more()
+ .group()
+ .char(",")
+ .zero_or_more()
+ .whitespace_char()
+ .one_or_more()
+ .digit()
+ .end()
+ .char(")")
+)
+"""Composable :class:`Pattern` fragment for a CSS ``rgb()``/``rgba()`` colour."""
diff --git a/edify/atoms/scheme.py b/edify/atoms/scheme.py
new file mode 100644
index 0000000..ccb2960
--- /dev/null
+++ b/edify/atoms/scheme.py
@@ -0,0 +1,20 @@
+"""``scheme`` — URI scheme shape (letter + letter/digit/+/./-)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+scheme = (
+ Pattern()
+ .letter()
+ .zero_or_more()
+ .any_of()
+ .range("a", "z")
+ .range("A", "Z")
+ .range("0", "9")
+ .char("+")
+ .char(".")
+ .char("-")
+ .end()
+)
+"""Composable :class:`Pattern` fragment for a URI scheme."""
diff --git a/edify/atoms/scientific.py b/edify/atoms/scientific.py
new file mode 100644
index 0000000..250ae61
--- /dev/null
+++ b/edify/atoms/scientific.py
@@ -0,0 +1,25 @@
+"""``scientific`` — number in scientific notation (``N[.N]e[±]N``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+scientific = (
+ Pattern()
+ .optional()
+ .any_of_chars("+-")
+ .one_or_more()
+ .digit()
+ .optional()
+ .group()
+ .char(".")
+ .one_or_more()
+ .digit()
+ .end()
+ .any_of_chars("eE")
+ .optional()
+ .any_of_chars("+-")
+ .one_or_more()
+ .digit()
+)
+"""Composable :class:`Pattern` fragment for a number in scientific notation."""
diff --git a/edify/atoms/sha256.py b/edify/atoms/sha256.py
new file mode 100644
index 0000000..49b8c39
--- /dev/null
+++ b/edify/atoms/sha256.py
@@ -0,0 +1,9 @@
+"""``sha256`` — 64-character hexadecimal SHA-256 digest."""
+
+from __future__ import annotations
+
+from edify import Pattern
+from edify.atoms.nibble import nibble
+
+sha256 = Pattern().exactly(64).use(nibble)
+"""Composable :class:`Pattern` fragment for a SHA-256 hex digest."""
diff --git a/edify/atoms/signed.py b/edify/atoms/signed.py
new file mode 100644
index 0000000..a95395c
--- /dev/null
+++ b/edify/atoms/signed.py
@@ -0,0 +1,8 @@
+"""``signed`` — decimal integer with a required leading sign."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+signed = Pattern().any_of_chars("+-").one_or_more().digit()
+"""Composable :class:`Pattern` fragment for a signed integer with the sign required."""
diff --git a/edify/atoms/space.py b/edify/atoms/space.py
new file mode 100644
index 0000000..84d7980
--- /dev/null
+++ b/edify/atoms/space.py
@@ -0,0 +1,8 @@
+"""``space`` — a single whitespace character."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+space = Pattern().whitespace_char()
+"""Composable :class:`Pattern` fragment for one whitespace character."""
diff --git a/edify/atoms/timezone.py b/edify/atoms/timezone.py
new file mode 100644
index 0000000..bf7c50e
--- /dev/null
+++ b/edify/atoms/timezone.py
@@ -0,0 +1,11 @@
+"""``timezone`` — UTC offset (``Z`` or ``±HH[:]MM``)."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+
+timezone = any_of(
+ Pattern().char("Z"),
+ Pattern().any_of_chars("+-").exactly(2).digit().optional().char(":").exactly(2).digit(),
+)
+"""Composable :class:`Pattern` fragment for a timezone offset."""
diff --git a/edify/atoms/tld.py b/edify/atoms/tld.py
new file mode 100644
index 0000000..01d4e7a
--- /dev/null
+++ b/edify/atoms/tld.py
@@ -0,0 +1,8 @@
+"""``tld`` — top-level domain (2 to 63 letters)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+tld = Pattern().between(2, 63).letter()
+"""Composable :class:`Pattern` fragment for a top-level domain."""
diff --git a/edify/atoms/truefalse.py b/edify/atoms/truefalse.py
new file mode 100644
index 0000000..bb27a83
--- /dev/null
+++ b/edify/atoms/truefalse.py
@@ -0,0 +1,15 @@
+"""``truefalse`` — ``true``/``false`` boolean value in common English forms."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+
+truefalse = any_of(
+ Pattern().string("true"),
+ Pattern().string("false"),
+ Pattern().string("True"),
+ Pattern().string("False"),
+ Pattern().string("TRUE"),
+ Pattern().string("FALSE"),
+)
+"""Composable :class:`Pattern` fragment for a true/false boolean."""
diff --git a/edify/atoms/ulid.py b/edify/atoms/ulid.py
new file mode 100644
index 0000000..a6c4eb2
--- /dev/null
+++ b/edify/atoms/ulid.py
@@ -0,0 +1,20 @@
+"""``ulid`` — 26-character Crockford base32 ULID."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+ulid = (
+ Pattern()
+ .exactly(26)
+ .any_of()
+ .range("0", "9")
+ .range("A", "H")
+ .range("J", "K")
+ .char("M")
+ .char("N")
+ .range("P", "T")
+ .range("V", "Z")
+ .end()
+)
+"""Composable :class:`Pattern` fragment for a Crockford-base32 ULID."""
diff --git a/edify/atoms/unsigned.py b/edify/atoms/unsigned.py
new file mode 100644
index 0000000..361f6f9
--- /dev/null
+++ b/edify/atoms/unsigned.py
@@ -0,0 +1,8 @@
+"""``unsigned`` — unsigned decimal integer (one or more digits)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+unsigned = Pattern().one_or_more().digit()
+"""Composable :class:`Pattern` fragment for an unsigned integer."""
diff --git a/edify/atoms/upper.py b/edify/atoms/upper.py
new file mode 100644
index 0000000..d59c919
--- /dev/null
+++ b/edify/atoms/upper.py
@@ -0,0 +1,8 @@
+"""``upper`` — a single uppercase ASCII letter."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+upper = Pattern().uppercase()
+"""Composable :class:`Pattern` fragment for one uppercase letter ``[A-Z]``."""
diff --git a/edify/atoms/uri.py b/edify/atoms/uri.py
new file mode 100644
index 0000000..8189213
--- /dev/null
+++ b/edify/atoms/uri.py
@@ -0,0 +1,23 @@
+"""``uri`` — generic URI shape (``scheme:opaque``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+uri = (
+ Pattern()
+ .letter()
+ .zero_or_more()
+ .any_of()
+ .range("a", "z")
+ .range("A", "Z")
+ .range("0", "9")
+ .char("+")
+ .char(".")
+ .char("-")
+ .end()
+ .char(":")
+ .one_or_more()
+ .anything_but_chars(" \t\r\n")
+)
+"""Composable :class:`Pattern` fragment for a generic URI."""
diff --git a/edify/atoms/url.py b/edify/atoms/url.py
new file mode 100644
index 0000000..6446906
--- /dev/null
+++ b/edify/atoms/url.py
@@ -0,0 +1,16 @@
+"""``url`` — permissive HTTP/HTTPS URL shape."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+url = (
+ Pattern()
+ .string("http")
+ .optional()
+ .char("s")
+ .string("://")
+ .one_or_more()
+ .anything_but_chars(" \t\r\n")
+)
+"""Composable :class:`Pattern` fragment for an HTTP/HTTPS URL."""
diff --git a/edify/atoms/weekday.py b/edify/atoms/weekday.py
new file mode 100644
index 0000000..749537e
--- /dev/null
+++ b/edify/atoms/weekday.py
@@ -0,0 +1,23 @@
+"""``weekday`` — day-of-week name (three-letter or full English)."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+
+weekday = any_of(
+ Pattern().string("Monday"),
+ Pattern().string("Tuesday"),
+ Pattern().string("Wednesday"),
+ Pattern().string("Thursday"),
+ Pattern().string("Friday"),
+ Pattern().string("Saturday"),
+ Pattern().string("Sunday"),
+ Pattern().string("Mon"),
+ Pattern().string("Tue"),
+ Pattern().string("Wed"),
+ Pattern().string("Thu"),
+ Pattern().string("Fri"),
+ Pattern().string("Sat"),
+ Pattern().string("Sun"),
+)
+"""Composable :class:`Pattern` fragment for an English weekday name."""
diff --git a/edify/atoms/word.py b/edify/atoms/word.py
new file mode 100644
index 0000000..a4e32ea
--- /dev/null
+++ b/edify/atoms/word.py
@@ -0,0 +1,8 @@
+"""``word`` — one or more word characters (``\\w+``)."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+word = Pattern().one_or_more().word()
+"""Composable :class:`Pattern` fragment for a run of word characters."""
diff --git a/edify/atoms/year.py b/edify/atoms/year.py
new file mode 100644
index 0000000..d1e6b89
--- /dev/null
+++ b/edify/atoms/year.py
@@ -0,0 +1,8 @@
+"""``year`` — four-digit calendar year."""
+
+from __future__ import annotations
+
+from edify import Pattern
+
+year = Pattern().exactly(4).digit()
+"""Composable :class:`Pattern` fragment for a four-digit calendar year."""
diff --git a/edify/atoms/yesno.py b/edify/atoms/yesno.py
new file mode 100644
index 0000000..39f1451
--- /dev/null
+++ b/edify/atoms/yesno.py
@@ -0,0 +1,19 @@
+"""``yesno`` — ``yes``/``no`` boolean value in common English forms."""
+
+from __future__ import annotations
+
+from edify import Pattern, any_of
+
+yesno = any_of(
+ Pattern().string("yes"),
+ Pattern().string("no"),
+ Pattern().string("Yes"),
+ Pattern().string("No"),
+ Pattern().string("YES"),
+ Pattern().string("NO"),
+ Pattern().string("y"),
+ Pattern().string("n"),
+ Pattern().string("Y"),
+ Pattern().string("N"),
+)
+"""Composable :class:`Pattern` fragment for a yes/no boolean."""
diff --git a/tests/atoms/alnum.test.py b/tests/atoms/alnum.test.py
new file mode 100644
index 0000000..3e3e6c0
--- /dev/null
+++ b/tests/atoms/alnum.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import alnum
+
+
+def _anchored():
+ return Pattern().start_of_input().use(alnum).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("a")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("!")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(alnum).end_of_input()
+ assert embedded("v=" + "a")
+ assert not embedded("a")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = alnum.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/ascii.test.py b/tests/atoms/ascii.test.py
new file mode 100644
index 0000000..4f94b0f
--- /dev/null
+++ b/tests/atoms/ascii.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import ascii
+
+
+def _anchored():
+ return Pattern().start_of_input().use(ascii).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("a")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("☃")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(ascii).end_of_input()
+ assert embedded("v=" + "a")
+ assert not embedded("a")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = ascii.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/base32.test.py b/tests/atoms/base32.test.py
new file mode 100644
index 0000000..b55df31
--- /dev/null
+++ b/tests/atoms/base32.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import base32
+
+
+def _anchored():
+ return Pattern().start_of_input().use(base32).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("JBSWY3DP===")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("lower")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(base32).end_of_input()
+ assert embedded("v=" + "JBSWY3DP===")
+ assert not embedded("JBSWY3DP===")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = base32.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/base58.test.py b/tests/atoms/base58.test.py
new file mode 100644
index 0000000..7af4b1c
--- /dev/null
+++ b/tests/atoms/base58.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import base58
+
+
+def _anchored():
+ return Pattern().start_of_input().use(base58).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("3P3Ceff5tj")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("0")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(base58).end_of_input()
+ assert embedded("v=" + "3P3Ceff5tj")
+ assert not embedded("3P3Ceff5tj")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = base58.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/base64.test.py b/tests/atoms/base64.test.py
new file mode 100644
index 0000000..03ada86
--- /dev/null
+++ b/tests/atoms/base64.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import base64
+
+
+def _anchored():
+ return Pattern().start_of_input().use(base64).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("SGVsbG8=")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("!!")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(base64).end_of_input()
+ assert embedded("v=" + "SGVsbG8=")
+ assert not embedded("SGVsbG8=")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = base64.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/base64url.test.py b/tests/atoms/base64url.test.py
new file mode 100644
index 0000000..51a7bf3
--- /dev/null
+++ b/tests/atoms/base64url.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import base64url
+
+
+def _anchored():
+ return Pattern().start_of_input().use(base64url).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("Hello_World-")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("!")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(base64url).end_of_input()
+ assert embedded("v=" + "Hello_World-")
+ assert not embedded("Hello_World-")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = base64url.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/bic.test.py b/tests/atoms/bic.test.py
new file mode 100644
index 0000000..ba92236
--- /dev/null
+++ b/tests/atoms/bic.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import bic
+
+
+def _anchored():
+ return Pattern().start_of_input().use(bic).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("DEUTDEFF500")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("bad")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(bic).end_of_input()
+ assert embedded("v=" + "DEUTDEFF500")
+ assert not embedded("DEUTDEFF500")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = bic.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/binnum.test.py b/tests/atoms/binnum.test.py
new file mode 100644
index 0000000..4018823
--- /dev/null
+++ b/tests/atoms/binnum.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import binnum
+
+
+def _anchored():
+ return Pattern().start_of_input().use(binnum).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("0b1010")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("1010")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(binnum).end_of_input()
+ assert embedded("v=" + "0b1010")
+ assert not embedded("0b1010")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = binnum.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/boolean.test.py b/tests/atoms/boolean.test.py
new file mode 100644
index 0000000..9b629c6
--- /dev/null
+++ b/tests/atoms/boolean.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import boolean
+
+
+def _anchored():
+ return Pattern().start_of_input().use(boolean).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("on")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("maybe")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(boolean).end_of_input()
+ assert embedded("v=" + "on")
+ assert not embedded("on")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = boolean.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/braces.test.py b/tests/atoms/braces.test.py
new file mode 100644
index 0000000..85cda16
--- /dev/null
+++ b/tests/atoms/braces.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import braces
+
+
+def _anchored():
+ return Pattern().start_of_input().use(braces).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("{hello}")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("hello")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(braces).end_of_input()
+ assert embedded("v=" + "{hello}")
+ assert not embedded("{hello}")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = braces.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/brackets.test.py b/tests/atoms/brackets.test.py
new file mode 100644
index 0000000..0bdf41c
--- /dev/null
+++ b/tests/atoms/brackets.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import brackets
+
+
+def _anchored():
+ return Pattern().start_of_input().use(brackets).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("[hello]")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("hello")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(brackets).end_of_input()
+ assert embedded("v=" + "[hello]")
+ assert not embedded("[hello]")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = brackets.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/cidr.test.py b/tests/atoms/cidr.test.py
new file mode 100644
index 0000000..a7dc9c9
--- /dev/null
+++ b/tests/atoms/cidr.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import cidr
+
+
+def _anchored():
+ return Pattern().start_of_input().use(cidr).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("10.0.0.0/8")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("10.0.0.0")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(cidr).end_of_input()
+ assert embedded("v=" + "10.0.0.0/8")
+ assert not embedded("10.0.0.0/8")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = cidr.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/clock12.test.py b/tests/atoms/clock12.test.py
new file mode 100644
index 0000000..528eae5
--- /dev/null
+++ b/tests/atoms/clock12.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import clock12
+
+
+def _anchored():
+ return Pattern().start_of_input().use(clock12).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("12:00 PM")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("13:00 PM")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(clock12).end_of_input()
+ assert embedded("v=" + "12:00 PM")
+ assert not embedded("12:00 PM")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = clock12.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/creditcard.test.py b/tests/atoms/creditcard.test.py
new file mode 100644
index 0000000..a436d2c
--- /dev/null
+++ b/tests/atoms/creditcard.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import creditcard
+
+
+def _anchored():
+ return Pattern().start_of_input().use(creditcard).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("4111111111111111")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("abc")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(creditcard).end_of_input()
+ assert embedded("v=" + "4111111111111111")
+ assert not embedded("4111111111111111")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = creditcard.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/currency.test.py b/tests/atoms/currency.test.py
new file mode 100644
index 0000000..21fa483
--- /dev/null
+++ b/tests/atoms/currency.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import currency
+
+
+def _anchored():
+ return Pattern().start_of_input().use(currency).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("USD")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("us")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(currency).end_of_input()
+ assert embedded("v=" + "USD")
+ assert not embedded("USD")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = currency.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/day.test.py b/tests/atoms/day.test.py
new file mode 100644
index 0000000..c751b94
--- /dev/null
+++ b/tests/atoms/day.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import day
+
+
+def _anchored():
+ return Pattern().start_of_input().use(day).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("15")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("32")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(day).end_of_input()
+ assert embedded("v=" + "15")
+ assert not embedded("15")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = day.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/decimal.test.py b/tests/atoms/decimal.test.py
new file mode 100644
index 0000000..13140c6
--- /dev/null
+++ b/tests/atoms/decimal.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import decimal
+
+
+def _anchored():
+ return Pattern().start_of_input().use(decimal).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("3.14")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("3")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(decimal).end_of_input()
+ assert embedded("v=" + "3.14")
+ assert not embedded("3.14")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = decimal.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/duration.test.py b/tests/atoms/duration.test.py
new file mode 100644
index 0000000..5f362c9
--- /dev/null
+++ b/tests/atoms/duration.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import duration
+
+
+def _anchored():
+ return Pattern().start_of_input().use(duration).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("P1Y")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("P")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(duration).end_of_input()
+ assert embedded("v=" + "P1Y")
+ assert not embedded("P1Y")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = duration.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/email.test.py b/tests/atoms/email.test.py
new file mode 100644
index 0000000..24fa895
--- /dev/null
+++ b/tests/atoms/email.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import email
+
+
+def _anchored():
+ return Pattern().start_of_input().use(email).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("[email protected]")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("not email")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(email).end_of_input()
+ assert embedded("v=" + "[email protected]")
+ assert not embedded("[email protected]")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = email.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/epoch.test.py b/tests/atoms/epoch.test.py
new file mode 100644
index 0000000..8be01e6
--- /dev/null
+++ b/tests/atoms/epoch.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import epoch
+
+
+def _anchored():
+ return Pattern().start_of_input().use(epoch).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("1704067200")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("12")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(epoch).end_of_input()
+ assert embedded("v=" + "1704067200")
+ assert not embedded("1704067200")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = epoch.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/extension.test.py b/tests/atoms/extension.test.py
new file mode 100644
index 0000000..5374e60
--- /dev/null
+++ b/tests/atoms/extension.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import extension
+
+
+def _anchored():
+ return Pattern().start_of_input().use(extension).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()(".txt")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("txt")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(extension).end_of_input()
+ assert embedded("v=" + ".txt")
+ assert not embedded(".txt")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = extension.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/filename.test.py b/tests/atoms/filename.test.py
new file mode 100644
index 0000000..c33840b
--- /dev/null
+++ b/tests/atoms/filename.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import filename
+
+
+def _anchored():
+ return Pattern().start_of_input().use(filename).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("myfile.txt")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("bad/path")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(filename).end_of_input()
+ assert embedded("v=" + "myfile.txt")
+ assert not embedded("myfile.txt")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = filename.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/filepath.test.py b/tests/atoms/filepath.test.py
new file mode 100644
index 0000000..4ea3552
--- /dev/null
+++ b/tests/atoms/filepath.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import filepath
+
+
+def _anchored():
+ return Pattern().start_of_input().use(filepath).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("/home/user")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(filepath).end_of_input()
+ assert embedded("v=" + "/home/user")
+ assert not embedded("/home/user")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = filepath.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/floatnum.test.py b/tests/atoms/floatnum.test.py
new file mode 100644
index 0000000..13bd114
--- /dev/null
+++ b/tests/atoms/floatnum.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import floatnum
+
+
+def _anchored():
+ return Pattern().start_of_input().use(floatnum).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("1.5e10")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("abc")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(floatnum).end_of_input()
+ assert embedded("v=" + "1.5e10")
+ assert not embedded("1.5e10")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = floatnum.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/guid.test.py b/tests/atoms/guid.test.py
new file mode 100644
index 0000000..45d2446
--- /dev/null
+++ b/tests/atoms/guid.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import guid
+
+
+def _anchored():
+ return Pattern().start_of_input().use(guid).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("550e8400-e29b-11d4-a716-446655440000")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("bad")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(guid).end_of_input()
+ assert embedded("v=" + "550e8400-e29b-11d4-a716-446655440000")
+ assert not embedded("550e8400-e29b-11d4-a716-446655440000")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = guid.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/hexnum.test.py b/tests/atoms/hexnum.test.py
new file mode 100644
index 0000000..20c8d16
--- /dev/null
+++ b/tests/atoms/hexnum.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import hexnum
+
+
+def _anchored():
+ return Pattern().start_of_input().use(hexnum).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("0xFF")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("FF")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(hexnum).end_of_input()
+ assert embedded("v=" + "0xFF")
+ assert not embedded("0xFF")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = hexnum.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/hexstring.test.py b/tests/atoms/hexstring.test.py
new file mode 100644
index 0000000..c1a53b4
--- /dev/null
+++ b/tests/atoms/hexstring.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import hexstring
+
+
+def _anchored():
+ return Pattern().start_of_input().use(hexstring).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("deadbeef")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("z")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(hexstring).end_of_input()
+ assert embedded("v=" + "deadbeef")
+ assert not embedded("deadbeef")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = hexstring.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/httpmethod.test.py b/tests/atoms/httpmethod.test.py
new file mode 100644
index 0000000..c77c973
--- /dev/null
+++ b/tests/atoms/httpmethod.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import httpmethod
+
+
+def _anchored():
+ return Pattern().start_of_input().use(httpmethod).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("POST")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("FOO")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(httpmethod).end_of_input()
+ assert embedded("v=" + "POST")
+ assert not embedded("POST")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = httpmethod.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/httpstatus.test.py b/tests/atoms/httpstatus.test.py
new file mode 100644
index 0000000..d26ac40
--- /dev/null
+++ b/tests/atoms/httpstatus.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import httpstatus
+
+
+def _anchored():
+ return Pattern().start_of_input().use(httpstatus).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("200")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("600")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(httpstatus).end_of_input()
+ assert embedded("v=" + "200")
+ assert not embedded("200")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = httpstatus.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/iban.test.py b/tests/atoms/iban.test.py
new file mode 100644
index 0000000..3966f5f
--- /dev/null
+++ b/tests/atoms/iban.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import iban
+
+
+def _anchored():
+ return Pattern().start_of_input().use(iban).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("GB33BUKB20201555555555")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("bad")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(iban).end_of_input()
+ assert embedded("v=" + "GB33BUKB20201555555555")
+ assert not embedded("GB33BUKB20201555555555")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = iban.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/integer.test.py b/tests/atoms/integer.test.py
new file mode 100644
index 0000000..d5e9fa8
--- /dev/null
+++ b/tests/atoms/integer.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import integer
+
+
+def _anchored():
+ return Pattern().start_of_input().use(integer).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("-42")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("abc")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(integer).end_of_input()
+ assert embedded("v=" + "-42")
+ assert not embedded("-42")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = integer.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/ipv4.test.py b/tests/atoms/ipv4.test.py
new file mode 100644
index 0000000..f55033d
--- /dev/null
+++ b/tests/atoms/ipv4.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import ipv4
+
+
+def _anchored():
+ return Pattern().start_of_input().use(ipv4).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("192.168.1.1")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("999.0.0.0")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(ipv4).end_of_input()
+ assert embedded("v=" + "192.168.1.1")
+ assert not embedded("192.168.1.1")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = ipv4.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/ipv6.test.py b/tests/atoms/ipv6.test.py
new file mode 100644
index 0000000..300bb09
--- /dev/null
+++ b/tests/atoms/ipv6.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import ipv6
+
+
+def _anchored():
+ return Pattern().start_of_input().use(ipv6).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("2001:db8:0:0:0:0:0:1")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("not_ipv6")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(ipv6).end_of_input()
+ assert embedded("v=" + "2001:db8:0:0:0:0:0:1")
+ assert not embedded("2001:db8:0:0:0:0:0:1")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = ipv6.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/isodatetime.test.py b/tests/atoms/isodatetime.test.py
new file mode 100644
index 0000000..78aa769
--- /dev/null
+++ b/tests/atoms/isodatetime.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import isodatetime
+
+
+def _anchored():
+ return Pattern().start_of_input().use(isodatetime).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("2024-01-15T12:00:00Z")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("bad")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(isodatetime).end_of_input()
+ assert embedded("v=" + "2024-01-15T12:00:00Z")
+ assert not embedded("2024-01-15T12:00:00Z")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = isodatetime.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/letter.test.py b/tests/atoms/letter.test.py
new file mode 100644
index 0000000..481032d
--- /dev/null
+++ b/tests/atoms/letter.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import letter
+
+
+def _anchored():
+ return Pattern().start_of_input().use(letter).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("a")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("1")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(letter).end_of_input()
+ assert embedded("v=" + "a")
+ assert not embedded("a")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = letter.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/line.test.py b/tests/atoms/line.test.py
new file mode 100644
index 0000000..a0f7b09
--- /dev/null
+++ b/tests/atoms/line.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import line
+
+
+def _anchored():
+ return Pattern().start_of_input().use(line).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("hello world")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("\n")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(line).end_of_input()
+ assert embedded("v=" + "hello world")
+ assert not embedded("hello world")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = line.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/lower.test.py b/tests/atoms/lower.test.py
new file mode 100644
index 0000000..0b947a8
--- /dev/null
+++ b/tests/atoms/lower.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import lower
+
+
+def _anchored():
+ return Pattern().start_of_input().use(lower).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("a")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("A")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(lower).end_of_input()
+ assert embedded("v=" + "a")
+ assert not embedded("a")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = lower.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/mac.test.py b/tests/atoms/mac.test.py
new file mode 100644
index 0000000..21a1084
--- /dev/null
+++ b/tests/atoms/mac.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import mac
+
+
+def _anchored():
+ return Pattern().start_of_input().use(mac).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("aa:bb:cc:dd:ee:ff")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("not-a-mac")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(mac).end_of_input()
+ assert embedded("v=" + "aa:bb:cc:dd:ee:ff")
+ assert not embedded("aa:bb:cc:dd:ee:ff")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = mac.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/md5.test.py b/tests/atoms/md5.test.py
new file mode 100644
index 0000000..4dd99e3
--- /dev/null
+++ b/tests/atoms/md5.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import md5
+
+
+def _anchored():
+ return Pattern().start_of_input().use(md5).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("short")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(md5).end_of_input()
+ assert embedded("v=" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+ assert not embedded("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = md5.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/mimetype.test.py b/tests/atoms/mimetype.test.py
new file mode 100644
index 0000000..267d3eb
--- /dev/null
+++ b/tests/atoms/mimetype.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import mimetype
+
+
+def _anchored():
+ return Pattern().start_of_input().use(mimetype).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("text/plain")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("text")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(mimetype).end_of_input()
+ assert embedded("v=" + "text/plain")
+ assert not embedded("text/plain")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = mimetype.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/money.test.py b/tests/atoms/money.test.py
new file mode 100644
index 0000000..edaebee
--- /dev/null
+++ b/tests/atoms/money.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import money
+
+
+def _anchored():
+ return Pattern().start_of_input().use(money).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("USD 100.50")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("bad")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(money).end_of_input()
+ assert embedded("v=" + "USD 100.50")
+ assert not embedded("USD 100.50")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = money.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/month.test.py b/tests/atoms/month.test.py
new file mode 100644
index 0000000..a1dcd99
--- /dev/null
+++ b/tests/atoms/month.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import month
+
+
+def _anchored():
+ return Pattern().start_of_input().use(month).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("07")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("13")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(month).end_of_input()
+ assert embedded("v=" + "07")
+ assert not embedded("07")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = month.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/natural.test.py b/tests/atoms/natural.test.py
new file mode 100644
index 0000000..d5f61d2
--- /dev/null
+++ b/tests/atoms/natural.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import natural
+
+
+def _anchored():
+ return Pattern().start_of_input().use(natural).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("42")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("07")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(natural).end_of_input()
+ assert embedded("v=" + "42")
+ assert not embedded("42")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = natural.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/objectid.test.py b/tests/atoms/objectid.test.py
new file mode 100644
index 0000000..b8ce687
--- /dev/null
+++ b/tests/atoms/objectid.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import objectid
+
+
+def _anchored():
+ return Pattern().start_of_input().use(objectid).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("507f1f77bcf86cd799439011")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("short")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(objectid).end_of_input()
+ assert embedded("v=" + "507f1f77bcf86cd799439011")
+ assert not embedded("507f1f77bcf86cd799439011")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = objectid.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/octnum.test.py b/tests/atoms/octnum.test.py
new file mode 100644
index 0000000..a7a0106
--- /dev/null
+++ b/tests/atoms/octnum.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import octnum
+
+
+def _anchored():
+ return Pattern().start_of_input().use(octnum).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("0o17")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("17")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(octnum).end_of_input()
+ assert embedded("v=" + "0o17")
+ assert not embedded("0o17")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = octnum.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/oid.test.py b/tests/atoms/oid.test.py
new file mode 100644
index 0000000..0c8182a
--- /dev/null
+++ b/tests/atoms/oid.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import oid
+
+
+def _anchored():
+ return Pattern().start_of_input().use(oid).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("1.3.6.1.4.1")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("1")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(oid).end_of_input()
+ assert embedded("v=" + "1.3.6.1.4.1")
+ assert not embedded("1.3.6.1.4.1")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = oid.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/parens.test.py b/tests/atoms/parens.test.py
new file mode 100644
index 0000000..e9d07a3
--- /dev/null
+++ b/tests/atoms/parens.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import parens
+
+
+def _anchored():
+ return Pattern().start_of_input().use(parens).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("(hello)")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("hello")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(parens).end_of_input()
+ assert embedded("v=" + "(hello)")
+ assert not embedded("(hello)")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = parens.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/percent.test.py b/tests/atoms/percent.test.py
new file mode 100644
index 0000000..40ad6c9
--- /dev/null
+++ b/tests/atoms/percent.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import percent
+
+
+def _anchored():
+ return Pattern().start_of_input().use(percent).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("50%")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("50")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(percent).end_of_input()
+ assert embedded("v=" + "50%")
+ assert not embedded("50%")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = percent.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/printable.test.py b/tests/atoms/printable.test.py
new file mode 100644
index 0000000..3e6a9be
--- /dev/null
+++ b/tests/atoms/printable.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import printable
+
+
+def _anchored():
+ return Pattern().start_of_input().use(printable).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("!")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("\x00")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(printable).end_of_input()
+ assert embedded("v=" + "!")
+ assert not embedded("!")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = printable.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/protocol.test.py b/tests/atoms/protocol.test.py
new file mode 100644
index 0000000..9e546e0
--- /dev/null
+++ b/tests/atoms/protocol.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import protocol
+
+
+def _anchored():
+ return Pattern().start_of_input().use(protocol).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("https")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("smtp")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(protocol).end_of_input()
+ assert embedded("v=" + "https")
+ assert not embedded("https")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = protocol.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/quoted.test.py b/tests/atoms/quoted.test.py
new file mode 100644
index 0000000..e0d409a
--- /dev/null
+++ b/tests/atoms/quoted.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import quoted
+
+
+def _anchored():
+ return Pattern().start_of_input().use(quoted).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()('"hello"')
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("hello")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(quoted).end_of_input()
+ assert embedded("v=" + '"hello"')
+ assert not embedded('"hello"')
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = quoted.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/ratio.test.py b/tests/atoms/ratio.test.py
new file mode 100644
index 0000000..c3ad541
--- /dev/null
+++ b/tests/atoms/ratio.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import ratio
+
+
+def _anchored():
+ return Pattern().start_of_input().use(ratio).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("16:9")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("16")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(ratio).end_of_input()
+ assert embedded("v=" + "16:9")
+ assert not embedded("16:9")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = ratio.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/rgbcolor.test.py b/tests/atoms/rgbcolor.test.py
new file mode 100644
index 0000000..1d0cd23
--- /dev/null
+++ b/tests/atoms/rgbcolor.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import rgbcolor
+
+
+def _anchored():
+ return Pattern().start_of_input().use(rgbcolor).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("rgb(255, 0, 0)")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("nope")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(rgbcolor).end_of_input()
+ assert embedded("v=" + "rgb(255, 0, 0)")
+ assert not embedded("rgb(255, 0, 0)")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = rgbcolor.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/scheme.test.py b/tests/atoms/scheme.test.py
new file mode 100644
index 0000000..1b77a63
--- /dev/null
+++ b/tests/atoms/scheme.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import scheme
+
+
+def _anchored():
+ return Pattern().start_of_input().use(scheme).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("https")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("1abc")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(scheme).end_of_input()
+ assert embedded("v=" + "https")
+ assert not embedded("https")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = scheme.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/scientific.test.py b/tests/atoms/scientific.test.py
new file mode 100644
index 0000000..6ccabc4
--- /dev/null
+++ b/tests/atoms/scientific.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import scientific
+
+
+def _anchored():
+ return Pattern().start_of_input().use(scientific).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("1e10")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("1.5")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(scientific).end_of_input()
+ assert embedded("v=" + "1e10")
+ assert not embedded("1e10")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = scientific.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/sha256.test.py b/tests/atoms/sha256.test.py
new file mode 100644
index 0000000..790f62f
--- /dev/null
+++ b/tests/atoms/sha256.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import sha256
+
+
+def _anchored():
+ return Pattern().start_of_input().use(sha256).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("short")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(sha256).end_of_input()
+ assert embedded("v=" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+ assert not embedded("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = sha256.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/signed.test.py b/tests/atoms/signed.test.py
new file mode 100644
index 0000000..ee4521c
--- /dev/null
+++ b/tests/atoms/signed.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import signed
+
+
+def _anchored():
+ return Pattern().start_of_input().use(signed).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("+42")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("42")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(signed).end_of_input()
+ assert embedded("v=" + "+42")
+ assert not embedded("+42")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = signed.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/space.test.py b/tests/atoms/space.test.py
new file mode 100644
index 0000000..1651bb9
--- /dev/null
+++ b/tests/atoms/space.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import space
+
+
+def _anchored():
+ return Pattern().start_of_input().use(space).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()(" ")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("a")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(space).end_of_input()
+ assert embedded("v=" + " ")
+ assert not embedded(" ")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = space.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/timezone.test.py b/tests/atoms/timezone.test.py
new file mode 100644
index 0000000..d1d4992
--- /dev/null
+++ b/tests/atoms/timezone.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import timezone
+
+
+def _anchored():
+ return Pattern().start_of_input().use(timezone).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("+05:30")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("bad")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(timezone).end_of_input()
+ assert embedded("v=" + "+05:30")
+ assert not embedded("+05:30")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = timezone.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/tld.test.py b/tests/atoms/tld.test.py
new file mode 100644
index 0000000..f61c4a8
--- /dev/null
+++ b/tests/atoms/tld.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import tld
+
+
+def _anchored():
+ return Pattern().start_of_input().use(tld).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("com")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("a")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(tld).end_of_input()
+ assert embedded("v=" + "com")
+ assert not embedded("com")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = tld.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/truefalse.test.py b/tests/atoms/truefalse.test.py
new file mode 100644
index 0000000..817bd3d
--- /dev/null
+++ b/tests/atoms/truefalse.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import truefalse
+
+
+def _anchored():
+ return Pattern().start_of_input().use(truefalse).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("true")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("maybe")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(truefalse).end_of_input()
+ assert embedded("v=" + "true")
+ assert not embedded("true")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = truefalse.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/ulid.test.py b/tests/atoms/ulid.test.py
new file mode 100644
index 0000000..94ce256
--- /dev/null
+++ b/tests/atoms/ulid.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import ulid
+
+
+def _anchored():
+ return Pattern().start_of_input().use(ulid).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("01ARZ3NDEKTSV4RRFFQ69G5FAV")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("short")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(ulid).end_of_input()
+ assert embedded("v=" + "01ARZ3NDEKTSV4RRFFQ69G5FAV")
+ assert not embedded("01ARZ3NDEKTSV4RRFFQ69G5FAV")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = ulid.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/unsigned.test.py b/tests/atoms/unsigned.test.py
new file mode 100644
index 0000000..28ba4a1
--- /dev/null
+++ b/tests/atoms/unsigned.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import unsigned
+
+
+def _anchored():
+ return Pattern().start_of_input().use(unsigned).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("42")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("-42")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(unsigned).end_of_input()
+ assert embedded("v=" + "42")
+ assert not embedded("42")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = unsigned.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/upper.test.py b/tests/atoms/upper.test.py
new file mode 100644
index 0000000..2817c0c
--- /dev/null
+++ b/tests/atoms/upper.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import upper
+
+
+def _anchored():
+ return Pattern().start_of_input().use(upper).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("A")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("a")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(upper).end_of_input()
+ assert embedded("v=" + "A")
+ assert not embedded("A")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = upper.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/uri.test.py b/tests/atoms/uri.test.py
new file mode 100644
index 0000000..5034d04
--- /dev/null
+++ b/tests/atoms/uri.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import uri
+
+
+def _anchored():
+ return Pattern().start_of_input().use(uri).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("mailto:[email protected]")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("just")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(uri).end_of_input()
+ assert embedded("v=" + "mailto:[email protected]")
+ assert not embedded("mailto:[email protected]")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = uri.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/url.test.py b/tests/atoms/url.test.py
new file mode 100644
index 0000000..73d52a9
--- /dev/null
+++ b/tests/atoms/url.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import url
+
+
+def _anchored():
+ return Pattern().start_of_input().use(url).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("https://example.com")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("not a url")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(url).end_of_input()
+ assert embedded("v=" + "https://example.com")
+ assert not embedded("https://example.com")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = url.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/weekday.test.py b/tests/atoms/weekday.test.py
new file mode 100644
index 0000000..c6f240e
--- /dev/null
+++ b/tests/atoms/weekday.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import weekday
+
+
+def _anchored():
+ return Pattern().start_of_input().use(weekday).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("Mon")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("Xyz")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(weekday).end_of_input()
+ assert embedded("v=" + "Mon")
+ assert not embedded("Mon")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = weekday.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/word.test.py b/tests/atoms/word.test.py
new file mode 100644
index 0000000..c73086e
--- /dev/null
+++ b/tests/atoms/word.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import word
+
+
+def _anchored():
+ return Pattern().start_of_input().use(word).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("hello_1")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("has space")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(word).end_of_input()
+ assert embedded("v=" + "hello_1")
+ assert not embedded("hello_1")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = word.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/year.test.py b/tests/atoms/year.test.py
new file mode 100644
index 0000000..cb34916
--- /dev/null
+++ b/tests/atoms/year.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import year
+
+
+def _anchored():
+ return Pattern().start_of_input().use(year).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("2024")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("24")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(year).end_of_input()
+ assert embedded("v=" + "2024")
+ assert not embedded("2024")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = year.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)
diff --git a/tests/atoms/yesno.test.py b/tests/atoms/yesno.test.py
new file mode 100644
index 0000000..775c1bb
--- /dev/null
+++ b/tests/atoms/yesno.test.py
@@ -0,0 +1,26 @@
+from edify import Pattern
+from edify.atoms import yesno
+
+
+def _anchored():
+ return Pattern().start_of_input().use(yesno).end_of_input()
+
+
+def test_accepts_sample_from_shape():
+ assert _anchored()("yes")
+
+
+def test_rejects_off_shape_input():
+ assert not _anchored()("maybe")
+
+
+def test_atom_composes_inside_a_larger_pattern():
+ embedded = Pattern().start_of_input().string("v=").use(yesno).end_of_input()
+ assert embedded("v=" + "yes")
+ assert not embedded("yes")
+
+
+def test_atom_regex_string_is_non_empty():
+ fragment = yesno.to_regex_string()
+ assert fragment
+ assert isinstance(fragment, str)