# Validated

> Like Either, but collects every error instead of stopping at the first.

Source: https://docs.pyrula.io/primitives/validated/

`Validated[T, E]` is `Valid(value)` or `Invalid(errors)`. Use it when a form, config file or batch should report every error in one pass.

```python
from pyrula import Validated, Valid, Invalid
```

## zip

`zip` combines two results. Two `Valid` give `Valid((a, b))`; any `Invalid` gives an `Invalid` with all errors concatenated.

```python
def check_name(name: str) -> Validated[str, str]:
    return Validated.valid(name) if name.strip() else Validated.invalid("name cannot be blank")

def check_age(age: int) -> Validated[int, str]:
    return Validated.valid(age) if 0 <= age <= 130 else Validated.invalid("age must be between 0 and 130")

result = check_name("").zip(check_age(200))
# Invalid(['name cannot be blank', 'age must be between 0 and 130'])
```

## Traversal and sequencing

`traverse(values, fn)` runs `fn` on every input, including those after an `Invalid`. `sequence(values)` collects existing results. Errors are flattened in encounter order, and success holds a `list`.

```python
Validated.traverse(["Ada", "Lin"], check_name)   # Valid(['Ada', 'Lin'])
Validated.traverse(["", "Ada", ""], check_name)
# Invalid(['name cannot be blank', 'name cannot be blank'])

Validated.sequence([Invalid(["name", "email"]), Valid(2), Invalid("age")])
# Invalid(['name', 'email', 'age'])
```

Callbacks and elements must be `Validated`, or `TypeError` is raised. Iterator and callback exceptions propagate and are not collected as errors. For stop-at-first-error, use [`Either.traverse`](https://docs.pyrula.io/primitives/either.md#traversal-and-sequencing).

## Constructors

| Call | Result |
|---|---|
| `Validated.valid(v)` / `Valid(v)` | `Valid(v)` |
| `Validated.invalid(e)` | `Invalid([e])` |
| `Validated.invalid([e1, e2])` | `Invalid([e1, e2])` |
| `Validated.cond(test, value, error)` | `Valid(value)` if `test` else `Invalid([error])` |

## Transform and fold

```python
Validated.valid(2).map(lambda x: x * 10)     # Valid(20)
Invalid("too short").map_err(lambda errs: [e.upper() for e in errs])
# Invalid(['TOO SHORT'])

result.fold(
    lambda errs: f"rejected: {', '.join(errs)}",
    lambda val: f"accepted: {val}",
)
```

`get_or_else` takes an eager default.

## Matching

```python
match Validated.sequence([check_name(""), check_name("Ada")]):
    case Valid(names):
        message = f"accepted: {names}"
    case Invalid(errors):
        message = f"rejected: {', '.join(errors)}"

assert message == "rejected: name cannot be blank"
```

## Errors are a tuple

`Invalid.errors` is an immutable `tuple`, so an `Invalid` is hashable when its errors are. The constructor takes a `list` or `tuple` as the whole error collection and wraps anything else as one error. `repr` and the JSON form write the errors as a list.

```python
Invalid("a").errors             # ('a',)
Invalid(("a", "b")).errors      # ('a', 'b'): two errors, not one tuple error
Invalid((("code", "msg"),))     # one error that is itself a tuple
```

## Either

```python
from pyrula import Ok, Err

Validated.from_either(Err("e")) # Invalid(['e'])
Invalid("e").to_either()        # Err(('e',)); the full error tuple is the payload
```

`@do` and `@do_async` do not accept `Validated`. Use `zip`, `sequence` or `traverse` to accumulate, and [`Either`](https://docs.pyrula.io/primitives/either.md) with `@do` for dependent steps. The JSON form is in [JSON form](https://docs.pyrula.io/primitives/json-form.md).
