Expand description
§alux-http
alux-http describes typed HTTP programs independently of a web framework.
The crate is a specification. It carries no interpreter and depends only on
alux-ext: an HTTP surface is described here as first-order syntax plus
the capability algebras an interpreter must witness. Domain specifications provide their own algebras
and compose first-order operations into routes.
use alux_ext::{OperationAlg, ext};
use alux_http::{HttpApiAlg, HttpProgramBuilder, JsonOutAlg, http};
use core::future::Future;
/// A downstream specification owns its primitive domain meaning.
trait StatusAlg {
type Status;
fn status(&self) -> impl Future<Output = Self::Status> + Send;
fn status_at(&self, id: u32) -> impl Future<Output = Self::Status> + Send;
}
/// Derived operations become first-order values that preserve their argument names.
#[ext(name = StatusOperationExt, defunc)]
impl<This> This
where
This: StatusAlg,
{
async fn status_current(&self) -> This::Status {
self.status().await
}
async fn status_for_id(&self, id: u32) -> This::Status {
self.status_at(id).await
}
}
/// The route program is declared before any framework is chosen.
#[ext(name = StatusApiExt, defunc(via = http))]
impl<This> This
where
This: HttpApiAlg + JsonOutAlg,
{
/// Declares the status surface: the current reading and one identified reading.
fn status_api<Alg>(&self)
where
Alg: StatusAlg,
{
self.routes()
// The reading as it stands.
.get("/status", self.op(Alg::status_current).json())
// One identified reading, its id taken from the path.
.get("/status/:id", self.op(Alg::status_for_id).path::<u32>().json())
}
}
struct App;
impl StatusAlg for App {
type Status = u32;
async fn status(&self) -> u32 {
1
}
async fn status_at(&self, id: u32) -> u32 {
id
}
}
// The same program is constructible directly, without the convenience macro and without an
// interpreter, because a route program is an ordinary value.
let builder = HttpProgramBuilder;
let program = builder
.routes()
// The same endpoint, declared without the convenience macro.
.get("/status", builder.op(StatusCurrentOperation::<App>::default()).json())
.into_program();
let _nested = builder.routes().nest("/api", builder.program(program)).into_program();
// Argument names and order survive from the authored method into the program.
assert_eq!(<StatusForIdOperation<App> as OperationAlg>::ARG_NAMES, ["id"]);§Composing surfaces
The reason to keep a surface first-order is that programs compose before anything interprets them. Two crates that know nothing about each other can each declare part of a service, and a third can state the whole of it — no shared route table, no registry, no framework in the picture yet.
use alux_ext::ext;
use alux_http::{HttpApiAlg, JsonOutAlg, http};
use core::future::Future;
trait StatusAlg {
type Status;
fn status(&self) -> impl Future<Output = Self::Status> + Send;
}
trait ItemsAlg {
type Items;
fn items(&self) -> impl Future<Output = Self::Items> + Send;
}
#[ext(name = StatusOperationExt, defunc)]
impl<This> This
where
This: StatusAlg,
{
/// Returns the status as it stands.
async fn status_current(&self) -> This::Status {
self.status().await
}
}
#[ext(name = ItemsOperationExt, defunc)]
impl<This> This
where
This: ItemsAlg,
{
/// Returns every item the domain holds.
async fn items_current(&self) -> This::Items {
self.items().await
}
}
/// One surface fragment. Its bounds name only what it uses: status, and JSON output.
#[ext(name = StatusApiExt, defunc(via = http))]
impl<This> This
where
This: HttpApiAlg + JsonOutAlg,
{
/// Declares the status route.
fn status_api<Alg>(&self)
where
Alg: StatusAlg,
{
// The reading as it stands.
self.routes().get("/status", self.op(Alg::status_current).json())
}
}
/// Another fragment, declared independently — plausibly in another crate.
#[ext(name = ItemsApiExt, defunc(via = http))]
impl<This> This
where
This: HttpApiAlg + JsonOutAlg,
{
/// Declares the item route.
fn items_api<Alg>(&self)
where
Alg: ItemsAlg,
{
// Every item the domain holds.
self.routes().get("/items", self.op(Alg::items_current).json())
}
}
/// The whole service: a coproduct of both fragments, with one of them under a path prefix.
#[ext(name = ServiceApiExt, defunc(via = http))]
impl<This> This
where
This: HttpApiAlg,
{
/// Declares `/status` beside `/v1/items`.
fn service_api<Alg>(&self)
where
Alg: StatusAlg + ItemsAlg,
{
self.routes()
// Merge forms the route coproduct.
.merge(self.status_api::<Alg>())
// Nesting precomposes a prefix over an entire subtree.
.nest("/v1", self.items_api::<Alg>())
}
}Two things there are worth pausing on, because the authored text is not what runs:
- The declarations look like they return nothing. They do return something. The macro replaces the
written signature with
-> ServiceApiProgram<Alg>and the written body withServiceApiProgram::default(), so callingservice_apihands back a zero-sized program value. Writing no return type is the convention: the type is generated, and naming it by hand would only repeat the macro. mergeandnestare not receiving routes. The authored body is read as a description rather than executed, and a call to a sibling declaration is lifted into a nested program — the emitted body readsbuilder.merge(builder.program(builder.status_api::<Alg>())). That is what type-checks, and it is why one fragment composes with another without either knowing how the other was declared.
What that buys, and why it is not just tidiness:
- Fragments state their own dependencies.
status_apirequiresJsonOutAlg; a fragment returning a file would requireFileOutAlginstead. Neither imposes its needs on the other, andservice_apiinherits exactly the union. - Nesting is selector precomposition, not a router feature — so
/v1/itemsarises from composing/v1with a subtree that never mentions it. - Composition happens before interpretation.
service_apiis a value; every interpreter sees the same merged surface, so documentation and execution cannot drift apart. - The surface is closed under composition. A merged program is a program, so it can be merged or nested again without a special case.
A program declared this way compiles through any crate that witnesses its algebras:
alux-http-textdescribes the surface as documentation or metadata.alux-http-poemcompiles the same program into executable Poem routes.
Structs§
- Auth
- Marks an HTTP authentication input.
- Body
- Marks an HTTP request-body input.
- Context
- Marks an endpoint-context input.
- Direct
- Marks an input supplied directly by an interpreter.
- Empty
- Represents the empty route program.
- Endpoint
- Represents an endpoint without choosing an HTTP interpreter.
- FileOut
- Selects streamed-file output semantics.
- Get
- Identifies a GET endpoint declaration.
- Header
- Marks an HTTP header input.
- Http
Program Builder - Constructs neutral HTTP route programs.
- JsonOut
- Selects JSON output semantics.
- Merge
- Represents the categorical coproduct of two route programs.
- Named
- Includes a separately named HTTP program in a route program.
- Nest
- Represents a route program nested below an HTTP path prefix.
- Operation
- Carries a typed operation declaration as first-order data.
- Path
- Marks an HTTP path input.
- Post
- Identifies a POST endpoint declaration.
- Query
- Marks an HTTP query input.
- Route
Program - Carries a typed route program during fluent composition.
- Routes
- Carries a fluent route composition over an interpreter.
Traits§
- Compile
Route Program - Compiles a first-order route program with a concrete interpreter.
- File
OutAlg - Selects the converter used for streamed file API outputs.
- Handler
Alg - Describes the capability to build typed handler endpoints.
- Handler
Endpoint Alg - Compiles a typed handler declaration supported by an interpreter.
- Http
ApiAlg - Combines the capabilities required to interpret a typed HTTP API.
- Http
Input Alg - Describes the HTTP input roles chosen by an interpreter.
- Http
Program Alg - Interprets a named, defunctionalized HTTP program with
Compiler. - Http
Program Ext - Compiles defunctionalized HTTP programs with an interpreter.
- Http
Route Alg - Combines the capabilities required to interpret HTTP route composition.
- Http
Selector Alg - Describes HTTP selectors independently of route composition.
- Interpret
Inputs Alg - Maps neutral input roles to the input types selected by an interpreter.
- Json
OutAlg - Selects the converter used for JSON API outputs.
- Output
Alg - Transforms an inferred handler result into its portable API output.
- Output
Kind Alg - Resolves a portable output kind through an interpreter.
- Route
Alg - Describes categorical construction and composition of routes.
- Route
AlgExt - Provides fluent route composition on any
RouteAlg. - Selector
Alg - Describes categorical composition of route selectors.
- WithAlg
- Describes type-level accumulation of one more input in a declaration’s product.
Functions§
- append_
path - Appends one normalized path segment to a composed absolute path.
Type Aliases§
- With
Endpoint - Carries a route program with one additional typed endpoint.
- With
Input - Carries an operation declaration with one additional typed input.
Attribute Macros§
- http
- Lowers extension methods into named, composable HTTP programs.