# `http_router`

The `http_router` library provides the first declarative routing layer on top of the `http_core` library. It is implemented as a category so that router objects can import it and implement the `http_handler_protocol` protocol directly while reusing common method dispatch and path-template matching logic.

## API documentation

Open the ../../apis/library_index.html#http_router link in a web browser.

## Loading

To load the library, load the `loader.lgt` file:

    | ?- logtalk_load(http_router(loader)).

## Testing

To test this library, load the `tester.lgt` file:

    | ?- logtalk_load(http_router(tester)).

## Layering

`http_router` sits between the normalized `http_core` message layer and higher-level API authoring helpers:

- Use `http_server` directly when you only need normalized messages, parsers, generators, or low-level handlers.

- Use `http_router` when you want the handler object to keep exposing `handle/2` while route matching, metadata annotation, middleware, and content negotiation are derived from `route/4` declarations.

- Add `rest` when `route/4` is still too low-level and you prefer endpoint descriptors plus small normalized result terms.

- Pair router objects with `open_api` when you want the route declarations and metadata to derive OpenAPI operations automatically.

- Add companion libraries such as `http_parameters`, `http_cors`, `http_htmx`, `http_session`, `http_static_files`, and `http_directory_listing` when you need parameter extraction, middleware helpers, sessions, or static-file routing on the same dispatch layer.

## Current scope

The initial routing solution provides one public predicate through the imported category:

- `handle/2` routes a normalized request using `route/4` clauses defined by the importing object.

The remaining router behavior is organized in the sections below.

## Route declarations

Importing router objects must define route descriptors using:

- `route(Id, Method, PathTemplate, Handler)`

The `Method` argument is a normalized lowercase HTTP method atom accepted by `http_core`, including `query` for RFC 10008 QUERY routes. The `Handler` argument is the name of a declared local predicate with arity 2, typically a protected predicate.

When automatic `OPTIONS` or `405 Method Not Allowed` responses are generated, QUERY routes are included in the `Allow` header like other route methods.

## Path templates

Path-template atoms support literal segments, anonymous `*` wildcard segments, plain `{name}` placeholders, and typed placeholders such as `{id:integer}` and `{score:number}`. Example:

    route(show_user, get, '/users/{id}', show_user).

## Request annotations

The router category calls the matched handler after annotating the request with `route(Id)`, `path_params(Pairs)`, and any `route_metadata/2` properties. When `route_produces/2` is defined and the request `Accept` header can be satisfied, it also adds the negotiated `response_media_type(MediaType)` property. On the normal routing path it scrubs stale internal synthetic properties such as `open_api_probe/1`, `automatic_options/1`, `effective_methods/1`, and `response_media_type/1` before handler execution.

## Route metadata

Importing router objects can optionally declare additional route-specific metadata using:

- `route_metadata(Id, Metadata)`

When defined, `Metadata` must be a list of compound terms. The router removes any existing request properties with the same functors, prepends the metadata to the matched request, and then adds the standard `route/1` and `path_params/1` annotations. This keeps route metadata available to both route handlers and response middleware and allows metadata descriptors such as `summary/1`, `description/1`, `tags/1`, or other application-specific terms.

## Route authorization

Importing router objects can optionally authorize or decorate routed requests after route matching and metadata annotation but before route handler dispatch using:

- `authorize_routed_request(Request, Action)`

The hook must return either `continue(Request)` or `respond(Response)`. Short-circuited responses still flow through response middleware and keep the routed request annotations such as `route/1`, `path_params/1`, and any route metadata properties. This hook is the main place to validate or decorate a routed request after route matching and metadata annotation but before the route handler is dispatched.

## Content negotiation

Importing router objects can optionally declare route response media types using:

- `route_produces(Id, MediaTypes)`

When defined, `MediaTypes` must be a non-empty list of media type atoms. The router negotiates the request `Accept` header against that list, annotates the matched request with `response_media_type(MediaType)`, and returns a generic `406 Not Acceptable` response when no produced media type matches.

On the normal routing path, any stale incoming `response_media_type/1` annotation is scrubbed before route dispatch and replaced only when the matched route successfully negotiates one of its declared `route_produces/2` media types.

## Middleware chaining

Importing router objects can also optionally define ordered middleware descriptors using:

- `middleware(Id, Handler)`

The `Handler` argument is the name of a declared local predicate with arity 2 that receives the current request and returns either `continue(Request)` or `respond(Response)`. Middleware runs before route matching, so it can rewrite requests before dispatch or short-circuit processing with an immediate response. Ordered middleware descriptors are evaluated in declaration order.

## Response middleware

Importing router objects can also optionally define ordered response middleware descriptors using:

- `response_middleware(Id, Handler)`

