# Guard Failure Handling Redesign

## What and why

The current `guard_silent_reject` boolean on the view mixin is too all-or-nothing. Developers get two modes: show validation errors, or fake a success redirect. There's no way to customize what happens when guard checks fail without subclassing and reimplementing `form_invalid`.

Replace it with a hybrid API: an overridable method (`guard_form_invalid`) plus an optional callable attribute (`guard_on_failure`). Ship a pre-built `reject_silently` callable class for the common "fake success" pattern. The default behavior stays the same: failed guard checks produce form validation errors.

## Design

### Default behavior (unchanged)

By default, a failed guard check adds a non-field error to the form, just like any other validation failure. The form re-renders with error messages. No special configuration needed.

### Customizing failure handling (CBVs)

Three levels of customization, progressive disclosure:

**Level 1: Pre-built callable.** Set `guard_on_failure` to a provided handler.

```python
from formguard.handlers import reject_silently

class ContactView(GuardedFormView):
    guard_on_failure = reject_silently()

class FancyView(GuardedFormView):
    guard_on_failure = reject_silently(message='Thanks for your message!')
```

**Level 2: Custom callable.** Write your own function with signature `(request, form, success_url=None, **kwargs)`.

```python
def log_and_reject(request, form, success_url=None, **kwargs):
    flag_ip(request)
    return redirect(success_url)

class SignupView(GuardedFormView):
    guard_on_failure = log_and_reject
```

**Level 3: Override the method.** Full control with access to the view instance.

```python
class PaymentView(GuardedFormView):
    def guard_form_invalid(self, form):
        log_fraud_attempt(self.request, form.guard_failures)
        return render(self.request, 'blocked.html', status=200)
```

### Customizing failure handling (FBVs)

Function-based views check `form.guard_failures` directly:

```python
def contact(request):
    form = ContactForm(request.POST or None, request=request)
    if form.is_valid():
        send_email(form)
        return redirect('thanks')

    if form.guard_failures:
        return redirect('thanks')  # fake success for bots

    return render(request, 'contact.html', {'form': form})
```

### Components

#### `reject_silently` callable class

Location: `formguard/handlers.py`

```python
class reject_silently:
    def __init__(self, message=None):
        self.message = message

    def __call__(self, request, form, success_url=None, **kwargs):
        if self.message:
            messages.success(request, self.message)
        return redirect(success_url)
```

- Lowercase class name (it's used as a callable, reads like a function)
- Accepts optional `message` to add a Django success message
- `**kwargs` for forward compatibility

#### `guard_form_invalid` method on `GuardedFormViewMixin`

```python
def guard_form_invalid(self, form):
    if self.guard_on_failure is not None:
        return self.guard_on_failure(
            self.request, form,
            success_url=self.get_success_url(),
        )
    return super().form_invalid(form)
```

- Checks for callable first, then falls back to default (show errors)
- Passes `request`, `form`, and `success_url` as keyword arg
- The method itself is the override point for full control

#### Updated `form_invalid` on `GuardedFormViewMixin`

```python
def form_invalid(self, form):
    if form.guard_failures:
        return self.guard_form_invalid(form)
    return super().form_invalid(form)
```

- Separates guard failures from regular validation failures
- Regular validation errors always re-render the form normally
- Guard failures route through the customizable path

### Removals

- `guard_silent_reject` attribute: removed
- `guard_silent_message` attribute: removed
- The `form_invalid` override that checked these: replaced with the new dispatch

This is a clean break. The library is young with few users.

## Error handling

- If `guard_on_failure` is set but not callable, raise `ImproperlyConfigured` at view dispatch time (fail fast, clear error)
- If `guard_form_invalid` returns `None`, raise `ValueError` (developer forgot to return a response)

## Documentation

### Structure

Restructure docs alongside this change. README stays broadly the same.

```
README.md                 (update silent reject mention, update Further Reading links)

docs/
  failure-handling.md     NEW - this feature's primary doc
  configuration.md        RENAMED from advanced-usage.md (per-form checks, settings, manual rendering)
  checks.md               NEW - built-in check reference (field trap, token, js, interaction; POW when it ships)
  custom-checks.md        KEEP
  testing.md              KEEP
  signals.md              KEEP
  contrib/                future home for turnstile.md, ratelimit.md
```

### failure-handling.md outline

Frame it as: "By default, a failed check generates a form validation error. You can change this by..."

1. Default behavior (validation errors, no config needed)
2. Using `guard_on_failure` with `reject_silently()` (simplest customization)
3. Writing a custom callable
4. Overriding `guard_form_invalid()` for full control
5. Function-based views (check `form.guard_failures` directly)

### configuration.md outline

Content migrated from advanced-usage.md (minus silent reject section):

1. Per-form checks / `default_checks()`
2. Manual field rendering / `guard_fields`
3. Settings reference

### checks.md outline

Reference page for each built-in check:

1. FieldTrapCheck (what it does, settings, fail mode)
2. TokenCheck
3. JsChallengeCheck
4. InteractionCheck

### README changes

- Update "optional stealth-reject mode" mention to reference `failure-handling.md`
- Update Further Reading links to match new doc filenames
- Remove `guard_silent_reject` mention from the CBV example text

## Testing

Tests should verify:

- Default behavior: guard failures produce form validation errors (re-render)
- `guard_on_failure` callable is called with correct args when guard fails
- `guard_on_failure` is NOT called for regular validation failures
- `guard_form_invalid` override takes precedence over `guard_on_failure`
- `reject_silently()` redirects to success URL
- `reject_silently(message='...')` adds Django success message and redirects
- Non-callable `guard_on_failure` raises `ImproperlyConfigured`
- FBV pattern: `form.guard_failures` is populated after `is_valid()`
