diff options
171 files changed, 3626 insertions, 38 deletions
diff --git a/edify/atoms/__init__.py b/edify/atoms/__init__.py new file mode 100644 index 0000000..066a56e --- /dev/null +++ b/edify/atoms/__init__.py @@ -0,0 +1,176 @@ +"""Composable :class:`Pattern` atoms. + +Each atom is an unanchored fragment meant to be spliced into a builder via +:meth:`edify.RegexBuilder.use` / :meth:`edify.Pattern.use`. Import any atom +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/clock.py b/edify/atoms/clock.py new file mode 100644 index 0000000..9ad64d3 --- /dev/null +++ b/edify/atoms/clock.py @@ -0,0 +1,23 @@ +"""``clock`` — 24-hour ``HH:MM[:SS]`` clock time.""" + +from __future__ import annotations + +from edify import Pattern + +clock = ( + Pattern() + .any_of() + .subexpression(Pattern().char("2").range("0", "3")) + .subexpression(Pattern().range("0", "1").digit()) + .end() + .char(":") + .range("0", "5") + .digit() + .optional() + .group() + .char(":") + .range("0", "5") + .digit() + .end() +) +"""Composable :class:`Pattern` fragment for a 24-hour clock time.""" 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/hexcolor.py b/edify/atoms/hexcolor.py new file mode 100644 index 0000000..cdb162b --- /dev/null +++ b/edify/atoms/hexcolor.py @@ -0,0 +1,22 @@ +"""``hexcolor`` — CSS hex-colour shape (``#RGB``, ``#RGBA``, ``#RRGGBB``, ``#RRGGBBAA``).""" + +from __future__ import annotations + +from edify import Pattern +from edify.atoms.nibble import nibble + +hexcolor = ( + Pattern() + .char("#") + .any_of() + .exactly(8) + .use(nibble) + .exactly(6) + .use(nibble) + .exactly(4) + .use(nibble) + .exactly(3) + .use(nibble) + .end() +) +"""Composable :class:`Pattern` fragment for a CSS hex colour.""" 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/hostname.py b/edify/atoms/hostname.py new file mode 100644 index 0000000..14feb0f --- /dev/null +++ b/edify/atoms/hostname.py @@ -0,0 +1,9 @@ +"""``hostname`` — RFC 1123 hostname (dot-separated labels).""" + +from __future__ import annotations + +from edify import Pattern +from edify.atoms.label import label + +hostname = Pattern().use(label).zero_or_more().group().char(".").use(label).end() +"""Composable :class:`Pattern` fragment for an RFC 1123 hostname.""" 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/isodate.py b/edify/atoms/isodate.py new file mode 100644 index 0000000..7408bba --- /dev/null +++ b/edify/atoms/isodate.py @@ -0,0 +1,8 @@ +"""``isodate`` — ISO 8601 calendar date ``YYYY-MM-DD``.""" + +from __future__ import annotations + +from edify import Pattern + +isodate = Pattern().exactly(4).digit().char("-").exactly(2).digit().char("-").exactly(2).digit() +"""Composable :class:`Pattern` fragment for an ISO 8601 ``YYYY-MM-DD`` date.""" 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/label.py b/edify/atoms/label.py new file mode 100644 index 0000000..bad0ae4 --- /dev/null +++ b/edify/atoms/label.py @@ -0,0 +1,32 @@ +"""``label`` — one RFC 1123 DNS label.""" + +from __future__ import annotations + +from edify import Pattern + +label = ( + Pattern() + .any_of() + .range("a", "z") + .range("A", "Z") + .range("0", "9") + .end() + .optional() + .group() + .between(0, 61) + .any_of() + .range("a", "z") + .range("A", "Z") + .range("0", "9") + .char("-") + .end() + .any_of() + .range("a", "z") + .range("A", "Z") + .range("0", "9") + .end() + .end() +) +"""Composable :class:`Pattern` fragment for one RFC 1123 DNS label +(letter/digit start, up to 63 chars, letter/digit end). +""" 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/localpart.py b/edify/atoms/localpart.py new file mode 100644 index 0000000..92f9682 --- /dev/null +++ b/edify/atoms/localpart.py @@ -0,0 +1,36 @@ +"""``localpart`` — RFC 5322 email local-part shape (permissive).""" + +from __future__ import annotations + +from edify import Pattern + +localpart = ( + Pattern() + .between(1, 64) + .any_of() + .range("a", "z") + .range("A", "Z") + .range("0", "9") + .char("!") + .char("#") + .char("$") + .char("%") + .char("&") + .char("'") + .char("*") + .char("+") + .char("/") + .char("=") + .char("?") + .char("^") + .char("_") + .char("`") + .char("{") + .char("|") + .char("}") + .char("~") + .char(".") + .char("-") + .end() +) +"""Composable :class:`Pattern` fragment for the local part of an email address.""" 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/nibble.py b/edify/atoms/nibble.py new file mode 100644 index 0000000..7e7cd9e --- /dev/null +++ b/edify/atoms/nibble.py @@ -0,0 +1,8 @@ +"""``nibble`` — one hexadecimal nibble ``[0-9a-fA-F]``.""" + +from __future__ import annotations + +from edify import Pattern + +nibble = Pattern().any_of().range("0", "9").range("a", "f").range("A", "F").end() +"""Composable :class:`Pattern` fragment for one mixed-case hex nibble.""" 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/octet.py b/edify/atoms/octet.py new file mode 100644 index 0000000..5911b47 --- /dev/null +++ b/edify/atoms/octet.py @@ -0,0 +1,14 @@ +"""``octet`` — one IPv4 octet in the range ``0``-``255``.""" + +from __future__ import annotations + +from edify import Pattern, any_of + +octet = any_of( + Pattern().string("25").range("0", "5"), + Pattern().char("2").range("0", "4").digit(), + Pattern().char("1").digit().digit(), + Pattern().range("1", "9").digit(), + Pattern().digit(), +) +"""Composable :class:`Pattern` fragment for a single IPv4 octet (``0``-``255``).""" 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/port.py b/edify/atoms/port.py new file mode 100644 index 0000000..1a52024 --- /dev/null +++ b/edify/atoms/port.py @@ -0,0 +1,16 @@ +"""``port`` — one TCP/UDP port in the range ``0``-``65535``.""" + +from __future__ import annotations + +from edify import Pattern, any_of + +port = any_of( + Pattern().string("6553").range("0", "5"), + Pattern().string("655").range("0", "2").digit(), + Pattern().string("65").range("0", "4").digit().digit(), + Pattern().char("6").range("0", "4").digit().digit().digit(), + Pattern().range("1", "5").exactly(4).digit(), + Pattern().range("1", "9").between(0, 3).digit(), + Pattern().char("0"), +) +"""Composable :class:`Pattern` fragment for a TCP/UDP port number.""" 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/semver.py b/edify/atoms/semver.py new file mode 100644 index 0000000..23ad54f --- /dev/null +++ b/edify/atoms/semver.py @@ -0,0 +1,16 @@ +"""``semver`` — ``MAJOR.MINOR.PATCH`` SemVer core.""" + +from __future__ import annotations + +from edify import Pattern, any_of + + +def _core() -> Pattern: + return any_of( + Pattern().char("0"), + Pattern().range("1", "9").zero_or_more().digit(), + ) + + +semver = Pattern().use(_core()).char(".").use(_core()).char(".").use(_core()) +"""Composable :class:`Pattern` fragment for a SemVer ``MAJOR.MINOR.PATCH`` core.""" 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/slug.py b/edify/atoms/slug.py new file mode 100644 index 0000000..5faee1b --- /dev/null +++ b/edify/atoms/slug.py @@ -0,0 +1,24 @@ +"""``slug`` — URL-safe lowercase hyphenated slug.""" + +from __future__ import annotations + +from edify import Pattern + +slug = ( + Pattern() + .one_or_more() + .any_of() + .range("a", "z") + .range("0", "9") + .end() + .zero_or_more() + .group() + .char("-") + .one_or_more() + .any_of() + .range("a", "z") + .range("0", "9") + .end() + .end() +) +"""Composable :class:`Pattern` fragment for a URL-safe slug.""" 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/username.py b/edify/atoms/username.py new file mode 100644 index 0000000..d75d101 --- /dev/null +++ b/edify/atoms/username.py @@ -0,0 +1,19 @@ +"""``username`` — permissive social-handle shape (3-30 chars).""" + +from __future__ import annotations + +from edify import Pattern + +username = ( + Pattern() + .between(3, 30) + .any_of() + .range("a", "z") + .range("A", "Z") + .range("0", "9") + .char(".") + .char("_") + .char("-") + .end() +) +"""Composable :class:`Pattern` fragment for a social-handle username.""" diff --git a/edify/atoms/uuid.py b/edify/atoms/uuid.py new file mode 100644 index 0000000..cb5fe4f --- /dev/null +++ b/edify/atoms/uuid.py @@ -0,0 +1,27 @@ +"""``uuid`` — version-4 UUID shape.""" + +from __future__ import annotations + +from edify import Pattern +from edify.atoms.nibble import nibble + +uuid = ( + Pattern() + .exactly(8) + .use(nibble) + .char("-") + .exactly(4) + .use(nibble) + .char("-") + .char("4") + .exactly(3) + .use(nibble) + .char("-") + .any_of_chars("89ab") + .exactly(3) + .use(nibble) + .char("-") + .exactly(12) + .use(nibble) +) +"""Composable :class:`Pattern` fragment for a version-4 UUID.""" 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/edify/library/_support/atoms.py b/edify/library/_support/atoms.py deleted file mode 100644 index 922e4f9..0000000 --- a/edify/library/_support/atoms.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Private composable atoms shared by the library validators. - -Every atom is a :class:`Pattern` fragment that renders exactly what the -comment beside it says; nothing here is exported publicly, but every entry -is imported freely by the exported validators to keep their fluent chains -readable. -""" - -from __future__ import annotations - -from edify import Pattern, any_of - -hex_lower = Pattern().any_of().range("0", "9").range("a", "f").end() -"""A single lowercase hex nibble: ``[0-9a-f]``.""" - -hex_upper = Pattern().any_of().range("0", "9").range("A", "F").end() -"""A single uppercase hex nibble: ``[0-9A-F]``.""" - -hex_any = Pattern().any_of().range("0", "9").range("a", "f").range("A", "F").end() -"""A single mixed-case hex nibble: ``[0-9a-fA-F]``.""" - -digit_atom = Pattern().digit() -"""A single decimal digit: ``\\d``.""" - -octet = any_of( - Pattern().string("25").range("0", "5"), - Pattern().char("2").range("0", "4").digit(), - Pattern().char("1").digit().digit(), - Pattern().range("1", "9").digit(), - Pattern().digit(), -) -"""An IPv4 octet in ``0``-``255``.""" diff --git a/edify/library/address/ip.py b/edify/library/address/ip.py index d085164..85945d7 100644 --- a/edify/library/address/ip.py +++ b/edify/library/address/ip.py @@ -3,11 +3,12 @@ from __future__ import annotations from edify import Pattern, any_of -from edify.library._support.atoms import hex_any, octet +from edify.atoms.nibble import nibble +from edify.atoms.octet import octet def _hex_group() -> Pattern: - return Pattern().between(1, 4).subexpression(hex_any) + return Pattern().between(1, 4).subexpression(nibble) _ipv4 = Pattern().subexpression(octet).exactly(3).group().char(".").subexpression(octet).end() @@ -94,7 +95,7 @@ def _b_link_local() -> Pattern: .group() .char(":") .between(0, 4) - .subexpression(hex_any) + .subexpression(nibble) .end() .char("%") .one_or_more() diff --git a/edify/library/address/ptr.py b/edify/library/address/ptr.py index 14d70ed..87382f1 100644 --- a/edify/library/address/ptr.py +++ b/edify/library/address/ptr.py @@ -3,7 +3,7 @@ from __future__ import annotations from edify import Pattern, any_of -from edify.library._support.atoms import hex_any +from edify.atoms.nibble import nibble _ipv4_ptr = ( Pattern() @@ -23,7 +23,7 @@ _ipv6_ptr = ( Pattern() .exactly(32) .group() - .subexpression(hex_any) + .subexpression(nibble) .char(".") .end() .string("ip6") diff --git a/edify/library/address/socket.py b/edify/library/address/socket.py index 2fa29c7..7231448 100644 --- a/edify/library/address/socket.py +++ b/edify/library/address/socket.py @@ -3,7 +3,7 @@ from __future__ import annotations from edify import Pattern, any_of -from edify.library._support.atoms import octet +from edify.atoms.octet import octet _ipv4 = Pattern().subexpression(octet).exactly(3).group().char(".").subexpression(octet).end() _ipv6_bracket = ( 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/clock.test.py b/tests/atoms/clock.test.py new file mode 100644 index 0000000..2abe10a --- /dev/null +++ b/tests/atoms/clock.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import clock + + +def _anchored(): + return Pattern().start_of_input().use(clock).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("23:59") + + +def test_rejects_off_shape_input(): + assert not _anchored()("24:00") + + +def test_atom_composes_inside_a_larger_pattern(): + embedded = Pattern().start_of_input().string("v=").use(clock).end_of_input() + assert embedded("v=" + "23:59") + assert not embedded("23:59") + + +def test_atom_regex_string_is_non_empty(): + fragment = clock.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/hexcolor.test.py b/tests/atoms/hexcolor.test.py new file mode 100644 index 0000000..6dbd865 --- /dev/null +++ b/tests/atoms/hexcolor.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import hexcolor + + +def _anchored(): + return Pattern().start_of_input().use(hexcolor).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("#abc") + + +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(hexcolor).end_of_input() + assert embedded("v=" + "#abc") + assert not embedded("#abc") + + +def test_atom_regex_string_is_non_empty(): + fragment = hexcolor.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/hostname.test.py b/tests/atoms/hostname.test.py new file mode 100644 index 0000000..1fdd883 --- /dev/null +++ b/tests/atoms/hostname.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import hostname + + +def _anchored(): + return Pattern().start_of_input().use(hostname).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("a.b.c") + + +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(hostname).end_of_input() + assert embedded("v=" + "a.b.c") + assert not embedded("a.b.c") + + +def test_atom_regex_string_is_non_empty(): + fragment = hostname.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/isodate.test.py b/tests/atoms/isodate.test.py new file mode 100644 index 0000000..aefb79a --- /dev/null +++ b/tests/atoms/isodate.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import isodate + + +def _anchored(): + return Pattern().start_of_input().use(isodate).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("2024-01-15") + + +def test_rejects_off_shape_input(): + assert not _anchored()("2024/01/15") + + +def test_atom_composes_inside_a_larger_pattern(): + embedded = Pattern().start_of_input().string("v=").use(isodate).end_of_input() + assert embedded("v=" + "2024-01-15") + assert not embedded("2024-01-15") + + +def test_atom_regex_string_is_non_empty(): + fragment = isodate.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/label.test.py b/tests/atoms/label.test.py new file mode 100644 index 0000000..ba16b34 --- /dev/null +++ b/tests/atoms/label.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import label + + +def _anchored(): + return Pattern().start_of_input().use(label).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("example") + + +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(label).end_of_input() + assert embedded("v=" + "example") + assert not embedded("example") + + +def test_atom_regex_string_is_non_empty(): + fragment = label.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/localpart.test.py b/tests/atoms/localpart.test.py new file mode 100644 index 0000000..51003ca --- /dev/null +++ b/tests/atoms/localpart.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import localpart + + +def _anchored(): + return Pattern().start_of_input().use(localpart).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("user.name") + + +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(localpart).end_of_input() + assert embedded("v=" + "user.name") + assert not embedded("user.name") + + +def test_atom_regex_string_is_non_empty(): + fragment = localpart.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/nibble.test.py b/tests/atoms/nibble.test.py new file mode 100644 index 0000000..94f39c8 --- /dev/null +++ b/tests/atoms/nibble.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import nibble + + +def _anchored(): + return Pattern().start_of_input().use(nibble).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("a") + + +def test_rejects_off_shape_input(): + assert not _anchored()("g") + + +def test_atom_composes_inside_a_larger_pattern(): + embedded = Pattern().start_of_input().string("v=").use(nibble).end_of_input() + assert embedded("v=" + "a") + assert not embedded("a") + + +def test_atom_regex_string_is_non_empty(): + fragment = nibble.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/octet.test.py b/tests/atoms/octet.test.py new file mode 100644 index 0000000..00985b2 --- /dev/null +++ b/tests/atoms/octet.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import octet + + +def _anchored(): + return Pattern().start_of_input().use(octet).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("192") + + +def test_rejects_off_shape_input(): + assert not _anchored()("999") + + +def test_atom_composes_inside_a_larger_pattern(): + embedded = Pattern().start_of_input().string("v=").use(octet).end_of_input() + assert embedded("v=" + "192") + assert not embedded("192") + + +def test_atom_regex_string_is_non_empty(): + fragment = octet.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/port.test.py b/tests/atoms/port.test.py new file mode 100644 index 0000000..3c3958b --- /dev/null +++ b/tests/atoms/port.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import port + + +def _anchored(): + return Pattern().start_of_input().use(port).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("65535") + + +def test_rejects_off_shape_input(): + assert not _anchored()("65536") + + +def test_atom_composes_inside_a_larger_pattern(): + embedded = Pattern().start_of_input().string("v=").use(port).end_of_input() + assert embedded("v=" + "65535") + assert not embedded("65535") + + +def test_atom_regex_string_is_non_empty(): + fragment = port.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/semver.test.py b/tests/atoms/semver.test.py new file mode 100644 index 0000000..2b173e2 --- /dev/null +++ b/tests/atoms/semver.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import semver + + +def _anchored(): + return Pattern().start_of_input().use(semver).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("1.0.0") + + +def test_rejects_off_shape_input(): + assert not _anchored()("1.0") + + +def test_atom_composes_inside_a_larger_pattern(): + embedded = Pattern().start_of_input().string("v=").use(semver).end_of_input() + assert embedded("v=" + "1.0.0") + assert not embedded("1.0.0") + + +def test_atom_regex_string_is_non_empty(): + fragment = semver.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/slug.test.py b/tests/atoms/slug.test.py new file mode 100644 index 0000000..063e478 --- /dev/null +++ b/tests/atoms/slug.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import slug + + +def _anchored(): + return Pattern().start_of_input().use(slug).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("my-slug") + + +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(slug).end_of_input() + assert embedded("v=" + "my-slug") + assert not embedded("my-slug") + + +def test_atom_regex_string_is_non_empty(): + fragment = slug.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/username.test.py b/tests/atoms/username.test.py new file mode 100644 index 0000000..392bb70 --- /dev/null +++ b/tests/atoms/username.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import username + + +def _anchored(): + return Pattern().start_of_input().use(username).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("bob") + + +def test_rejects_off_shape_input(): + assert not _anchored()("ab") + + +def test_atom_composes_inside_a_larger_pattern(): + embedded = Pattern().start_of_input().string("v=").use(username).end_of_input() + assert embedded("v=" + "bob") + assert not embedded("bob") + + +def test_atom_regex_string_is_non_empty(): + fragment = username.to_regex_string() + assert fragment + assert isinstance(fragment, str) diff --git a/tests/atoms/uuid.test.py b/tests/atoms/uuid.test.py new file mode 100644 index 0000000..b085719 --- /dev/null +++ b/tests/atoms/uuid.test.py @@ -0,0 +1,26 @@ +from edify import Pattern +from edify.atoms import uuid + + +def _anchored(): + return Pattern().start_of_input().use(uuid).end_of_input() + + +def test_accepts_sample_from_shape(): + assert _anchored()("550e8400-e29b-41d4-a716-446655440000") + + +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(uuid).end_of_input() + assert embedded("v=" + "550e8400-e29b-41d4-a716-446655440000") + assert not embedded("550e8400-e29b-41d4-a716-446655440000") + + +def test_atom_regex_string_is_non_empty(): + fragment = uuid.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) |
