Metadata-Version: 2.4
Name: validate-with-resolute
Version: 0.9.0
Summary: Immutable object modifications with the Result pattern - inspired by C# records
Home-page: https://github.com/Marcurion/
Author: Marcurion
Author-email: Marcurion <marcurion@example.com>
License: MIT
Project-URL: Homepage, https://github.com/Marcurion/validate-with-resolute
Project-URL: Repository, https://github.com/Marcurion/validate-with-resolute
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.11.11
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"
Dynamic: author
Dynamic: home-page
Dynamic: requires-python

# validate-with-resolute

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

`validate-with-resolute` provides a safe, type-preserving way to modify dataclasses and Pydantic models using the Result pattern. All validation errors are captured and returned—no exceptions escape the API.

## Features

- ✅ **No exceptions escape** - All errors captured in `Resolute[T]`
- ✅ **Type-safe** - Full type preservation with generics
- ✅ **Immutable** - Original objects never modified
- ✅ **Framework support** - Works with dataclasses and Pydantic v2
- ✅ **Error accumulation** - Multiple validation errors collected together
- ✅ **Flexible API** - Use standalone function or decorator

## Installation

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

**Requirements:** Python 3.13+

## Quick Start

### ✅ Recommended: `Record` base class

Best option for type checking and autocomplete:

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

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

# Create with validation error handling
result = User.from_(name="Bob", age=25)
if result.is_success:
    user = result.value

# Modify with .with_() method - full autocomplete support!
result = user.with_(name="Alice", age=30)
if result.is_success:
    print(result.value.name)  # "Alice"
    print(user.name)          # "Bob" - original unchanged
```

**Works with Pydantic too:**
```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

# Safe creation - validation errors captured!
result = User.from_(name="Bob", age=-5)
if result.has_errors:
    print(result.errors)  # [ValidationError(...)]

# Safe modification
user = User(name="Bob", age=25)
result = user.with_(age=30)  # Autocomplete works!
```

### Alternative: Standalone `modify()` function

If you prefer not to use inheritance:

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

@dataclass
class User:
    name: str
    age: int

user = User(name="Bob", age=25)

# Successful modification
result = modify(user, name="Alice")
if result.is_success:
    print(result.value.name)  # "Alice"
```

## Usage Examples

### Multiple changes in one call

```python
result = modify(user, name="Charlie", age=35, active=True)
```

### Pydantic validation

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

@with_modify
class User(BaseModel):
    name: str
    age: int

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

user = User(name="Bob", age=25)
result = user.with_(age=-5)

# Validation error captured, not raised
assert result.has_errors
print(result.errors)  # [ValidationError(...)]
```

### Error accumulation

```python
# Multiple errors collected together
result = modify(user, invalid_field="x", another_bad="y")
assert len(result.errors) == 2
```

### Safe instance creation with `from_()`

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

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

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

    @field_validator('email')
    @classmethod
    def validate_email(cls, v: str) -> str:
        if '@' not in v:
            raise ValueError('invalid email')
        return v

# Traditional approach - raises exception
try:
    user = User(name="Bob", age=-5, email="invalid")
except Exception as e:
    print(f"Exception: {e}")

# With Record.from_() - returns Result
result = User.from_(name="Bob", age=-5, email="invalid")
if result.has_errors:
    for error in result.errors:
        print(f"Validation error: {error}")
    # All validation errors captured, no exception!
```

### Nested objects

```python
@dataclass
class Address:
    city: str
    country: str

@dataclass
class Person:
    name: str
    address: Address

address = Address(city="NYC", country="USA")
person = Person(name="Bob", address=address)

# Modify nested object first
new_address_result = modify(address, city="LA")

# Then update parent
result = modify(person, address=new_address_result.value)
```

## API Reference

### `Record` (Base Class)

**✅ Recommended:** Base class that provides `.from_()` and `.with_()` methods with full type checking support.

**Class Methods:**

#### `Record.from_(**kwargs) -> Resolute[T]`

Create a new instance with validation errors wrapped in Resolute (no exceptions raised).

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

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

# Safe creation - validation errors captured
result = User.from_(name="Bob", age=25)
if result.is_success:
    user = result.value
else:
    print(result.errors)  # All errors captured
```

**Perfect for:**
- Processing user input
- Parsing external data
- API request handling
- Any scenario where construction might fail

#### `instance.with_(**changes) -> Resolute[Self]`

Modify an existing instance with validation errors wrapped in Resolute.

```python
user = User(name="Bob", age=25)
result = user.with_(name="Alice")  # Full autocomplete & type checking!
if result.is_success:
    updated = result.value
```

**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

# Both from_() and with_() capture validation errors
result = User.from_(name="Bob", age=-5)  # ✓ Errors captured
result = user.with_(age=-5)              # ✓ Errors captured
```

**Benefits:**
- ✅ Full autocomplete in IDEs
- ✅ Full type checking with mypy
- ✅ Works with both dataclasses and Pydantic
- ✅ No decorators needed
- ✅ Clear inheritance model
- ✅ Safe creation with `from_()`
- ✅ Safe modification with `with_()`

