Metadata-Version: 2.1
Name: edpopenapibuilder
Version: 0.2.4
Summary: E-Deploy OpenAPI Builder
Author-email: Max Guenes <max.santos@e-deploy.com.br>
Maintainer-email: Max Guenes <max.santos@e-deploy.com.br>
Requires-Python: <4,>=3.8
Description-Content-Type: text/markdown
Requires-Dist: httpmessageprocessor~=2.1
Requires-Dist: messageprocessor~=4.1
Requires-Dist: PyYAML~=6.0
Requires-Dist: StrEnum==0.4.15
Provides-Extra: dependencyinjection
Requires-Dist: DiHttpMessageProcessor<3.0.0,>=2.0.0; extra == "dependencyinjection"

# E-Deploy Open API Builder

Automatically generates OpenAPI 3.x YAML documentation for Python applications built on the `httpmessageprocessor`/`messageprocessor` framework. The library introspects your routes, processors, and interactors at runtime — deriving request/response schemas, path parameters, and query parameters directly from Python type annotations, with no manual spec writing required.

## How it works

Routes map to `MessageProcessor` instances that delegate to typed `Interactor` classes:

```
Route → MessageProcessor → Interactor.execute(RequestType) -> ResponseType
```

The builder walks this chain via `inspect`, resolves `Optional`, `List`, `Union`, and `Enum` generics, and emits `$ref` entries in `components/schemas` for complex object types. GET/DELETE routes produce query parameters; all other methods produce a `requestBody`.

## Installation

```bash
# Core
pip install edpopenapibuilder

# With dependency-injection support
pip install "edpopenapibuilder[dependencyinjection]"
```

## Usage

### 1. Dependency injection (recommended)

Load your module and `edpopenapibuilder.injection` together. The builder registers itself automatically and exposes `GET /oas/yaml`.

```python
import yourhttpmodule
import edpopenapibuilder.injection
from dependencyinjection import DependencyLoader
from dihttpmessageprocessor import AutoMappedDiRouterProcessorBuilder

dependencies_loader = DependencyLoader()
dependencies_loader.load_modules(
    modules=[
        yourhttpmodule,
        edpopenapibuilder.injection,
    ],
)

router_processor_builder = AutoMappedDiRouterProcessorBuilder(
    dependency_context=dependencies_loader.build_context(),
)
```

Or invoke the interactor directly:

```python
from edpopenapibuilder.injection.interactor import DependencyInjectionApiYamlBuilderInteractor
from edpopenapibuilder.interactor.dto import AutoOpenApiYamlBuilderRequest

interactor = DependencyInjectionApiYamlBuilderInteractor(
    router_processor_builder=router_processor_builder,
)
yaml_output = interactor.execute(AutoOpenApiYamlBuilderRequest())
```

### 2. Append route to an existing route dict

```python
from edpopenapibuilder.route import append_open_api_build_route

routes = {
    Route("GET", "/items"): AutoMappedMessageProcessor(
        interactor_type=ListItemsInteractor,
        request_type=ListItemsRequest,
    ),
}

routes = append_open_api_build_route(routes)
message_handler = HttpMessageProcessorMessageHandler(routes)
```

### 3. Build from a `MessageHandlerBuilder`

```python
from edpopenapibuilder.builder.helper import build_yaml_from_message_handler_builder

yaml_output = build_yaml_from_message_handler_builder(my_message_handler_builder)
```

The OpenAPI endpoint is available at:

```
GET {HOST}/oas/yaml
```

The `host` header is read automatically and added as the server URL in the spec.

## Configuration

The builder accepts a `BuilderConfig` TypedDict to control its behavior. Currently the main option is `conflict_strategy`, which determines how schema name conflicts are handled when the same DTO is used across routes with different path parameters.

### Schema conflict strategies

| Strategy | Behavior | Example |
|----------|----------|---------|
| `readable` (default) | Appends a `Without{ParamNames}` suffix | `AllTypesDtoWithoutId` |
| `reuse` | Reuses the same schema for all routes | `AllTypesDto` (single schema) |
| `uuid` | Appends a hash suffix on conflict | `AllTypesDto-502d8fd31d` |

```python
from edpopenapibuilder.builder.dto import BuilderConfig, SchemaConflictStrategy
from edpopenapibuilder.builder.impl import OpenApiModelBuilderImpl

# Pass config directly to the builder
builder = OpenApiModelBuilderImpl(config={
    "conflict_strategy": SchemaConflictStrategy.readable,
})
```

Or set a global default via the registry:

```python
from edpopenapibuilder.builder.dto import BuilderConfig, SchemaConflictStrategy
from edpopenapibuilder.builder.registry import set_default_config

set_default_config({
    "conflict_strategy": SchemaConflictStrategy.reuse,
})
```