The `Handler` argument is the name of a declared local predicate with arity 3 that receives the current request, the current response, and returns the transformed response. Response middleware runs after route dispatch or short-circuit processing, so it can decorate or rewrite any generated response. It is the final place where a response can be adjusted before it is returned to the caller.

## HEAD fallback

`HEAD` requests match exact `head` routes first and otherwise fall back to a matching `get` route.

## OPTIONS handling

`OPTIONS` requests match explicit `options` routes first. When no explicit `options` route exists for a matched path, the router returns an automatic `200 OK` response with the derived `Allow` header and an empty body. The synthetic request used for this path is annotated with `automatic_options(true)` and `effective_methods(Methods)`. When the router can identify exactly one matching non-`options` route template, it also annotates that synthetic request with `route(Id)`, `path_params(Pairs)`, and that route `route_metadata/2` properties before response middleware runs. When multiple non-`options` routes match the same path, the synthetic request omits `route/1`, keeps `path_params/1` only when all matches produce the same value, and preserves only metadata properties that are identical across all matched routes. Automatic `OPTIONS` can be customized using `route_automatic_options_response/3` and still flows through response middleware.

## Custom bad-request responses

Importing router objects can optionally define:

- `route_bad_request_response(Request, Errors, Response)`

Dedicated route-handler exceptions matching `error(http_parameter_validation(Errors), Context)` with a non-empty `Errors` list are translated into `400 Bad Request` responses before response middleware runs. The router uses the optional `route_bad_request_response/3` hook when it is defined and otherwise falls back to a default plain-text response. The routed request annotations remain available to the hook and to any later response middleware.

## Custom error responses

Importing router objects can optionally customize other routing errors by defining:

- `route_not_found_response(Request, Response)`

- `route_method_not_allowed_response(Request, AllowedMethods, Response)`

When a path matches but the request method does not, the router returns a `405 Method Not Allowed` response with an `Allow` header derived from the matching route descriptors. A `get` route implicitly contributes both `GET` and `HEAD` to that header. Automatic router support for `OPTIONS` also contributes `OPTIONS` to the header for matched paths.

The `AllowedMethods` argument passed to the `405` hook is the effective method list as lowercase atoms and already includes implicit `head` support for `get` routes and automatic `options` support. The `Request` passed to the `405` hook and to response middleware is annotated with `matched_path(true)` and `effective_methods(AllowedMethods)`.

## Custom negotiation failures

Importing router objects can optionally define:

- `route_not_acceptable_response(Request, ProducedMediaTypes, Response)`

The `ProducedMediaTypes` argument passed to the `406` hook is the normalized list declared by `route_produces/2` for the matched route.

## OpenAPI derivation

When a router object also implements the `open_api_provider_protocol` protocol, the imported category can automatically derive `operations/1` from `route/4`, `route_metadata/2`, and `route_produces/2`. Recognized route-metadata terms for OpenAPI derivation are:

- `summary(Summary)`

- `description(Description)`

- `tags(Tags)`

- `deprecated(Boolean)`

- `security(Requirements)`

- `parameters(Parameters)`

- `request_body(RequestBody)`

- `responses(Responses)`

Path-template placeholders automatically derive default OpenAPI path parameter descriptors using a string schema. Metadata `parameters/1` can override those defaults or add extra query, header, or cookie parameters. When `responses/1` is not provided, the router first attempts to infer a successful response by probing the route handler with a synthetic annotated request and then falls back to a default successful response. When available, `route_produces/2` guides both response probing and the derived OpenAPI media types.

When `request_body/1` metadata is not provided, the router also attempts to infer a request body descriptor by probing the route handler with a synthetic annotated request whose body is left open for the handler to constrain. The inferred request and response schemas are emitted inline and are derived from the observed payload terms:

- `json/1` payloads infer JSON Schema types recursively from the observed JSON values.

- `text/1` and `binary/1` payloads infer a string schema.

- `form/1` payloads infer an object schema from the observed key-value pairs.

Explicit `request_body/1` or `responses/1` metadata always takes precedence over inferred descriptors.

The imported category also exposes top-level OpenAPI provider predicates. The default `api_info/1` descriptor is derived from the importing object identifier, `servers/1` defaults to `[]`, and importing objects can override or extend the provider surface with these hooks:

- `open_api_info(Info)`

- `open_api_servers(Servers)`

- `open_api_security(Security)`

- `open_api_schema(Name, Schema)`

- `open_api_security_scheme(Name, SecurityScheme)`

## Current limitations

- OpenAPI inference is best-effort. Even with the optional `route_open_api_request_body_example/2` and `route_open_api_response_example/2` hooks, explicit metadata remains the right choice for reusable component schemas and for routes whose contracts should be stated directly instead of inferred.