### `modify(obj: T, **changes: Any) -> Resolute[T]`

Standalone function for immutable modifications (no inheritance required).

**Parameters:**
- `obj` - The object to modify (dataclass or Pydantic model)
- `**changes` - Field names and their new values

**Returns:**
- `Resolute[T]` - Success with modified object, or failure with errors

**Supported types:**
- Dataclasses (via `dataclasses.replace`)
- Pydantic v2 models (via `model_validate`)
- Objects with `copy.replace()` support (Python 3.13+)

**Example:**
```python
from validate_with_resolute import modify

result = modify(user, name="Alice")
```

### `@with_modify` (Decorator)

Decorator that adds `.with_()` method to a class (legacy approach).

**Note:** Prefer using `Record` base class instead for better type checking.

**Example:**
```python
@with_modify
@dataclass
class MyClass:
    field: str

result = obj.with_(field="value")  # type: ignore[attr-defined]
```

## Result Pattern

`validate-with-resolute` uses the [`resolute`](https://pypi.org/project/resolute/) package for Result types.

**Key methods:**
```python
result = modify(obj, field="value")

result.is_success   # True if successful
result.has_errors   # True if failed
result.value        # The modified object (on success)
result.errors       # List of errors (on failure)
```

## Error Handling

All exceptions during modification are caught and returned in `Resolute.errors`:

- `ValueError` - Invalid field names
- `ValidationError` - Pydantic validation failures
- `TypeError` - Unsupported object types
- Any other exception raised during modification

**No exceptions ever escape the API.**

## Framework Detection

`validate-with-resolute` automatically detects the object type:

1. **Pydantic v2** - Has `model_copy` method → uses `model_validate`
2. **Dataclass** - Detected via `dataclasses.is_dataclass()` → uses `dataclasses.replace`
3. **Fallback** - Attempts `copy.replace()` (Python 3.13+)
4. **Unsupported** - Returns `TypeError` in `Resolute.errors`

**Why only dataclasses and Pydantic?**
- These frameworks provide **validated, immutable updates**
- Regular Python classes would bypass `__init__` validation
- This ensures modifications are **always-valid** (core principle of validate-with-resolute)

## Type Safety

Full type preservation with generics:

```python
user: User = User(name="Bob", age=25)
result: Resolute[User] = modify(user, age=26)

# Type checkers know result.value is User
if result.is_success:
    updated_user: User = result.value
```

### Type Checking with mypy

The package includes a `py.typed` marker and full type annotations:

```bash
# Type check your code
mypy your_code.py --strict
```

**Type checking comparison:**

| Approach | Type Checking | Autocomplete | Recommendation |
|----------|--------------|--------------|----------------|
| `Record` base class | ✅ Full | ✅ Full | ⭐ **Best** |
| `modify()` function | ✅ Full | ✅ Full | ✅ Great |
| `@with_modify` decorator | ⚠️ Needs ignore | ❌ No | ⚠️ Legacy |

```python
# ⭐ Best: Record base class
@dataclass
class User(Record):
    name: str

result = user.with_(name="Alice")  # ✅ Full support!

# ✅ Great: modify() function
result = modify(user, name="Alice")  # ✅ Full support!

# ⚠️ Legacy: @with_modify decorator
result = user.with_(name="Alice")  # type: ignore[attr-defined]
```

## Why validate-with-resolute?

**Problem:**
```python
# Traditional approach - exceptions escape
user = User(name="Bob", age=25)
try:
    user.age = -5  # Might raise, might silently fail
    # Or need to create new instance manually
except ValidationError as e:
    # Handle error
```

**Solution with validate-with-resolute:**
```python
# All errors captured, no exceptions
result = user.with_(age=-5)
if result.has_errors:
    # All validation errors available
    print(result.errors)
```

## Design Principles

- **Immutability** - Original objects never modified
- **Type safety** - Full generic type preservation
- **Error accumulation** - Collect all errors, not just first
- **Framework agnostic** - Works with dataclasses, Pydantic, and more
- **No magic** - Simple, predictable behavior

## Development

```bash
# Clone repository
git clone https://github.com/Marcurion/validate-with-resolute
cd validate-with-resolute

# Install in development mode
pip install -e ".[dev,pydantic]"

# Run tests
pytest tests/ -v

# Type checking
mypy src/
```

## License

MIT License - see LICENSE file for details.

## Contributing

Contributions welcome! Please open an issue or PR.

## Credits

- Inspired by C# record types and the `with` keyword
- Uses the [`resolute`](https://pypi.org/project/resolute/) package for Result types
- Built with ❤️ for type-safe Python

## See Also

- [resolute](https://pypi.org/project/resolute/) - Result pattern for Python
- [pydantic](https://docs.pydantic.dev/) - Data validation library
- [dataclasses](https://docs.python.org/3/library/dataclasses.html) - Python standard library
