Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ dmypy.json

# Pyre type checker
.pyre/
CLAUDE.md
.claude.json
29 changes: 25 additions & 4 deletions nitro_validator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
validated = validator.validate(data, rules)
"""

__version__ = "1.0.2"
__version__ = "1.0.3"

from .core import (
NitroValidator,
Expand Down Expand Up @@ -87,11 +87,32 @@


class NitroValidator(_OriginalNitroValidator):
"""Validate data against rules, with every built-in rule pre-registered.

This is the public entry point exported at the top level of the
package. It is a thin subclass of
:class:`nitro_validator.core.NitroValidator` whose only job is to
default the registry to a shared instance pre-populated with all 51+
built-in rules. Pass ``registry=`` to opt out of the defaults.

Example:
>>> from nitro_validator import NitroValidator
>>> v = NitroValidator()
>>> v.validate(
... {'email': 'a@b.co', 'age': 21},
... {'email': 'required|email', 'age': 'required|integer|min:18'},
... )
{'email': 'a@b.co', 'age': 21}
"""
NitroValidator with built-in rules pre-registered.
"""

def __init__(self, registry=None):
def __init__(self, registry: "NitroRuleRegistry" = None):
"""Create a validator, defaulting to the shared built-in registry.

Args:
registry: A custom :class:`NitroRuleRegistry`. Omit to reuse
the module-level registry that already contains every
built-in rule.
"""
super().__init__(registry or _default_registry)


Expand Down
60 changes: 53 additions & 7 deletions nitro_validator/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,79 @@
Custom exception classes for nitro-validator.
"""

from typing import Dict, List


class NitroValidatorException(Exception):
"""Base exception for all validator errors."""
"""Base exception for every error raised by nitro-validator.

Catch this when you want to trap anything the library throws without
caring which specific failure occurred. All other exceptions in the
package subclass this one.

Example:
>>> try:
... validator.validate(data, rules)
... except NitroValidatorException:
... # catches ValidationError, RuleNotFoundError, InvalidRuleError
... pass
"""

pass


class NitroValidationError(NitroValidatorException):
"""
Raised when validation fails.
"""Raise when one or more fields fail validation.

The `errors` attribute maps each failing field name to a list of
human-readable error messages. Rules that pass do not appear. This is
the exception raised by :meth:`NitroValidator.validate` on failure.

Attributes:
errors: Dictionary of field names to error messages
errors: Dict mapping field name to a list of error messages for
that field.

Example:
>>> try:
... validator.validate({'email': ''}, {'email': 'required|email'})
... except NitroValidationError as e:
... print(e.errors)
{'email': ['The email field is required.']}
"""

def __init__(self, errors):
def __init__(self, errors: Dict[str, List[str]]):
"""Build the exception with a field-keyed error map.

Args:
errors: Dict of field name to list of error strings.
"""
self.errors = errors
super().__init__(f"Validation failed: {errors}")


class NitroRuleNotFoundError(NitroValidatorException):
"""Raised when a validation rule is not found."""
"""Raise when a rule name is referenced but has not been registered.

Typically surfaced when a rule string like ``'my_custom_rule'`` is
used before the corresponding rule class is registered on the
validator's registry.

Example:
>>> validator.validate({'x': 1}, {'x': 'not_a_real_rule'})
Traceback (most recent call last):
...
NitroRuleNotFoundError: Rule 'not_a_real_rule' not found in registry
"""

pass


class NitroInvalidRuleError(NitroValidatorException):
"""Raised when a rule is invalid or improperly defined."""
"""Raise when a rule class is malformed or fails registration checks.

Thrown by :meth:`NitroRuleRegistry.register` when the supplied class
does not inherit from :class:`NitroValidationRule` or lacks a usable
``name`` attribute.
"""

pass
103 changes: 83 additions & 20 deletions nitro_validator/core/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,113 @@


class NitroValidationRule:
"""
Base class for all validation rules.

Custom rules should inherit from NitroValidationRule and implement the validate() method.
"""Base class for every validation rule in nitro-validator.

Subclass this to add a new rule. Set two class attributes and
implement :meth:`validate`:

* ``name`` — the string used in pipe-delimited rule syntax
(e.g. ``"required"``, ``"min"``).
* ``message`` — the default error template. Placeholders ``{field}``,
``{args}``, and positional ``{0}``, ``{1}``, ... are substituted
by :meth:`get_message`.

A rule instance carries its positional arguments in ``self.args`` —
these come from the colon/comma-delimited tail of a rule string
(``"between:1,100"`` → ``args == ("1", "100")``), always as strings
when parsed from a rule string, so cast inside :meth:`validate` if
you need numbers.

Convention: return ``True`` for ``None`` or empty string in every rule
*except* presence checks (``required``, ``accepted``, ``declined``).
This lets callers compose rules like ``"email"`` with an optional
field by omitting ``required``.

Attributes:
name: Class-level identifier used by the rule registry.
message: Class-level default error template.
args: Positional arguments the rule was instantiated with.
kwargs: Keyword arguments (reserved; ``message`` is extracted).
custom_message: Per-instance override applied by the validator
when the caller passes custom messages.

Example:
>>> from nitro_validator import NitroValidationRule, NitroValidator
>>> class StartsUpperRule(NitroValidationRule):
... name = "starts_upper"
... message = "The {field} field must start with an uppercase letter."
... def validate(self, field, value, data):
... if not value:
... return True # let `required` enforce presence
... return isinstance(value, str) and value[:1].isupper()
>>> v = NitroValidator()
>>> v.register_rule(StartsUpperRule)
>>> v.is_valid({'name': 'Alice'}, {'name': 'starts_upper'})
True
"""

name = None # Rule name (e.g., 'required', 'email', 'min')
message = "The {field} field is invalid." # Default error message

def __init__(self, *args, **kwargs):
"""
Initialize the rule with optional parameters.
def __init__(self, *args: Any, **kwargs: Any):
"""Store the rule's positional and keyword arguments.

Rule strings like ``"between:1,100"`` are parsed into
``*args = ("1", "100")`` — arguments therefore arrive as
strings and must be cast inside :meth:`validate`. Pass
``message=`` to override the default template for this
instance only.

Args:
*args: Positional arguments for the rule (e.g., min value, max value)
**kwargs: Keyword arguments (e.g., custom error message)
*args: Positional rule arguments (e.g. min value, pattern).
**kwargs: Reserved keyword options. ``message`` becomes
``self.custom_message``.
"""
self.args = args
self.kwargs = kwargs
self.custom_message = kwargs.get("message")

def validate(self, field: str, value: Any, data: dict) -> bool:
"""
Validate the field value.
"""Return ``True`` if ``value`` passes this rule, otherwise ``False``.

Subclasses must override this method. The full ``data`` dict is
provided so cross-field rules (``same``, ``confirmed``, ...) can
peek at sibling values.

Args:
field: The field name being validated
value: The value to validate
data: The complete data dictionary (for cross-field validation)
field: Name of the field being validated.
value: The value to check.
data: The full data dict under validation.

Returns:
True if validation passes, False otherwise
``True`` to accept the value, ``False`` to mark the field as
failing this rule.

Raises:
NotImplementedError: If a subclass does not override this.
"""
raise NotImplementedError("Subclasses must implement validate()")

def get_message(self, field: str) -> str:
"""
Get the error message for this rule.
"""Return the formatted error message for a failing field.

Uses ``self.custom_message`` when set (e.g. via the validator's
``messages`` argument), otherwise the class-level ``message``.
Substitutes ``{field}``, ``{args}`` (comma-joined), and
positional placeholders ``{0}``, ``{1}``, ... from ``self.args``.

Args:
field: The field name being validated
field: Name of the failing field to substitute into the
message template.

Returns:
The formatted error message
The rendered error message.

Example:
>>> rule = NitroValidationRule("18")
>>> rule.message = "The {field} must be at least {0}."
>>> rule.get_message("age")
'The age must be at least 18.'
"""
message = self.custom_message or self.message

Expand All @@ -68,6 +131,6 @@ def get_message(self, field: str) -> str:

return message

def __repr__(self):
def __repr__(self) -> str:
args_str = ", ".join(str(arg) for arg in self.args) if self.args else ""
return f"{self.__class__.__name__}({args_str})"
Loading
Loading