README.md

# elli_openapi
Library for building type-safe HTTP APIs with automatic OpenAPI documentation generation using Elli and Spectra.
This library is not ready for production use, but it wont take long to finish it.

## Usage

1. **Add to your rebar.config dependencies:**

```erlang
{deps, [
    {elli_openapi, "~> 0.1.1"}
]}.
```

2. **Start Elli with elli_openapi_handler as the callback and your routes as arguments to elli_openapi_handler:**

```erlang
%% Define your routes
Routes = [
    {<<"POST">>, <<"/api/users">>, fun user_handler:create_user/4},
    {<<"GET">>, <<"/api/users/{userId}">>, fun user_handler:get_user/4}
],

%% Configure and start Elli, preferably in your supervisor spec.
ElliOpts = [
    {callback, elli_openapi_handler},
    {callback_args, Routes},
    {port, 3000}
],

{ok, Pid} = elli:start_link(ElliOpts).
```

You can optionally pass custom OpenAPI metadata by wrapping `callback_args` in a `{MetaData, Routes}` tuple:

```erlang
MetaData = #{title => <<"My API">>, version => <<"1.0.0">>},
ElliOpts = [
    {callback, elli_openapi_handler},
    {callback_args, {MetaData, Routes}},
    {port, 3000}
].
```

See the `example/` directory for a runnable example application with handler implementations.

## Handler Functions

All handler functions must follow this signature:

```erlang
handler_name(PathArgs, QueryArgs, Headers, Body) -> {StatusCode, ResponseHeaders, ResponseBody}
```

### Arguments

1. **PathArgs** (`map()`): URL path parameters extracted from the route
   - Example: For route `<<"/api/users/{userId}">>`, PathArgs would be `#{userId => <<"42">>}`
   - Empty map `#{}` if no path parameters

2. **QueryArgs** (`map()`): URL query parameters
   - Example: `#{page => 1, per_page => 20}`
   - Declare expected query params in the function spec; undeclared params are ignored

3. **Headers** (`map()`): HTTP request headers with atom keys
   - Example: `#{'Authorization' => <<"Bearer ...">>, 'Content-Type' => <<"application/json">>}`
   - Required headers must be declared in the function spec

4. **Body** (`any()`): Request body, automatically decoded based on the type in your function spec
   - JSON requests: `map()` or record type
   - Plain text requests: `binary()`
   - Bodyless methods (GET, HEAD, etc.): declare as `binary()` — an empty body decodes cleanly to `<<"">>`
   - The library validates and decodes the body according to your spec

### Return Value

Must be a 3-tuple: `{StatusCode, ResponseHeaders, ResponseBody}`

- **StatusCode**: HTTP status code integer (200, 201, 400, etc.)
- **ResponseHeaders**: Map with atom keys (e.g., `#{'Location' => <<"...">>, 'ETag' => <<"...">>}`)
- **ResponseBody**: Response body (record, map, or binary) - will be encoded based on content type

To return different status codes from the same handler, use union types in your function spec where each branch represents a possible response:

```erlang
-spec my_handler(PathArgs, QueryArgs, Headers, Body) ->
    {200, Headers1, SuccessBody}
    | {400, Headers2, ErrorBody}
    | {404, Headers3, NotFoundBody}.
```

### Spec placement

`-spectra()` metadata attributes and `-spec` declarations must appear **before any function clause** in the file. The Erlang compiler processes attributes in declaration order — placing them after a function clause will cause them to be ignored or crash at startup.

```erlang
%% Correct order
-spectra(#{summary => <<"Create user">>}).
-spec create_user(#{}, #{}, #{}, #user{}) -> {201, #{}, #user{}}.
create_user(#{}, #{}, #{}, User) -> ...

%% Wrong — attributes after a function clause are not processed
some_other_function() -> ...
-spectra(#{summary => <<"Create user">>}).   %% too late
-spec create_user(...) -> ...
create_user(...) -> ...
```

Handler specs use Spectra's type system. See the [Spectra documentation](https://hexdocs.pm/spectra/readme.html) for supported types and serialization rules.

For complete handler examples, see `example/src/elli_openapi_demo.erl`.

## Example Application

The `example/` directory contains a runnable demo application showcasing multiple handler implementations including user management, echo, status updates, and item updates with conflict detection.

To run the example:

```bash
make demo
```

The demo starts on port 3000. Access the API documentation at:
- Swagger UI: http://localhost:3000/swagger
- ReDoc: http://localhost:3000/redoc
- OpenAPI JSON: http://localhost:3000/api-docs