Metadata-Version: 2.4
Name: validate-with-resolute
Version: 0.9.2
Summary: Immutable object modifications with the Result pattern - inspired by C# records
Author-email: Marcurion <marcurion@private.com>
License: MIT
Project-URL: Homepage, https://github.com/Marcurion/py-validate-with-resolute-package
Project-URL: Repository, https://github.com/Marcurion/py-validate-with-resolute-package
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.13
Description-Content-Type: text/markdown
Requires-Dist: resolute>=1.0.3
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Provides-Extra: pydantic
Requires-Dist: pydantic>=2.0; extra == "pydantic"

# validate-with-resolute

Immutable object modifications with the Result pattern, inspired by C# records.

Wraps dataclass and Pydantic model construction/modification in [`resolute`](https://pypi.org/project/resolute/) so validation errors are returned, never raised.

## Installation

```bash
pip install validate-with-resolute
```

Requires Python 3.13+.

## Usage

### `Record` base class (recommended)

Inherit from `Record` to get `.from_()` and `.with_()` with full type checking and IDE autocomplete.

```python
from dataclasses import dataclass
from validate_with_resolute import Record

@dataclass
class User(Record):
    name: str
    age: int

# Safe construction
result = User.from_(name="Bob", age=25)
if result.is_success:
    user = result.value

# Safe modification — original unchanged
result = user.with_(name="Alice", age=30)
if result.is_success:
    print(result.value.name)  # "Alice"
    print(user.name)          # "Bob"
```

Works with Pydantic:

```python
from pydantic import BaseModel, field_validator
from validate_with_resolute import Record

class User(BaseModel, Record):
    name: str
    age: int

    @field_validator('age')
    @classmethod
    def validate_age(cls, v: int) -> int:
        if v < 0:
            raise ValueError('age must be positive')
        return v

result = User.from_(name="Bob", age=-5)
if result.has_errors:
    print(result.errors)  # validation errors captured, not raised

user = User(name="Bob", age=25)
result = user.with_(age=-5)  # same — errors captured
```

### `modify()` standalone function

No inheritance required:

```python
from dataclasses import dataclass
from validate_with_resolute import modify

@dataclass
class User:
    name: str
    age: int

user = User(name="Bob", age=25)
result = modify(user, name="Alice", age=30)
if result.is_success:
    print(result.value.name)  # "Alice"
```

### `@with_modify` decorator

Adds `.with_()` to an existing class without inheritance. Type checkers require `# type: ignore[attr-defined]` on call sites.

```python
from validate_with_resolute import with_modify

@with_modify
@dataclass
class User:
    name: str
    age: int

result = user.with_(name="Alice")  # type: ignore[attr-defined]
```

Prefer `Record` or `modify()` instead.

## Result API

All functions return `Resolute[T]` from the `resolute` package:

```python
result.is_success   # True on success
result.has_errors   # True on failure
result.value        # Modified object (success only)
result.errors       # List of captured errors (failure only)
```

## Supported types

| Type | Strategy |
|------|----------|
| Pydantic v2 model | `model_validate` |
| Dataclass | `dataclasses.replace` |
| Other (Python 3.13+) | `copy.replace()` |
| Unsupported | `TypeError` in `result.errors` |

## Type checking

The package ships a `py.typed` marker and full generic annotations. `result.value` is typed as `T`.

| Approach | mypy | Autocomplete |
|----------|------|--------------|
| `Record` base class | full | full |
| `modify()` function | full | full |
| `@with_modify` decorator | needs ignore | none |

## Nested objects

```python
new_address = modify(address, city="LA").value
result = modify(person, address=new_address)
```

## License

MIT
