valid8r 0.5.7__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of valid8r might be problematic. Click here for more details.
- {valid8r-0.5.7 → valid8r-0.6.0}/PKG-INFO +1 -1
- {valid8r-0.5.7 → valid8r-0.6.0}/pyproject.toml +5 -1
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/__init__.py +1 -1
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/core/parsers.py +349 -4
- {valid8r-0.5.7 → valid8r-0.6.0}/LICENSE +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/README.md +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/core/__init__.py +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/core/combinators.py +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/core/maybe.py +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/core/validators.py +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/prompt/__init__.py +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/prompt/basic.py +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/py.typed +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/testing/__init__.py +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/testing/assertions.py +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/testing/generators.py +0 -0
- {valid8r-0.5.7 → valid8r-0.6.0}/valid8r/testing/mock_input.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "valid8r"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Clean, flexible input validation for Python applications"
|
|
5
5
|
authors = ["Mike Lane <mikelane@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -141,9 +141,13 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
|
141
141
|
"D102", # Don't require method docstrings in tests
|
|
142
142
|
"D103", # Don't require function docstrings in tests
|
|
143
143
|
"D104", # Don't require init docstrings in tests
|
|
144
|
+
"E501", # Allow long lines in tests
|
|
145
|
+
"ERA001", # Allow commented-out code (test labels)
|
|
144
146
|
"PGH003", # Allow tests to casually ignore mypy complaints about incorrect types
|
|
145
147
|
"PLC0415", # Allow imports inside test functions
|
|
146
148
|
"PLR0913", # Allow tests to have as many arguments as we want
|
|
149
|
+
"PLR2004", # Allow magic values in tests
|
|
150
|
+
"S101", # Allow asserts in tests
|
|
147
151
|
]
|
|
148
152
|
"tests/bdd/steps/*" = [
|
|
149
153
|
"S101", # Allow asserts in behave tests
|
|
@@ -45,6 +45,9 @@ except ImportError:
|
|
|
45
45
|
EmailNotValidError = None # type: ignore[assignment,misc]
|
|
46
46
|
validate_email = None # type: ignore[assignment]
|
|
47
47
|
|
|
48
|
+
import base64
|
|
49
|
+
import binascii
|
|
50
|
+
import json
|
|
48
51
|
from dataclasses import dataclass
|
|
49
52
|
from ipaddress import (
|
|
50
53
|
IPv4Address,
|
|
@@ -70,6 +73,11 @@ E = TypeVar('E', bound=Enum)
|
|
|
70
73
|
|
|
71
74
|
ISO_DATE_LENGTH = 10
|
|
72
75
|
|
|
76
|
+
# Compiled regex patterns for phone parsing (cached for performance)
|
|
77
|
+
_PHONE_EXTENSION_PATTERN = re.compile(r'\s*[,;]\s*(\d+)$|\s+(?:x|ext\.?|extension)\s*(\d+)$', re.IGNORECASE)
|
|
78
|
+
_PHONE_VALID_CHARS_PATTERN = re.compile(r'^[\d\s()\-+.]+$', re.MULTILINE)
|
|
79
|
+
_PHONE_DIGIT_EXTRACTION_PATTERN = re.compile(r'\D')
|
|
80
|
+
|
|
73
81
|
|
|
74
82
|
def parse_int(input_value: str, error_message: str | None = None) -> Maybe[int]:
|
|
75
83
|
"""Parse a string to an integer."""
|
|
@@ -1329,8 +1337,7 @@ def parse_phone(text: str | None, *, region: str = 'US', strict: bool = False) -
|
|
|
1329
1337
|
|
|
1330
1338
|
# Extract extension if present
|
|
1331
1339
|
extension = None
|
|
1332
|
-
|
|
1333
|
-
extension_match = re.search(extension_pattern, s, re.IGNORECASE)
|
|
1340
|
+
extension_match = _PHONE_EXTENSION_PATTERN.search(s)
|
|
1334
1341
|
if extension_match:
|
|
1335
1342
|
# Get the captured group (either group 1 or 2)
|
|
1336
1343
|
extension = extension_match.group(1) or extension_match.group(2)
|
|
@@ -1342,11 +1349,11 @@ def parse_phone(text: str | None, *, region: str = 'US', strict: bool = False) -
|
|
|
1342
1349
|
|
|
1343
1350
|
# Check for invalid characters before extracting digits
|
|
1344
1351
|
# Allow only: digits, whitespace (including tabs/newlines), ()-.+ and common separators
|
|
1345
|
-
if not
|
|
1352
|
+
if not _PHONE_VALID_CHARS_PATTERN.match(s):
|
|
1346
1353
|
return Maybe.failure('Invalid format: phone number contains invalid characters')
|
|
1347
1354
|
|
|
1348
1355
|
# Extract only digits
|
|
1349
|
-
digits =
|
|
1356
|
+
digits = _PHONE_DIGIT_EXTRACTION_PATTERN.sub('', s)
|
|
1350
1357
|
|
|
1351
1358
|
# Check for strict mode - original must have formatting
|
|
1352
1359
|
if strict and text.strip() == digits:
|
|
@@ -1404,3 +1411,341 @@ def parse_phone(text: str | None, *, region: str = 'US', strict: bool = False) -
|
|
|
1404
1411
|
extension=extension,
|
|
1405
1412
|
)
|
|
1406
1413
|
)
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
def parse_slug(
|
|
1417
|
+
text: str,
|
|
1418
|
+
*,
|
|
1419
|
+
min_length: int | None = None,
|
|
1420
|
+
max_length: int | None = None,
|
|
1421
|
+
) -> Maybe[str]:
|
|
1422
|
+
"""Parse a URL-safe slug (lowercase letters, numbers, hyphens only).
|
|
1423
|
+
|
|
1424
|
+
A valid slug contains only lowercase letters, numbers, and hyphens.
|
|
1425
|
+
Cannot start/end with hyphen or have consecutive hyphens.
|
|
1426
|
+
|
|
1427
|
+
Args:
|
|
1428
|
+
text: String to validate as slug
|
|
1429
|
+
min_length: Minimum length (optional)
|
|
1430
|
+
max_length: Maximum length (optional)
|
|
1431
|
+
|
|
1432
|
+
Returns:
|
|
1433
|
+
Maybe[str]: Success with slug or Failure with error
|
|
1434
|
+
|
|
1435
|
+
Examples:
|
|
1436
|
+
>>> from valid8r.core.parsers import parse_slug
|
|
1437
|
+
>>>
|
|
1438
|
+
>>> # Valid slugs
|
|
1439
|
+
>>> parse_slug('hello-world').value_or(None)
|
|
1440
|
+
'hello-world'
|
|
1441
|
+
>>> parse_slug('blog-post-123').value_or(None)
|
|
1442
|
+
'blog-post-123'
|
|
1443
|
+
>>> parse_slug('a').value_or(None)
|
|
1444
|
+
'a'
|
|
1445
|
+
>>>
|
|
1446
|
+
>>> # With length constraints
|
|
1447
|
+
>>> parse_slug('hello', min_length=5).value_or(None)
|
|
1448
|
+
'hello'
|
|
1449
|
+
>>> parse_slug('hello', max_length=10).value_or(None)
|
|
1450
|
+
'hello'
|
|
1451
|
+
>>>
|
|
1452
|
+
>>> # Invalid slugs
|
|
1453
|
+
>>> parse_slug('').is_failure()
|
|
1454
|
+
True
|
|
1455
|
+
>>> parse_slug('Hello-World').is_failure()
|
|
1456
|
+
True
|
|
1457
|
+
>>> parse_slug('hello_world').is_failure()
|
|
1458
|
+
True
|
|
1459
|
+
>>> parse_slug('-hello').is_failure()
|
|
1460
|
+
True
|
|
1461
|
+
>>> parse_slug('hello-').is_failure()
|
|
1462
|
+
True
|
|
1463
|
+
>>> parse_slug('hello--world').is_failure()
|
|
1464
|
+
True
|
|
1465
|
+
>>>
|
|
1466
|
+
>>> # Length constraint failures
|
|
1467
|
+
>>> parse_slug('hi', min_length=5).is_failure()
|
|
1468
|
+
True
|
|
1469
|
+
>>> parse_slug('very-long-slug', max_length=5).is_failure()
|
|
1470
|
+
True
|
|
1471
|
+
"""
|
|
1472
|
+
if not text:
|
|
1473
|
+
return Maybe.failure('Slug cannot be empty')
|
|
1474
|
+
|
|
1475
|
+
# Check length constraints
|
|
1476
|
+
if min_length is not None and len(text) < min_length:
|
|
1477
|
+
return Maybe.failure(f'Slug is too short (minimum {min_length} characters)')
|
|
1478
|
+
|
|
1479
|
+
if max_length is not None and len(text) > max_length:
|
|
1480
|
+
return Maybe.failure(f'Slug is too long (maximum {max_length} characters)')
|
|
1481
|
+
|
|
1482
|
+
# Check for leading hyphen
|
|
1483
|
+
if text.startswith('-'):
|
|
1484
|
+
return Maybe.failure('Slug cannot start with a hyphen')
|
|
1485
|
+
|
|
1486
|
+
# Check for trailing hyphen
|
|
1487
|
+
if text.endswith('-'):
|
|
1488
|
+
return Maybe.failure('Slug cannot end with a hyphen')
|
|
1489
|
+
|
|
1490
|
+
# Check for consecutive hyphens
|
|
1491
|
+
if '--' in text:
|
|
1492
|
+
return Maybe.failure('Slug cannot contain consecutive hyphens')
|
|
1493
|
+
|
|
1494
|
+
# Check for invalid characters (not lowercase, digit, or hyphen)
|
|
1495
|
+
if not re.match(r'^[a-z0-9-]+$', text):
|
|
1496
|
+
# Check specifically for uppercase
|
|
1497
|
+
if any(c.isupper() for c in text):
|
|
1498
|
+
return Maybe.failure('Slug must contain only lowercase letters, numbers, and hyphens')
|
|
1499
|
+
return Maybe.failure('Slug contains invalid characters')
|
|
1500
|
+
|
|
1501
|
+
return Maybe.success(text)
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def parse_json(text: str) -> Maybe[object]:
|
|
1505
|
+
"""Parse a JSON string into a Python object.
|
|
1506
|
+
|
|
1507
|
+
Supports all JSON types: objects, arrays, strings, numbers, booleans, null.
|
|
1508
|
+
|
|
1509
|
+
Args:
|
|
1510
|
+
text: JSON-formatted string
|
|
1511
|
+
|
|
1512
|
+
Returns:
|
|
1513
|
+
Maybe[object]: Success with parsed object or Failure with error
|
|
1514
|
+
|
|
1515
|
+
Examples:
|
|
1516
|
+
>>> from valid8r.core.parsers import parse_json
|
|
1517
|
+
>>>
|
|
1518
|
+
>>> # JSON objects
|
|
1519
|
+
>>> parse_json('{"name": "Alice", "age": 30}').value_or(None)
|
|
1520
|
+
{'name': 'Alice', 'age': 30}
|
|
1521
|
+
>>>
|
|
1522
|
+
>>> # JSON arrays
|
|
1523
|
+
>>> parse_json('[1, 2, 3, 4, 5]').value_or(None)
|
|
1524
|
+
[1, 2, 3, 4, 5]
|
|
1525
|
+
>>>
|
|
1526
|
+
>>> # JSON primitives
|
|
1527
|
+
>>> parse_json('"hello world"').value_or(None)
|
|
1528
|
+
'hello world'
|
|
1529
|
+
>>> parse_json('42').value_or(None)
|
|
1530
|
+
42
|
|
1531
|
+
>>> parse_json('true').value_or(None)
|
|
1532
|
+
True
|
|
1533
|
+
>>> parse_json('false').value_or(None)
|
|
1534
|
+
False
|
|
1535
|
+
>>> parse_json('null').value_or(None)
|
|
1536
|
+
>>>
|
|
1537
|
+
>>> # Invalid JSON
|
|
1538
|
+
>>> parse_json('').is_failure()
|
|
1539
|
+
True
|
|
1540
|
+
>>> parse_json('{invalid}').is_failure()
|
|
1541
|
+
True
|
|
1542
|
+
>>> parse_json('{"name": "Alice"').is_failure()
|
|
1543
|
+
True
|
|
1544
|
+
"""
|
|
1545
|
+
if not text:
|
|
1546
|
+
return Maybe.failure('JSON input cannot be empty')
|
|
1547
|
+
|
|
1548
|
+
try:
|
|
1549
|
+
result = json.loads(text)
|
|
1550
|
+
return Maybe.success(result)
|
|
1551
|
+
except json.JSONDecodeError as e:
|
|
1552
|
+
return Maybe.failure(f'Invalid JSON: {e.msg}')
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
def parse_base64(text: str) -> Maybe[bytes]:
|
|
1556
|
+
r"""Parse and decode a base64-encoded string.
|
|
1557
|
+
|
|
1558
|
+
Accepts both standard and URL-safe base64, with or without padding.
|
|
1559
|
+
Handles whitespace and newlines within the base64 string.
|
|
1560
|
+
|
|
1561
|
+
Args:
|
|
1562
|
+
text: Base64-encoded string
|
|
1563
|
+
|
|
1564
|
+
Returns:
|
|
1565
|
+
Maybe[bytes]: Success with decoded bytes or Failure with error
|
|
1566
|
+
|
|
1567
|
+
Examples:
|
|
1568
|
+
>>> from valid8r.core.parsers import parse_base64
|
|
1569
|
+
>>>
|
|
1570
|
+
>>> # Standard base64 with padding
|
|
1571
|
+
>>> parse_base64('SGVsbG8gV29ybGQ=').value_or(None)
|
|
1572
|
+
b'Hello World'
|
|
1573
|
+
>>>
|
|
1574
|
+
>>> # Standard base64 without padding
|
|
1575
|
+
>>> parse_base64('SGVsbG8gV29ybGQ').value_or(None)
|
|
1576
|
+
b'Hello World'
|
|
1577
|
+
>>>
|
|
1578
|
+
>>> # URL-safe base64 (hyphens and underscores)
|
|
1579
|
+
>>> parse_base64('A-A=').is_success()
|
|
1580
|
+
True
|
|
1581
|
+
>>> parse_base64('Pz8_').is_success()
|
|
1582
|
+
True
|
|
1583
|
+
>>>
|
|
1584
|
+
>>> # Base64 with whitespace (automatically stripped)
|
|
1585
|
+
>>> parse_base64(' SGVsbG8gV29ybGQ= ').value_or(None)
|
|
1586
|
+
b'Hello World'
|
|
1587
|
+
>>>
|
|
1588
|
+
>>> # Invalid base64
|
|
1589
|
+
>>> parse_base64('').is_failure()
|
|
1590
|
+
True
|
|
1591
|
+
>>> parse_base64('Not@Valid!').is_failure()
|
|
1592
|
+
True
|
|
1593
|
+
>>> parse_base64('====').is_failure()
|
|
1594
|
+
True
|
|
1595
|
+
"""
|
|
1596
|
+
# Strip all whitespace (including internal newlines)
|
|
1597
|
+
text = ''.join(text.split())
|
|
1598
|
+
|
|
1599
|
+
if not text:
|
|
1600
|
+
return Maybe.failure('Base64 input cannot be empty')
|
|
1601
|
+
|
|
1602
|
+
try:
|
|
1603
|
+
# Replace URL-safe characters with standard base64
|
|
1604
|
+
text = text.replace('-', '+').replace('_', '/')
|
|
1605
|
+
|
|
1606
|
+
# Add padding if missing
|
|
1607
|
+
missing_padding = len(text) % 4
|
|
1608
|
+
if missing_padding:
|
|
1609
|
+
text += '=' * (4 - missing_padding)
|
|
1610
|
+
|
|
1611
|
+
decoded = base64.b64decode(text, validate=True)
|
|
1612
|
+
return Maybe.success(decoded)
|
|
1613
|
+
except (ValueError, binascii.Error):
|
|
1614
|
+
return Maybe.failure('Base64 contains invalid characters')
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
def parse_jwt(text: str) -> Maybe[str]:
|
|
1618
|
+
"""Parse and validate a JWT (JSON Web Token) structure.
|
|
1619
|
+
|
|
1620
|
+
Validates that the JWT has exactly three parts (header.payload.signature)
|
|
1621
|
+
separated by dots, and that each part is valid base64url.
|
|
1622
|
+
Also validates that header and payload are valid JSON.
|
|
1623
|
+
|
|
1624
|
+
Note: This function validates JWT structure only. It does NOT verify
|
|
1625
|
+
the cryptographic signature. Use a dedicated JWT library (e.g., PyJWT)
|
|
1626
|
+
for signature verification and claims validation.
|
|
1627
|
+
|
|
1628
|
+
Args:
|
|
1629
|
+
text: JWT string to validate
|
|
1630
|
+
|
|
1631
|
+
Returns:
|
|
1632
|
+
Maybe[str]: Success with original JWT or Failure with error
|
|
1633
|
+
|
|
1634
|
+
Examples:
|
|
1635
|
+
>>> from valid8r.core.parsers import parse_jwt
|
|
1636
|
+
>>>
|
|
1637
|
+
>>> # Valid JWT (structure only - signature not verified)
|
|
1638
|
+
>>> jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig'
|
|
1639
|
+
>>> parse_jwt(jwt).is_success()
|
|
1640
|
+
True
|
|
1641
|
+
>>>
|
|
1642
|
+
>>> # JWT with whitespace (automatically stripped)
|
|
1643
|
+
>>> parse_jwt(' ' + jwt + ' ').is_success()
|
|
1644
|
+
True
|
|
1645
|
+
>>>
|
|
1646
|
+
>>> # Invalid: empty string
|
|
1647
|
+
>>> parse_jwt('').is_failure()
|
|
1648
|
+
True
|
|
1649
|
+
>>>
|
|
1650
|
+
>>> # Invalid: wrong number of parts
|
|
1651
|
+
>>> parse_jwt('header.payload').is_failure()
|
|
1652
|
+
True
|
|
1653
|
+
>>> parse_jwt('a.b.c.d').is_failure()
|
|
1654
|
+
True
|
|
1655
|
+
>>>
|
|
1656
|
+
>>> # Invalid: non-base64url encoding
|
|
1657
|
+
>>> parse_jwt('not-base64!.eyJzdWIiOiIxMjM0In0.sig').is_failure()
|
|
1658
|
+
True
|
|
1659
|
+
>>>
|
|
1660
|
+
>>> # Invalid: non-JSON header/payload
|
|
1661
|
+
>>> parse_jwt('bm90anNvbg==.eyJzdWIiOiIxMjM0In0.sig').is_failure()
|
|
1662
|
+
True
|
|
1663
|
+
"""
|
|
1664
|
+
# Strip whitespace
|
|
1665
|
+
text = text.strip()
|
|
1666
|
+
|
|
1667
|
+
if not text:
|
|
1668
|
+
return Maybe.failure('JWT cannot be empty')
|
|
1669
|
+
|
|
1670
|
+
parts = text.split('.')
|
|
1671
|
+
if len(parts) != 3:
|
|
1672
|
+
return Maybe.failure('JWT must have exactly three parts separated by dots')
|
|
1673
|
+
|
|
1674
|
+
# Helper to convert base64url to base64 with padding
|
|
1675
|
+
def decode_base64url(part: str) -> bytes:
|
|
1676
|
+
base64_part = part.replace('-', '+').replace('_', '/')
|
|
1677
|
+
missing_padding = len(base64_part) % 4
|
|
1678
|
+
if missing_padding:
|
|
1679
|
+
base64_part += '=' * (4 - missing_padding)
|
|
1680
|
+
return base64.b64decode(base64_part, validate=True)
|
|
1681
|
+
|
|
1682
|
+
# Validate header (part 0)
|
|
1683
|
+
if not parts[0]:
|
|
1684
|
+
return Maybe.failure('JWT header cannot be empty')
|
|
1685
|
+
|
|
1686
|
+
try:
|
|
1687
|
+
header_bytes = decode_base64url(parts[0])
|
|
1688
|
+
json.loads(header_bytes)
|
|
1689
|
+
except (ValueError, binascii.Error):
|
|
1690
|
+
return Maybe.failure('JWT header is not valid base64')
|
|
1691
|
+
except json.JSONDecodeError:
|
|
1692
|
+
return Maybe.failure('JWT header is not valid JSON')
|
|
1693
|
+
|
|
1694
|
+
# Validate payload (part 1)
|
|
1695
|
+
if not parts[1]:
|
|
1696
|
+
return Maybe.failure('JWT payload cannot be empty')
|
|
1697
|
+
|
|
1698
|
+
try:
|
|
1699
|
+
payload_bytes = decode_base64url(parts[1])
|
|
1700
|
+
json.loads(payload_bytes)
|
|
1701
|
+
except (ValueError, binascii.Error):
|
|
1702
|
+
return Maybe.failure('JWT payload is not valid base64')
|
|
1703
|
+
except json.JSONDecodeError:
|
|
1704
|
+
return Maybe.failure('JWT payload is not valid JSON')
|
|
1705
|
+
|
|
1706
|
+
# Validate signature (part 2)
|
|
1707
|
+
if not parts[2]:
|
|
1708
|
+
return Maybe.failure('JWT signature cannot be empty')
|
|
1709
|
+
|
|
1710
|
+
try:
|
|
1711
|
+
decode_base64url(parts[2])
|
|
1712
|
+
except (ValueError, binascii.Error):
|
|
1713
|
+
return Maybe.failure('JWT signature is not valid base64')
|
|
1714
|
+
|
|
1715
|
+
return Maybe.success(text)
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
# Public API exports
|
|
1719
|
+
__all__ = [
|
|
1720
|
+
'EmailAddress',
|
|
1721
|
+
'PhoneNumber',
|
|
1722
|
+
'UrlParts',
|
|
1723
|
+
'create_parser',
|
|
1724
|
+
'make_parser',
|
|
1725
|
+
'parse_base64',
|
|
1726
|
+
'parse_bool',
|
|
1727
|
+
'parse_cidr',
|
|
1728
|
+
'parse_complex',
|
|
1729
|
+
'parse_date',
|
|
1730
|
+
'parse_decimal',
|
|
1731
|
+
'parse_dict',
|
|
1732
|
+
'parse_dict_with_validation',
|
|
1733
|
+
'parse_email',
|
|
1734
|
+
'parse_enum',
|
|
1735
|
+
'parse_float',
|
|
1736
|
+
'parse_int',
|
|
1737
|
+
'parse_int_with_validation',
|
|
1738
|
+
'parse_ip',
|
|
1739
|
+
'parse_ipv4',
|
|
1740
|
+
'parse_ipv6',
|
|
1741
|
+
'parse_json',
|
|
1742
|
+
'parse_jwt',
|
|
1743
|
+
'parse_list',
|
|
1744
|
+
'parse_list_with_validation',
|
|
1745
|
+
'parse_phone',
|
|
1746
|
+
'parse_set',
|
|
1747
|
+
'parse_slug',
|
|
1748
|
+
'parse_url',
|
|
1749
|
+
'parse_uuid',
|
|
1750
|
+
'validated_parser',
|
|
1751
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|