Extension¶
Koda Validate aims to provide enough tools to handle most common validation needs; for the cases it doesn’t cover, it aims to allow easy extension.
Validators¶
We’ll build a simple Validator for float values to demonstrate how Validators
are built.
from typing import Any
from koda_validate import ValidationResult, Invalid, Valid, Validator, TypeErr
class SimpleFloatValidator(Validator[float]):
# `__call__` allows a `SimpleFloatValidator` instance to be called like a function
def __call__(self, val: Any) -> ValidationResult[float]:
if isinstance(val, float):
return Valid(val)
else:
return Invalid(TypeErr(float), val, self)
Some notes:
Validatoris subclassed and parameterized withfloat. This just means a value of typeValid[float]must be returned when valid__call__acceptsAnybecause the type of input may be unknown before submitting to theValidatorInvalidis returned with all relevant validation context for downstream use, namely:an error type (
TypeErr(float))the value being validated (
val)a reference to the validator being used (
self)
Here’s how our Validator can be used:
>>> float_validator = SimpleFloatValidator()
>>> float_validator(5.5)
Valid(val=5.5)
>>> float_validator(5)
Invalid(
err_type=TypeErr(expected_type=<class 'float'>),
value=5,
validator=<SimpleFloatValidator object at ...>
)
Predicates¶
When we want to perform additional refinement against a value, we use Predicates. In
the case of float, we might want to check things like min, max, or fuzzy equality.
We’ll make a FloatMin Predicate to validate that float values are above some
threshold:
from dataclasses import dataclass
from koda_validate import Predicate
@dataclass
class FloatMin(Predicate[float]):
min: float
def __call__(self, val: float) -> bool:
return val >= self.min
We can use FloatMin on its own, but it’s not terribly useful.
>>> min_5 = FloatMin(5.0)
>>> min_5(5.678)
True
>>> min_5(1.23)
False
Predicates are more useful when we allow them to work with Validators. For simplicity,
we’ll allow just one.
from dataclasses import dataclass
from typing import Any, Optional
from koda_validate import Validator, Predicate, ValidationResult, PredicateErrs, Valid, Invalid
class SimpleFloatValidator(Validator[float]):
def __init__(self, predicate: Optional[Predicate[float]] = None) -> None:
self.predicate = predicate
def __call__(self, val: Any) -> ValidationResult[float]:
if isinstance(val, float):
if self.predicate(val):
return Valid(val)
else:
return Invalid(PredicateErrs([self.predicate]), val, self)
else:
return Invalid(TypeErr(float), val, self)
In the code above, if Predicate is specified, we’ll check it after we’ve verified the type of the value.
>>> validator = SimpleFloatValidator(FloatMin(2.5))
>>> validator(3.14)
Valid(val=3.14)
>>> validator(1.1)
Invalid(
err_type=PredicateErrs(predicates=[
FloatMin(min=2.5),
]),
value=1.1,
validator=<SimpleFloatValidator object at ...>
)
We limited the Validator to one Predicate for simplicity. In Koda Validate, Validators
that accept predicates typically allow of a list of Predicates. Because Predicates
cannot alter values, it’s safe to have as many as you want (i.e. SimpleFloatValidator(FloatMin(3.3), FloatMax(4.4), ...)).
Processors¶
We can also conforming values using processors. For this example, we’ll say we want to
convert floats to their absolute value before we validate it.
from koda_validate import Processor
class FloatAbs(Processor[float]):
def __call__(self, val: float) -> float:
return abs(val)
To allow a processor to be this to our Validator, we can change the code similarly to
how we did with a Predicate.
from typing import Optional, Any
class SimpleFloatValidator(Validator[float]):
def __init__(self,
predicate: Optional[Predicate[float]] = None,
preprocessor: Optional[Processor[float]] = None) -> None:
self.predicate = predicate
self.preprocessor = preprocessor
def __call__(self, val: Any) -> ValidationResult[float]:
if isinstance(val, float):
if self.preprocessor:
val = self.preprocessor(val)
if self.predicate(val):
return Valid(val)
else:
return Invalid(PredicateErrs([self.predicate]), val, self)
else:
return Invalid(TypeErr(float), val, self)
Usage:
>>> validator = SimpleFloatValidator(predicate=FloatMin(2.2), preprocessor=FloatAbs())
>>> validator(-5.5)
Valid(val=5.5)
Async¶
There are only a few things to do differently if we want to make this Validator work
asynchronously:
implement a
validate_asyncmethod on the Validator (which should be very similar to the existing__call__method)if desired, allow for
PredicateAsyncpredicates to be passed in
Then when you use the Validator in an async context, you just need to call it like:
validator = SimpleFloatValidator(...)
await validator.validate_async(5.5)
Note
It’s important to mention that you can build Validators, Predicates, and
Processors to be initialized with any combination of attributes you want. The only
contracts for these kinds of objects are on the __call__ and validate_async
methods; otherwise you have complete freedom to structure the logic as you see fit.
This discussion has focused on extension only in terms of what we can validate. To learn more about how we inspect validators to add new capabilities, check out Metadata.