Expand description
§alux-http
alux-http lets you declare an HTTP API once, as an ordinary value, and then run that same
declaration on any web framework. The same declaration also reads as an OpenAPI 3.1
document or a typed TypeScript client, so the API is never written a second time.
This crate holds only the declaration: which routes exist, where each handler argument comes from,
and what each endpoint answers with. It contains no web framework and depends only on
alux-ext. A separate crate then interprets the declaration: one turns
it into Poem routes, another into an axum router, another into an OpenAPI document. Adding one
never changes the declaration.
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;
}
/// A derived method becomes a value an endpoint can be declared with, argument names included.
#[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"]);§Methods
Declare the method an endpoint answers on with .get, .post, .put, .patch, .delete,
.head, .options, .trace, or .connect. Use .method to take the method as a type parameter
instead.
§Inputs
Every handler argument says where it comes from: .path(), .query(), .body() for JSON,
.form(), .raw_body(), .multipart(), .in_header(), .cookie(), .auth(), and .context() when
you want the framework’s own extractor. .with() takes a value the interpreter supplies directly.
Arguments are filled in the order you declare them. The declaration parses nothing itself: each interpreter does that with its framework’s extractors.
Query strings, headers and cookies hold names and values, so the type you read one into has to be
a struct rather than a single value. Say so by implementing NamedValuesAlg:
#[derive(Deserialize)]
struct Filters {
since: u64,
limit: Option<u32>,
}
impl NamedValuesAlg for Filters {}
// `?since=…&limit=…`, and `limit` is optional because the field is.
self.routes().get("/readings", self.op(Alg::search).query::<Filters>().json()).query::<String>() does not compile, because a lone String has no name for a caller to send it
under. Header names are converted for you, so a User-Agent header arrives in a user_agent field.
.multipart::<T>() reads a body that arrives as parts. Your type implements
FromPartsAlg, which says how to build it from a reader of parts, and each
interpreter supplies whichever reader its framework has. A reader of parts is a
ChunksAlg, which is a sequence you take one item at a time. Its items are
PartAlg, and a part’s own content is another such sequence.
§Outputs
Declare what an endpoint answers with: .json(), .text(), .html(), .bytes(), .file(),
.empty(), .redirect(), or .stream(). Your handler’s return type is inferred, so you never
repeat it just to pick a format.
Not every kind accepts every result, and that is checked when you compile. .empty() takes a handler
returning () and rejects one returning data, so you cannot quietly throw a value away.
.stream() answers with a body produced over time. Your handler returns a type implementing
ChunksAlg, which says what a chunk is and how to take the next one, so you are not
committed to any particular stream type.
Three kinds wrap the one before them:
.status::<201>()sets the status code. Which code a created resource answers with belongs to the endpoint, not the handler..result()handles a handler returningResult. Success answers with the kind you already chose; a failure answers with the status and message itsHttpErrorAlgimpl gives..out_header::<CacheControl>()adds a response header. The handler returns(value, body), because only the handler knows anETagor a cache lifetime.
A header is just a name, so one this crate does not already ship is three lines of your own and no interpreter changes:
use alux_http::HeaderNameAlg;
/// States the `x-request-id` header an answer carries.
struct RequestId;
impl HeaderNameAlg for RequestId {
const HEADER_NAME: &'static str = "x-request-id";
}self.routes()
// A recording, which creates something and says so.
.post("/record", self.op(Alg::record).body::<u32>().json().status::<201>())
// One identified reading, or what its failure means.
.get("/find/{id}", self.op(Alg::find).path::<u32>().json().result())§Paths
Paths are parsed into segments when you declare them, so you do not write them for one particular
router. :id and {id} both mean one segment bound as id; *rest and {*rest} both mean
everything left over. Anything else matches literally.
Each interpreter then renders those segments the way its own router wants: Poem gets :id, axum and
actix-web get {id}, Rocket gets <id>. Every interpreter describes the path the same way though,
so two interpretations of one declaration can be compared.
§Composing surfaces
A declaration is a value, so it composes before anything runs it. Two crates that know nothing about each other can each declare part of a service, and a third can declare the whole of it, with no shared route table, no registry, and 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: 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()
// Both fragments, side by side.
.merge(self.status_api::<Alg>())
// The whole items fragment, under one prefix.
.nest("/v1", self.items_api::<Alg>())
}
}Write no return type on a declaration: calling it hands back the declared API, and the type is
generated for you. Inside the body, merge puts two declarations beside each other and nest puts
one under a prefix, including a declaration from another crate.
That gives you:
- Fragments that state their own needs.
status_apirequiresJsonOutAlg; a fragment answering with a file requiresFileOutAlginstead. Neither imposes its needs on the other, andservice_apirequires exactly the union. - One surface everywhere.
service_apiis a value, so the served API, theOpenAPIdocument and the generated client are the same merged surface and cannot drift apart. - No special cases. A merged declaration is a declaration, so it can be merged or nested again.
§Servers and lifecycle
HttpServerAlg specifies what it means to open, close and end one framework’s executable route
surface. Closing releases the address; ending waits for what was already being served as well, up to
the interpreter’s own drain. Each crate that serves chooses the surface, open handle and error types it needs. .lifecycle(commands)
turns a sequence of open and close commands into the events they produce, so an application can run one
server or switch between several without the declaration knowing anything about a runtime.
These crates serve the same declaration, without changing it:
alux-http-poemserves it with Poem.alux-http-axumserves it with axum.alux-http-actixserves it with actix-web.alux-http-salvoserves it with Salvo.alux-http-warpserves it with warp.alux-http-rocketserves it with Rocket.alux-http-directserves it with no web framework at all: it matches the request against the routes and calls the handler itself.alux-http-hypercarries the bytes for it over hyper.
These read the same declaration instead of serving it:
alux-http-openapigenerates anOpenAPIdocument for it.alux-http-typescriptgenerates a typed TypeScript client for it.alux-http-textprints its routes and types, which is useful in tests and documentation.
alux-http-conformance is a shared test suite: one declared
API, plus the requests and expected responses every crate above is checked against. It is how the
project knows they all behave the same.
Structs§
- Auth
- Marks an HTTP authentication input.
- Body
- Marks an HTTP request-body input.
- Bytes
Out - Selects raw-byte output semantics.
- Cache
Control - States the
cache-controlheader an answer carries. - Canonical
Path - Spells parameters the way a described surface states them.
- Connect
- Identifies a
CONNECTendpoint declaration. - Content
Disposition - States the
content-dispositionheader an answer carries. - Content
Language - States the
content-languageheader an answer carries. - Context
- Marks an endpoint-context input.
- Cookie
- Marks an input read from the cookies a caller sent.
- Delete
- Identifies a
DELETEendpoint declaration. - Direct
- Marks an input supplied directly by an interpreter.
- ETag
- States the
etagheader an answer carries. - Empty
- Represents the empty route program.
- Empty
Out - Selects empty output semantics.
- Endpoint
- Represents an endpoint without choosing an HTTP interpreter.
- Expires
- States the
expiresheader an answer carries. - FileOut
- Selects streamed-file output semantics.
- Form
- Marks a form-encoded HTTP request-body input.
- Get
- Identifies a
GETendpoint declaration. - Head
- Identifies a
HEADendpoint declaration. - Header
- Marks an HTTP header input.
- Header
Out - Answers with a header the handler states, beside the body
Kindstates. - HtmlOut
- Selects HTML output semantics.
- Http
Bind - Names the socket address at which an HTTP surface is bound.
- Http
Program Builder - Constructs neutral HTTP route programs.
- Http
Server Setup - States one executable HTTP surface together with the address at which it is served.
- Http
Status - Names the status a response is answered with.
- JsonOut
- Selects JSON output semantics.
- Last
Modified - States the
last-modifiedheader an answer carries. - Link
- States the
linkheader an answer carries. - Location
- States the
locationheader an answer carries. - Merge
- Represents the categorical coproduct of two route programs.
- Multipart
- Marks an input read from a request body arriving as parts.
- 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.
- Options
- Identifies a
OPTIONSendpoint declaration. - Patch
- Identifies a
PATCHendpoint declaration. - Path
- Marks an HTTP path input.
- Post
- Identifies a
POSTendpoint declaration. - Put
- Identifies a
PUTendpoint declaration. - Query
- Marks an HTTP query input.
- RawBody
- Marks an HTTP request body taken as it arrived.
- Redirect
Out - Selects redirect output semantics.
- Result
Out - Answers with
Kindwhen the handler succeeded, and with what its failure means otherwise. - Retry
After - States the
retry-afterheader an answer carries. - Route
Path - Carries a route path as the segments it matches.
- Route
Program - Carries a typed route program during fluent composition.
- Routes
- Carries a fluent route composition over an interpreter.
- Status
Out - Answers with
CODEand the bodyKindstates. - Stream
Out - Selects streamed output semantics.
- TextOut
- Selects plain-text output semantics.
- Trace
- Identifies a
TRACEendpoint declaration. - Vary
- States the
varyheader an answer carries.
Enums§
- Http
Method - Names one HTTP request method.
- Http
Server Command - Requests the desired lifecycle state of one bound HTTP server.
- Http
Server Event - Records the lifecycle transition an HTTP server interpreter made.
- Path
Segment - Names one part of a route path.
Traits§
- Bytes
OutAlg - Selects the converter used for raw-byte API outputs.
- Chunks
Alg - States a body produced over time: the chunks it carries, and what taking the next one means.
- Chunks
Ext - The operations a body stated as chunks derives.
- Compile
Route Program - Compiles a first-order route program with a concrete interpreter.
- Empty
OutAlg - Selects the converter used for empty API outputs.
- File
OutAlg - Selects the converter used for streamed-file API outputs.
- From
Parts Alg - States how an argument is read from a body that arrives as parts.
- Handler
Alg - Describes the capability to build typed handler endpoints.
- Handler
Endpoint Alg - Compiles a typed handler declaration supported by an interpreter.
- Header
Name Alg - States one header an answer carries.
- Header
OutAlg - Selects the converter used to answer with a header beside a body.
- Html
OutAlg - Selects the converter used for HTML API outputs.
- Http
ApiAlg - Combines the capabilities required to interpret a typed HTTP API.
- Http
Error Alg - States what one failure means to a caller.
- Http
Input Alg - Describes the HTTP input roles chosen by an interpreter.
- Http
Method Alg - Names the request method a declaration marker denotes.
- 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.
- Http
Server Alg - Interprets the lifecycle of one bound HTTP surface.
- Http
Server Ext - Derives streaming access to a concrete HTTP server’s lifecycle.
- 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.
- Named
Values Alg - States that an argument is read from a collection of names and values.
- Output
Alg - Transforms an inferred handler result into its portable API output.
- Output
Kind Alg - Resolves a portable output kind through an interpreter.
- PartAlg
- States one part of a body that arrives as parts.
- Path
Syntax Alg - Spells route-path parameters the way one router reads them.
- Redirect
OutAlg - Selects the converter used for redirect API outputs.
- Result
OutAlg - Selects the converter used for a handler that can fail.
- 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.
- Status
OutAlg - Selects the converter used to answer with a declared status.
- Stream
OutAlg - Selects the converter used for streamed API outputs.
- Text
OutAlg - Selects the converter used for plain-text API outputs.
- WithAlg
- Describes type-level accumulation of one more input in a declaration’s product.
Functions§
- compose_
path - Composes one absolute path from the parts a selector holds, spelled as
syntaxreads them. - describe_
path - Describes one absolute path in the spelling every interpretation states it in.
- read_
cookies - Reads the cookies one
Cookieheader states, as the names and values it carries. - read_
header_ name - Reads the name a header states as the name an argument states.
- write_
header_ name - Writes the name an argument states as the name a header states.
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.