Skip to content

Commit

Permalink
ValidationResult.map (#40)
Browse files Browse the repository at this point in the history
* wip

* map valid

* map and docs

* version

* classifier update

* more docs

* added other example
  • Loading branch information
keithasaurus committed Mar 1, 2024
1 parent d325a27 commit 6c9d48b
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 27 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
4.1.0 (Feb 29, 2024)
**Features**
- `ValidationResult.map()` can be used to succinctly convert data contained within `Valid` objects to some other type or value

4.0.0 (Sep 27, 2023)
**Breaking Changes**
- `to_serializable_errs` produces different text, removing assumption that validation input was deserialized from json
Expand Down
55 changes: 36 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,63 @@
# Koda Validate

Build typesafe validators automatically or explicitly -- or write your own. Combine them to
build validators of arbitrary complexity. Koda Validate is async-friendly, pure Python, and
1.5x - 12x faster than Pydantic.
Koda Validate is a library and toolkit for building composable and typesafe validators. In many cases,
validators can be derived from typehints (e.g. TypedDicts, dataclasses, and NamedTuples). For everything else, you can
combine existing validation logic, or write your own. At its heart, Koda Validate is just a few kinds of
callables that fit together, so the possibilities are endless. It is async-friendly and comparable in performance to Pydantic 2.

Koda Validate can be used in normal control flow or as a runtime type checker.

Docs: [https://koda-validate.readthedocs.io/en/stable/](https://koda-validate.readthedocs.io/en/stable/)

## At a Glance

#### Explicit Validators

```python
from koda_validate import ListValidator, StringValidator, MaxLength, MinLength

my_string_validator = StringValidator(MinLength(1), MaxLength(20))
my_string_validator("a string!")
#> Valid("a string!")
my_string_validator(5)
#> Invalid(...)


# Composing validators
list_string_validator = ListValidator(my_string_validator)
list_string_validator(["a", "b", "c"])
#> Valid(["a", "b", "c"])
```

#### Derived Validators

```python
from typing import TypedDict
from koda_validate import (StringValidator, MaxLength, MinLength,
ListValidator, TypedDictValidator, Valid, Invalid)
from koda_validate import (TypedDictValidator, Valid, Invalid)
from koda_validate.serialization import to_serializable_errs
from koda_validate.signature import validate_signature


# Automatic Validators
class Person(TypedDict):
name: str
hobbies: list[str]


person_validator = TypedDictValidator(Person)

# Produce readable errors
match person_validator({"name": "Guido"}):
case Valid(string_list):
print(f"woohoo, valid!")
case Invalid() as invalid:
# readable errors
print(to_serializable_errs(invalid))

# prints: {'hobbies': ['key missing']}


# Explicit Validators
string_validator = StringValidator(MinLength(8), MaxLength(20))
#> {'hobbies': ['key missing']}
```

list_string_validator = ListValidator(string_validator)
#### Runtime Type Checking

```python
from koda_validate.signature import validate_signature

# Runtime type checking
@validate_signature
def add(a: int, b: int) -> int:
return a + b
Expand All @@ -53,11 +71,10 @@ add(1, "2") # raises `InvalidArgsError`
# -----------------------
# b='2'
# expected <class 'int'>

```

There's much, much more... Check out the [Docs](https://koda-validate.readthedocs.io/en/stable/).
There's much, much more in the [Docs](https://koda-validate.readthedocs.io/en/stable/).


## Something's Missing Or Wrong
## Something's Missing or Wrong
Open an [issue on GitHub](https://github.com/keithasaurus/koda-validate/issues) please!
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
project = "Koda Validate"
copyright = "2023, Keith Philpott"
author = "Keith Philpott"
release = "4.0.0"
release = "4.1.0"

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
11 changes: 11 additions & 0 deletions docs/how_to/results.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ Let's try it
'Error of type TypeErr, while validating abc with IntValidator()'


ValidationResult.map()
^^^^^^^^^^^^^^^^^^^^^^
Sometimes you might want to convert the data contained by :class:`Valid` into another
type. ``.map`` allows you to do that without a lot of boilerplate:

.. doctest:: valid-map
>>> validator = IntValidator()
>>> validator(5).map(str)
Valid(val="5")


Working with ``Invalid``
------------------------
:class:`Invalid` instances provide machine-readable validation failure data.
Expand Down
10 changes: 7 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ Koda Validate
.. module:: koda_validate
:noindex:

Build validation logic :ref:`automatically<index:Derived Validators>`, explicitly, or :ref:`write your own<how_to/extension:Extension>`. Combine
validators for arbitrarily complex validation logic. Koda Validate can be used in normal control flow
(:ref:`compatible with asyncio<how_to/async:Async>`) or as a :ref:`runtime type checker<how_to/runtime_type_checking:Runtime Type Checking>`.

Koda Validate is a library and toolkit for building composable and typesafe validators. In many cases, validators can be
derived from typehints :ref:`automatically<index:Derived Validators>` (e.g. TypedDicts, dataclasses, and NamedTuples).
For everything else, you can compose validator callables or :ref:`write your own<how_to/extension:Extension>`. At its heart, Koda Validate
is just a few kinds of functions that fit together, so the possibilities are endless. It is async-friendly and comparable in performance to Pydantic 2.

Koda Validate can be used in normal control flow or as a :ref:`runtime type checker<how_to/runtime_type_checking:Runtime Type Checking>`.


Basic Usage
Expand Down
10 changes: 8 additions & 2 deletions koda_validate/valid.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, Union
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, Literal, Union

from koda_validate._generics import A
from koda_validate._generics import A, B
from koda_validate.errors import ErrType

if TYPE_CHECKING:
Expand All @@ -25,6 +25,9 @@ class Valid(Generic[A]):
statements. Mypy understands it as a tag for a tagged union.
"""

def map(self, func: Callable[[A], B]) -> "ValidationResult[B]":
return Valid(func(self.val))


@dataclass
class Invalid:
Expand Down Expand Up @@ -55,5 +58,8 @@ class Invalid:
statements. Mypy understands it as a tag for a tagged union.
"""

def map(self, func: Callable[[Any], B]) -> "ValidationResult[B]":
return self


ValidationResult = Union[Valid[A], Invalid]
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "koda-validate"
version = "4.0.0"
version = "4.1.0"
readme = "README.md"
description = "Typesafe, composable validation"
documentation = "https://koda-validate.readthedocs.io/en/stable/"
Expand All @@ -10,7 +10,6 @@ homepage = "https://github.com/keithasaurus/koda-validate"
keywords = ["validation", "type hints", "asyncio", "serialization", "typesafe", "validate", "validators", "predicate", "processor"]
classifiers = [
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Environment :: MacOS X',
'Environment :: Web Environment',
'Intended Audience :: Developers',
Expand Down
20 changes: 20 additions & 0 deletions tests/test_valid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from copy import copy

from koda_validate import Invalid, StringValidator, TypeErr, Valid


def test_valid_map() -> None:
assert Valid("something").map(lambda x: x.replace("some", "no")) == Valid("nothing")
assert Valid(5).map(str) == Valid("5")

inv = Invalid(
err_type=TypeErr(str),
value=5,
validator=StringValidator(),
)

mapped = copy(inv).map(lambda x: x.replace("some", "no"))
assert isinstance(mapped, Invalid)
assert mapped.value == inv.value
assert mapped.err_type == inv.err_type
assert mapped.validator == inv.validator

0 comments on commit 6c9d48b

Please sign in to comment.