# HttpMessageProcessor

HTTP routing and request-parsing library built on top of the `MessageProcessor` and `SimpleHttpRouter` packages. It provides two packages: a core routing layer (`httpmessageprocessor`) and a higher-level automation layer (`httpmessageprocessorutil`).

## Installation

```bash
pip install HttpMessageProcessor --extra-index-url https://pip.e-deploy.com.br
```

## Packages

### `httpmessageprocessor` — Core routing

Routes `HttpRequestMessage` events to per-route `MessageProcessor` instances.

```python
from httpmessageprocessor import (
    HttpMessageProcessor,
    HttpMessageProcessorMessageHandler,
    ApiRequest,
    RouterProcessorBuilder,
)
```

**`HttpMessageProcessor`** accepts either a `Dict[Route, MessageProcessor]` mapping or a custom `RouterProcessorBuilder`.

**`HttpMessageProcessorMessageHandler`** is a convenience wrapper that binds `HttpMessageProcessor` to `DefaultToken.TK_HTTP_MSG` on the message bus.

### `httpmessageprocessorutil` — Automation layer

#### `AutoMappedMessageProcessor`

Automates the full HTTP request lifecycle: parse → business call → serialize response.

```python
from httpmessageprocessorutil.automapped import AutoMappedMessageProcessor

processor = AutoMappedMessageProcessor(
    request_type=MyRequestDto,   # DTO class (or None for no body)
    interactor=my_interactor,    # must have execute(dto) or execute()
    exception_map={              # optional: maps exception types to HTTP status codes
        NotFoundException: 404,
        ConflictException: (409, b"conflict"),
        MyException: lambda e: HttpResponseMessage(422, ...),
    },
    success_status=200,          # optional, default 200
    extra_mapping=None,          # optional, see below
    response_content_type=None,  # optional, forces Content-Type header
    sanitizer=...,               # optional, see Sanitization section
)
```

The interactor must expose exactly one `execute` method accepting zero or one parameter. When `request_type` is `None`, `execute()` is called with no arguments.

#### DTO parsing (`ApiRequestParser`)

The parser deserializes `ApiRequest` into a typed DTO using Python type annotations. It searches **path parameters → query parameters → JSON body** in that priority order and automatically resolves field names across `snake_case`, `camelCase`, `PascalCase`, and `kebab-case` variants.

**Supported field types:** `int`, `float`, `bool`, `str`, `Decimal`, `date`, `datetime`, `UUID`, `Enum`, `List[T]`, `Dict[K, V]`, `Optional[T]`, `Union[A, B]`, nested DTOs, `ForwardRef`.

**GET requests:** the body is never parsed; all values come from path/query parameters.

#### `extra_mapping`

Pass as the `extra_mapping` keyword argument to `AutoMappedMessageProcessor` or `ApiRequestParser`:

```python
extra_mapping = {
    # Remove attributes absent from the payload (useful for PATCH)
    "removeNotSent": True,

    # Per-field overrides
    "user_id": {
        "location": "path",   # "path" | "query" | "header" | "body"
        "name": "userId",     # key name to look up
        "type": UUID,         # override parsed type
    },
}
```

#### `@auto_map` decorator

Override per-field parsing metadata directly on the DTO class:

```python
from httpmessageprocessorutil.automapped import auto_map

@auto_map({
    "host": {"location": "header", "name": "X-Host", "type": str},
})
class MyRequestDto:
    def __init__(self, host: str, name: str):
        self.host = host
        self.name = name
```

#### `@exec_map` decorator

Attach HTTP metadata to custom exception classes:

```python
from httpmessageprocessorutil.automapped import exec_map

@exec_map({"status_code": 422, "body": {"error": "unprocessable"}})
class MyException(Exception):
    pass
```

#### Exception formatting

`PropertyError` subclasses are always returned as **400** with a structured body:

```json
{"property": "field_name", "reason": "cannot be null"}
```

For list errors, an `innerError` key is included with the nested cause.

The `exception_map` values can be:
- `int` — status code; body is auto-serialised from the exception if it has a custom `__init__`
- `(int, body)` — explicit status code and body (`bytes` or `dict`)
- `callable` — receives the exception, returns `HttpResponseMessage` or a JSON-serialisable object

### Sanitization

By default, all `str` values parsed from requests are sanitized using `nh3` (strips all HTML tags). This prevents XSS from reaching business logic.

```python
from httpmessageprocessorutil.sanitizer import Sanitizer, Nh3Sanitizer

# Disable sanitization
processor = AutoMappedMessageProcessor(..., sanitizer=None)

# Custom sanitizer
class MySanitizer(Sanitizer):
    def _types_to_sanitize(self):
        return [str]

    def _sanitize(self, value):
        return my_custom_clean(value)

processor = AutoMappedMessageProcessor(..., sanitizer=MySanitizer())
```

## Data flow

```
MessageBus (HttpRequestMessage)
  → HttpMessageProcessorMessageHandler
    → HttpMessageProcessor          # route dispatch
      → AutoMappedMessageProcessor.parse_data()
          → ApiRequestParser.parse()   # builds typed DTO; sanitizes strings
      → AutoMappedMessageProcessor.call_business()
          → interactor.execute(dto)
      → AutoMappedMessageProcessor.format_response() / format_exception()
  → MessageBus (HttpResponseMessage)
```

## Response serialization

- Default: JSON (`application/json`), status 200 (or `success_status`).
- `Decimal` values are serialised as `float` rounded to 8 decimal places.
- `Enum` values are serialised by their `.value`.
- Pass `response_content_type` to force a custom `Content-Type` and skip JSON serialisation.

## Requirements

- Python ≥ 3.8
- `MessageProcessor ~= 4.1`
- `SimpleHttpRouter ~= 1.0`
- `iso8601 == 2.1.0`
- `nh3 >= 0.3.3, < 0.4.0`