Or pass config through the interactors:

```python
from edpopenapibuilder.injection.interactor import DependencyInjectionApiYamlBuilderInteractor
from edpopenapibuilder.interactor.dto import AutoOpenApiYamlBuilderRequest

interactor = DependencyInjectionApiYamlBuilderInteractor(
    router_processor_builder=router_processor_builder,
    config={"conflict_strategy": SchemaConflictStrategy.readable},
)
yaml_output = interactor.execute(AutoOpenApiYamlBuilderRequest())
```

## Customising generated output

Use **descriptors** to override or augment auto-generated paths and schemas without subclassing. Register them before the builder runs.

```python
from edpopenapibuilder.builder.registry import (
    add_route_descriptor,
    add_interactor_descriptor,
    add_payload_descriptor,
)
from edpopenapibuilder.builder.descriptors import path_descriptor, payload_descriptor
from edpopenapibuilder.builder import OpenApiModelBuilder
from edpopenapibuilder.builder.dto import RouteBuildParams
from edpopenapibuilder.model import PathModel, SchemaModel

# Augment a path by route
def describe_my_route(builder: OpenApiModelBuilder, params: RouteBuildParams, path: PathModel) -> PathModel:
    path.summary = "My endpoint summary"
    path.tags.append("my-tag")
    return path

add_route_descriptor(Route("GET", "/items"), path_descriptor(update=describe_my_route))

# Augment a path by interactor type
add_interactor_descriptor(MyInteractor, path_descriptor(update=describe_my_route))

# Augment a schema by payload type
def describe_my_dto(builder, params, schema: SchemaModel, path=None) -> SchemaModel:
    schema.description = "My DTO description"
    return schema

add_payload_descriptor(MyDto, payload_descriptor(update=describe_my_dto))
```

Each descriptor accepts `create` and `update` callbacks:
- `create` — replaces the default build logic entirely. **First-hit wins**: the registry walks descriptors in priority order (route → processor → interactor for paths; payload for schemas) and uses the first descriptor whose `create` callback is defined. Descriptors registered with only `update` do not short-circuit this lookup.
- `update` — receives the auto-built result and returns a modified version. **All matching descriptors run**: every route, processor, interactor (and payload, where applicable) descriptor whose `update` callback is defined will fire, in priority order.

Path descriptors also support a `build_params` callback (first-hit wins across route → processor).

### Route regex matching

Route descriptors support regex patterns. If no exact route match is found, the registry falls back to `re.fullmatch` against registered patterns:

```python
# Matches any GET route under /api/v1/
add_route_descriptor(
    Route("GET", "/api/v1/.+"),
    path_descriptor(update=describe_my_route),
)

# Matches routes with numeric IDs
add_route_descriptor(
    Route("GET", "/items/[0-9]+/details"),
    path_descriptor(update=describe_my_route),
)
```

Exact route matches always take priority over regex patterns.

### Type inheritance matching

Interactor, processor, and payload descriptors support type inheritance. A descriptor registered for a base class also matches its subclasses:

```python
class BaseInteractor:
    pass

class SpecificInteractor(BaseInteractor):
    pass

# This descriptor applies to SpecificInteractor (and any other subclass)
add_interactor_descriptor(BaseInteractor, path_descriptor(update=describe_my_route))

# Same for payload types
add_payload_descriptor(BaseDto, payload_descriptor(update=describe_my_dto))
```

Exact type matches always take priority over inheritance matches.

## Extra mapping support

When an `AutoMappedMessageProcessor` uses `extra_mapping` to map request fields from non-standard locations (headers, query parameters), the builder automatically:

1. Creates the corresponding OpenAPI parameter with the correct `in` location (`header`, `query`, or `path`)
2. Excludes the mapped field from the request body schema to avoid duplication

```python
Route("POST", "/items/{id}/archive"): AutoMappedMessageProcessor(
    ArchiveItemRequest,
    archive_item_interactor,
    extra_mapping={
        "tenant_id": {
            "name": "X-Tenant-Id",
            "location": "header",
        }
    },
)
```

This generates a required `X-Tenant-Id` header parameter and removes `tenant_id` from the request body schema. If all fields in the request type are covered by path parameters and extra mappings, the `requestBody` is omitted entirely.

## Adding server metadata

```python
from edpopenapibuilder.builder.registry import get_builder
from edpopenapibuilder.model import ServerModel, ServerVariableModel

builder = get_builder()
server = ServerModel(url="https://api.example.com", description="Production")
server.add_variable("env", ServerVariableModel(default="prod", description="Environment"))
builder.get_model().add_server(server)
```
