Welcome
Hi! Welcome to the documentation for Trillium, a modular toolkit for building async rust web applications.
Trillium runs on stable rust, is fully async, and can run on tokio, async-std, or smol. Using Trillium starts with code as simple as this:
fn main() {
trillium_smol::run(|conn: trillium::Conn| async move {
conn.ok("hello from trillium!")
});
}
Trillium is also built to scale up to complex applications with a full middleware stack comparable to Rails or Phoenix. Currently, opt-in features include a router, cookies, sessions, websockets, serving static files from disk or memory, a reverse proxy, and integrations for three template engine options. Trillium is just getting started, though, and there's a lot more to build.
Perhaps most importantly, Trillium intends to be a production-quality open source http framework for async rust, with support options available for commercial users.
Trillium's code is at github and rustdocs are available at docs.trillium.rs.
About this document
Here are some conventions in this document.
use
declarations will only be listed once on the first usage of a given type in order to keep code samples concise- In-line code looks like this:
|conn: Conn| async move { conn }
and will generally not involve fully qualified paths - Footnotes are represented like this1
- Informational asides look like this:
βΉοΈ Fun fact: Facts are fun
- Advanced asides look like this
π§βπ The handler trait provides several other lifecycle hooks for library authors
- Comparisons with Tide
π Tide endpoints look like
|_req: Request<_>| async { Response::new(200) }
whereas Trillium handlers look like|conn: Conn| async move { conn.with_status(200) }
- Comparisons with Plug:
π Halting a plug looks like
conn |> halt
(elixir), and the equivalent in trillium is returningconn.halt()
Footnotes can always be skipped
Who is this document for?
This document expects some familiarity with async rust. We intend to offer a beginner level document at some point, but for now we recommend looking at the rust book and the async book.
We also assume familiarity with web development in general, including concepts and patterns in http servers and frameworks.
In particular, we offer comparisons to rust's tide and elixir's phoenix / plug, as they serve as the primary inspirations for trillium.
Architectural Overview
Composition and Substitution
Trillium is published as a set of components that can be easily composed to create web servers. One of the goals of this design is that to the extent possible, all components be replaceable by alternatives.
Why is substitution so important?
Async rust web frameworks still have a lot of exciting exploration left in the near future. Instead of offering one solution as the best, trillium offers a playground in which you can experiment with alternatives. I want it to be painless to plug in an alternative router, or a different http logger, or anything else you can imagine.
There are a lot of different purposes a web framework might be used for, and the core library should not have to adapt in order for someone to add support for each of those features.
Although I imagine that for each of the core components there will only be one or two options, I think it is an essential aspect of good software design that frameworks be modular and composable, as there will always be tradeoffs for any given design.
Only compile what you need
Instead of your application depending on a library with a large list
of reexported dependencies and conditionally including/excluding them
based on cargo features, trillium tries to apply rust's "only pay for
what you need" approach both at runtime and compile time. In
particular, trillium avoids pulling in runtimes like tokio or
async-std except in the crates where you explicitly need those,
preferring instead to depend on small crates like futures_lite
wherever possible. Additionally, and in specific contrast to tide,
there is minimal default behavior. If you don't need a router, you
don't need to compile or run a router.
Everything is opt-in, instead of opt-out. Trillium uses small crates, each of which declares its own dependencies.
Relation to tide, http-types, and async-h1
As of trillium-v0.2.0, trillium no longer depends on http-types.
Trillium shares the same session store backends as tide.
Relation to Elixir Plug and Phoenix
The general architecture is directly inspired by Plug, and is intended to be a hybrid of the best of plug and the best of tide. Eventually, I intend to build an opinionated framework like Phoenix on top of the components that are Trillium, but I don't expect that to happen for a bit. I hope to keep the core feature set of trillium quite small and focus on getting the design right and improving performance as much as possible.
Core concepts: Handlers, Conn, and Adapters
The most important concepts when building a trillium application are
the Conn
type and the Handler
trait. Each Conn
represents a
single http request/response pair, and a Handler
is the trait that
all applications, middleware, and endpoints implement.
Let's start with an overview of a simple trillium application and then dig into each of those concepts a little more.
fn main() {
trillium_smol::run(|conn: trillium::Conn| async move {
conn.ok("hello from trillium!")
});
}
In this example, trillium_smol::run
is the runtime adapter and the
closure is a Handler that responds "hello from trillium!" to any web
request it receives. This is a fully functional example that you can
run with only the following dependencies:
[dependencies]
trillium = "0.2"
trillium-smol = "0.2"
If we cargo run
this example, we can then visit
http://localhost:8080 in a browser or make a curl request against that
url and see "hello from trillium!" as the response body. Note that we
won't see any output in the terminal because trillium is silent by
default.
Handlers
The simplest form of a handler is any async function that takes a
Conn
and returns that Conn
. This example sets a 200 Ok
status
and sets a string body.
use trillium::Conn;
async fn hello_world(conn: Conn) -> Conn {
conn.ok("hello world!")
}
With no further modification, we can drop this handler into a trillium
server and it will respond to http requests for us. We're using the
smol
-runtime based server adapter
here.
pub fn main() {
trillium_smol::run(hello_world);
}
We can also define this as a closure:
pub fn main() {
trillium_smol::run(|conn: trillium::Conn| async move {
conn.ok("hello world")
});
}
This handler will respond to any request regardless of path, and it
will always send a 200 Ok
http status with the specified body of
"hello world"
.
The State Handler
Trillium offers only one handler in the main trillium
crate: The
State handler, which places a clone of any type you provide into the
state set of each conn that passes through it. See the
rustdocs for State
for example usage.
Tuple Handlers
Earlier, we discussed that we can use state to send data between handlers and that handlers can always pass along the conn unchanged. In order to use this, we need to introduce the notion of tuple handlers.
Each handler in a tuple handler is called from left to right until the conn is halted.
π Readers familiar with elixir plug will recognize this notion as identical to pipelines, and that the term halt is
stolen frominspired by plug
env_logger::init();
use trillium_logger::Logger;
run((
Logger::new(),
|conn: Conn| async move { conn.ok("tuple!") }
));
This snippet adds a http logger to our application, so that if we
execute our application with RUST_LOG=info cargo run
and make a
request to http://localhost:8000, we'll see log output on stdout.
π§βπβ Why not vectors or arrays? Rust vectors and arrays are type homogeneous, so in order to store the Logger and closure type in the above example in an array or vector, we'd need to allocate them to the heap and actually store a smart pointer in our homogeneous collection. Trillium initially was built around a notion of "sequences," which were a wrapper around
Vec<Box<dyn Handler + 'static>>
. Because tuples are generic over each of their elements, they can contain heterogeneous elements of different sizes, without heap allocation or smart pointers.
Implementing Handler
The rustdocs for Handler contains the full details of the Handler interface for library authors. For many applications, it will not be necessary to use anything other than an async function or closure, but Handler can contain its own state and be implemented for any type that you author.
Assorted implementations provided by the trillium crate
You may see a few other types used in tests and examples.
()
: the noop handler, identical to|conn: Conn| async move { conn }
&'static str
andString
: This simple handler responds to all conns by halting, setting a 200-ok status, and sending the string content as response body.trillium_smol::run("hello")
is identical totrillium_smol::run(|conn: Conn| async move { conn.ok("hello") })
Option<impl Handler>
: This handler will noop if the option variant is none. This is useful for conditionally including handlers at runtime based on configuration or environment.
Conn
Before we explore the concept of a handler further, let's take a look
at Conn
. As mentioned above, Conn
represents both the request and
response, as well as any data your application associates with that
request-response cycle.
π§βπ Advanced aside: Although the naming of Conn is directly borrowed from Elixir's
plug
and therefore alsoPhoenix
, it does in fact also own (in the rust sense) the singularTcpStream
that represents the connection with the http client, and dropping aConn
will also disconnect the client as a result.
The rustdocs for Conn contain the full details for all of the things you can do with a conn.
Returning Conn
In general, because you'll be returning Conn
from handlers, it
supports a chainable (fluent) interface for setting properties, like:
conn.with_status(202)
.with_response_header("content-type", "application/something-custom")
.with_body("this is my custom body")
Accessing http request properties
Conn also contains read-only properties like request headers, request path, and request method, each of which have getter associated functions.
Default Response
The default response for a Conn is a 404 with no response body, so it
is always valid to return the Conn from a handler unmodified (|conn: Conn| async move { conn }
is the simplest valid handler).
State
In addition to holding the request properties and accumulating the response your application is going to send, a Conn also serves as a data structure for any information your application needs to associate with that request. This is especially valuable for communicating between handlers, and most core handlers are implemented using conn state. One important caveat to is that each Conn can only contain exactly one of each type, so it is highly recommended that you only store types that you define in state.
π Comparison with Tide: Tide has three different types of state: Server state, request state, and response state. In Trillium, server state is achieved using the
trillium::State
handler, which holds any type that is Clone and puts a clone of it into the state of each Conn that passes through the handler.
Extending Conn
It is a very common pattern in trillium for libraries to extend Conn
in order to provide additional functionality1. The Conn interface
does not provide support for sessions, cookies, route params, or many
other building blocks that other frameworks build into the core
types. Instead, to use sessions as an example, trillium_sessions
provides a SessionConnExt
trait which provides associated functions
for Conn that offer session support. In general, handlers that put
data into conn state also will provide convenience functions for
accessing that state, and will export a [Something]ConnExt
trait.
1 π§βπ see library_patterns for an example of authoring one of these
Runtime Adapters and TLS
Runtime Adapters
Let's talk a little more about that trillium_smol::run
line we've
been writing. Trillium itself is built on futures
(futures-lite
,
specifically). In order to run it, it needs an adapter to an async
runtime. There there are four of these
currently:
Although we've been using the smol adapter in these docs thus far, you should use whichever runtime you prefer. If you expect to have a dependency on async-std or tokio anyway, you might as well use the adapter for that runtime. If you're new to async rust or don't have an opinion, I recommend starting with trillium_smol. It is easy to switch trillium between runtimes at any point.
12-Factor by default, but overridable
Trillium seeks to abide by a 12 factor approach to configuration, accepting configuration from the environment wherever possible. The number of configuration points that can be customized through environment variables will likely increase over time.
To run trillium on a different host or port, either provide a HOST
and/or PORT
environment variables, or compile the specific values
into the application as follows:
pub fn main() {
trillium_smol::config()
.with_port(1337)
.with_host("127.0.0.1")
.run(|conn: trillium::Conn| async move { conn.ok("hello world") })
}
In addition to accepting the HOST
and PORT
configuration from the environment, on cfg(unix) systems, trillium will also pick up a LISTEN_FD
environment variable for use with catflap/systemfd. On cfg(unix)
systems, if the HOST
begins with .
, /
, or ~
, it is interpreted as a path and bound as a unix domain socket.
For more documentation on the default values and what configuration can be chained onto config(), see trillium_server_common::Config.
TLS / HTTPS
With the exception of aws lambda, which provides its own tls
termination at the load balancer, each of the above servers can be
combined with either rustls or native-tls, or with trillium-acme
to register
a certificate automatically with an ACME certificate provider like Let's
Encrypt.
Rustls:
use trillium::Conn;
use trillium_rustls::RustlsAcceptor;
const KEY: &[u8] = include_bytes!("./key.pem");
const CERT: &[u8] = include_bytes!("./cert.pem");
pub fn main() {
env_logger::init();
trillium_smol::config()
.with_acceptor(RustlsAcceptor::from_single_cert(CERT, KEY))
.run(|conn: Conn| async move { conn.ok("ok") });
}
Native tls:
use trillium::Conn;
use trillium_native_tls::NativeTlsAcceptor;
pub fn main() {
env_logger::init();
let acceptor = NativeTlsAcceptor::from_pkcs12(include_bytes!("./identity.p12"), "changeit");
trillium_smol::config()
.with_acceptor(acceptor)
.run(|conn: Conn| async move { conn.ok("ok") });
}
Automatic HTTPS via Let's Encrypt:
See the trillium-acme
documentation for examples.
A tour of some of the handlers that exist today
In order for trillium to be a usable web framework, we offer a number of core utilities. However, it is my hope that alternative implementations for at least some of these will exist in order to explore the design space and accommodate different design constraints and tradeoffs. Because not every application will need this functionality, they are each released as distinct crates from the core of trillium.
- Logger
- Cookies
- Sessions
- Template Engines
- Proxy
- Static File Serving
- Websockets
- Router
- http client
- reverse proxy
- use trillium as a reverse proxy for another server
- rustdocs (main)
- example
- method override
- the trillium-method-override crate adds support for using post requests with a query param as a substitute for other http methods
- rustdocs (main)
- example
- head
- the trillium-head crate supports responding to head requests
- rustdocs (main)
- example
- forwarding
- the trillium-forwarding crate supports setting remote ip and protocol from forwarded/x-forwarded-* headers sent by trusted reverse proxies
- rustdocs (main)
- example
Router
The trillium router is based on
routefinder. This router
supports two types of patterns: Untyped params and a single
wildcard. Named params are captured in a map-like interface. Any
handler can be mounted inside of a Router (including other Routers),
allowing entire applications to be mounted on a path, and allowing for
tuple handlers to be run on a given route. Any handler mounted inside
of a route that includes a *
will have the url rewritten to the
contents of that star.
Alternative routers that are not based on routefinder are a prime opportunity for innovation and exploration.
Here's a simple example of an application that responds to a request like http://localhost:8000/greet/earth with "hello earth" and http://localhost:8000/greet/mars with "hello mars" and responds to http://localhost:8000 with "hello everyone"
use trillium::Conn;
use trillium_router::{Router, RouterConnExt};
pub fn main() {
trillium_smol::run(
Router::new()
.get("/", |conn: Conn| async move { conn.ok("hello everyone") })
.get("/hello/:planet", |conn: Conn| async move {
let planet = conn.param("planet").unwrap();
let response_body = format!("hello {planet}");
conn.ok(response_body)
}),
);
}
Nesting
Trillium also supports nesting routers, making it possible to express
complex sub-applications, vaguely along the lines of a rails
engine. When there are
additional types of routers, it will be possible for an application
built with one type of router to be published as a crate and nested
inside of another router as long as they depend on a compatible
version of the trillium
crate.
use trillium::{conn_try, conn_unwrap, Conn, Handler};
use trillium_logger::Logger;
use trillium_router::{Router, RouterConnExt};
struct User {
id: usize,
}
mod nested_app {
use super::*;
async fn load_user(conn: Conn) -> Conn {
let id = conn_try!(conn.param("user_id").unwrap().parse(), conn);
let user = User { id }; // imagine we were loading a user from a database here
conn.with_state(user)
}
async fn greeting(mut conn: Conn) -> Conn {
let user = conn_unwrap!(conn.take_state::<User>(), conn);
conn.ok(format!("hello user {}", user.id))
}
async fn post(mut conn: Conn) -> Conn {
let user = conn_unwrap!(conn.take_state::<User>(), conn);
let body = conn_try!(conn.request_body_string().await, conn);
conn.ok(format!("hello user {}, {}", user.id, body))
}
async fn some_other_route(conn: Conn) -> Conn {
conn.ok("this is an uninspired example")
}
pub fn handler() -> impl Handler {
(
load_user,
Router::new()
.get("/greeting", greeting)
.get("/some/other/route", some_other_route)
.post("/post", post),
)
}
}
pub fn main() {
env_logger::init();
trillium_smol::run((
Logger::new(),
Router::new()
.get("/", |conn: Conn| async move { conn.ok("hello everyone") })
.any(&["get", "post"], "/users/:user_id/*", nested_app::handler()),
));
}
Template engines
There are currently three template engines for trillium. Although they are in no way mutually exclusive, most applications will want at most one of these.
Askama
Askama is a jinja-based template engine that preprocesses templates at compile time, resulting in efficient and type-safe templates that are compiled into the application binary. Here's how it looks:
Given the following file in (cargo root)/templates/examples/hello.html
,
Hello, {{ name }}!
use trillium::Conn;
use trillium_askama::{AskamaConnExt, Template};
#[derive(Template)]
#[template(path = "examples/hello.html")]
struct HelloTemplate<'a> {
name: &'a str,
}
fn main() {
trillium_smol::run(|conn: Conn| async move { conn.render(HelloTemplate { name: "world" }) });
}
Ructe
Ructe is a compile-time typed template system similar to askama, but using a build script instead of macros.
- crate: https://crates.io/crates/trillium-ructe
- repository: https://github.com/prabirshrestha/trillium-ructe
- docs: https://docs.rs/trillium-ructe/latest/trillium_ructe/
Tera
Tera offers runtime templating. Trillium's tera integration provides an interface very similar to phoenix
or rails
, with the notion of assigns
being set on the conn prior to render.
Given the following file in the same directory as main.rs (examples in this case),
Hello, {{ name }}!
use trillium::Conn;
use trillium_tera::{TeraConnExt, TeraHandler};
fn main() {
trillium_smol::run((TeraHandler::new("**/*.html"), |conn: Conn| async move {
conn.assign("name", "hi").render("examples/hello.html")
}));
}
Handlebars
Handlebars also offers runtime templating. Given the following file in examples/templates/hello.hbs
,
hello {{name}}!
use trillium::Conn;
use trillium_handlebars::{HandlebarsConnExt, HandlebarsHandler};
fn main() {
env_logger::init();
trillium_smol::run((
HandlebarsHandler::new("./examples/templates/*.hbs"),
|conn: Conn| async move {
conn.assign("name", "world")
.render("examples/templates/hello.hbs")
},
));
}
Logger
use trillium::{Conn, State};
use trillium_logger::{apache_combined, Logger};
#[derive(Clone, Copy)]
struct User(&'static str);
impl User {
pub fn name(&self) -> &'static str {
self.0
}
}
fn user_id(conn: &Conn, _color: bool) -> &'static str {
conn.state::<User>().map(User::name).unwrap_or("-")
}
pub fn main() {
trillium_smol::run((
State::new(User("jacob")),
Logger::new().with_formatter(apache_combined("-", user_id)),
"ok",
));
}
Cookies
use trillium::Conn;
use trillium_cookies::{CookiesConnExt, CookiesHandler};
pub fn main() {
env_logger::init();
trillium_smol::run((CookiesHandler::new(), |conn: Conn| async move {
if let Some(cookie_value) = conn.cookies().get("some_cookie") {
println!("current cookie value: {}", cookie_value.value());
}
conn.with_cookie(("some_cookie", "some-cookie-value"))
.ok("ok!")
}));
}
Sessions
Sessions are a common convention in web frameworks, allowing for a
safe and secure way to associate server-side data with a given http
client (browser). Trillium's session storage is built on the
async-session
crate, which allows us to share session stores with
tide. Currently, these session stores exist:
- MemoryStore (reexported as trillium_sessions::MemoryStore) 1
- CookieStore (reexported as trillium_sessions::CookieStore) 1
- PostgresSessionStore and SqliteSessionStore from async-sqlx-session
- RedisSessionStore from async-redis-session
- MongodbSessionStore from async-mongodb-session
The memory store and cookie store should be avoided for use in production applications. The memory store will lose all session state on server process restart, and the cookie store makes different security tradeoffs than the database-backed stores. If possible, use a database.
βThe session handler must be used in conjunction with the cookie handler, and it must run after the cookie handler. This particular interaction is also present in other frameworks, and is due to the fact that regardless of which session store is used, sessions use a secure cookie as a unique identifier.
use trillium::Conn;
use trillium_cookies::CookiesHandler;
use trillium_sessions::{MemoryStore, SessionConnExt, SessionHandler};
pub fn main() {
env_logger::init();
trillium_smol::run((
CookiesHandler::new(),
SessionHandler::new(MemoryStore::new(), "01234567890123456789012345678901123"),
|conn: Conn| async move {
let count: usize = conn.session().get("count").unwrap_or_default();
conn.with_session("count", count + 1)
.ok(format!("count: {count}"))
},
));
}
Proxy
Trillium includes a custom http client implementation in order to support reverse proxying requests. There are two tls implementations for this client.
use trillium_client::Client;
use trillium_logger::Logger;
use trillium_proxy::{
upstream::{ConnectionCounting, IntoUpstreamSelector, UpstreamSelector},
Proxy,
};
use trillium_smol::ClientConfig;
pub fn main() {
env_logger::init();
let upstream = if std::env::args().count() == 1 {
"http://localhost:8080".into_upstream().boxed()
} else {
std::env::args()
.skip(1)
.collect::<ConnectionCounting<_>>()
.boxed()
};
trillium_smol::run((
Logger::new(),
Proxy::new(
Client::new(ClientConfig::default()).with_default_pool(),
upstream,
)
.with_via_pseudonym("trillium-proxy"),
));
}
Static file serving
Trillium offers two rudimentary approaches to static file serving for now. Neither of these approaches perform any cache-related header checking yet.
From disk
This handler loads content from disk at request, and does not yet do any in-memory caching.
#[cfg(unix)]
pub fn main() {
use trillium_static::{crate_relative_path, files};
trillium_smol::run((
trillium_logger::logger(),
files(crate_relative_path!("examples/files")).with_index_file("index.html"),
))
}
#[cfg(not(unix))]
pub fn main() {}
From memory, at compile time
This handler includes all of the static content in the compiled binary, allowing it to be shipped independently from the assets.
#[cfg(unix)]
pub fn main() {
use trillium_static_compiled::static_compiled;
trillium_smol::run((
trillium_logger::Logger::new(),
trillium_caching_headers::CachingHeaders::new(),
static_compiled!("./examples/files").with_index_file("index.html"),
));
}
#[cfg(not(unix))]
pub fn main() {}
WebSocket support
use futures_util::StreamExt;
use trillium_logger::logger;
use trillium_websockets::{websocket, Message, WebSocketConn};
async fn websocket_handler(mut conn: WebSocketConn) {
while let Some(Ok(Message::Text(input))) = conn.next().await {
let result = conn
.send_string(format!("received your message: {}", &input))
.await;
if let Err(e) = result {
log::error!("{e}");
break;
}
}
}
pub fn main() {
env_logger::init();
trillium_smol::run((logger(), websocket(websocket_handler)));
}
π WebSockets work a lot like tide's, since I recently wrote that interface as well. One difference in trillium is that the websocket connection also contains some aspects of the original http request, such as request headers, the request path and method, and any state that has been accumulated by previous handlers in a sequence.
Testing trillium applications
Trillium provides a testing crate that intends to provide both "functional/unit testing" and "integration testing" of trillium applications.
Given a totally-contrived application like this:
use trillium::{conn_try, Conn, Handler, KnownHeaderName};
use trillium_logger::Logger;
async fn teapot(mut conn: Conn) -> Conn {
let request_body = conn_try!(conn.request_body_string().await, conn);
if request_body.is_empty() {
conn.with_status(406).with_body("unacceptable!").halt()
} else {
conn.with_body(format!("request body was: {request_body}"))
.with_status(418)
.with_response_header(KnownHeaderName::Server, "zojirushi")
}
}
fn application() -> impl Handler {
(Logger::new(), teapot)
}
fn main() {
trillium_smol::run(application());
}
Here's what some simple tests would look like:
#[cfg(test)]
mod tests {
use super::{application, teapot};
use trillium_testing::prelude::*;
#[test]
fn handler_sends_correct_headers_and_is_a_teapot() {
let application = application();
assert_response!(
post("/").with_request_body("hello trillium!").on(&application),
Status::ImATeapot,
"request body was: hello trillium!",
"server" => "zojirushi",
"content-length" => "33"
);
}
#[test]
fn we_can_also_test_the_individual_handler() {
assert_body!(
post("/").with_request_body("a different body").on(&teapot),
"request body was: a different body"
);
}
#[test]
fn application_is_lemongrab_when_body_is_empty() {
let application = application();
assert_response!(
post("/").on(&application),
Status::NotAcceptable,
"unacceptable!"
);
}
}
Patterns for library authors
State
Let's take a look at an implementation of a library that incrementally counts the number of conns that pass through it and attaches the number to each conn. It would be unsafe to store a u64 directly in the state set, because other libraries might be doing so, so we wrap it with a private newtype called ConnNumber. Since this isn't accessible outside of our library, we can be sure that our handler is the only place that sets it. We provide a ConnExt trait in order to provide access to this data.
mod conn_counter {
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use trillium::{async_trait, Conn, Handler};
struct ConnNumber(u64);
#[derive(Default)]
pub struct ConnCounterHandler(Arc<AtomicU64>);
impl ConnCounterHandler {
pub fn new() -> Self {
Self::default()
}
}
#[async_trait]
impl Handler for ConnCounterHandler {
async fn run(&self, conn: Conn) -> Conn {
let number = self.0.fetch_add(1, Ordering::SeqCst);
conn.with_state(ConnNumber(number))
}
}
pub trait ConnCounterConnExt {
fn conn_number(&self) -> u64;
}
impl ConnCounterConnExt for Conn {
fn conn_number(&self) -> u64 {
self.state::<ConnNumber>()
.expect("conn_number must be called after the handler")
.0
}
}
}
And usage of the library looks like this:
use conn_counter::{ConnCounterConnExt, ConnCounterHandler};
use trillium::{Conn, Handler};
fn handler() -> impl Handler {
(ConnCounterHandler::new(), |conn: Conn| async move {
let conn_number = conn.conn_number();
conn.ok(format!("conn number was {conn_number}"))
})
}
fn main() {
trillium_smol::run(handler());
}
#[cfg(test)]
mod test {
use trillium_testing::prelude::*;
#[test]
fn test_conn_counter() {
let handler = super::handler();
assert_ok!(get("/").on(&handler), "conn number was 0");
assert_ok!(get("/").on(&handler), "conn number was 1");
assert_ok!(get("/").on(&handler), "conn number was 2");
assert_ok!(get("/").on(&handler), "conn number was 3");
}
}
Contributing
If you're excited about the ideas here, this page will be kept up to date with ways to get involved.
- Build stuff using trillium. Open source if possible, but even if not, feedback from actual deployed applications will be given higher priority than issues that aren't driven by real use cases.
- Build new handlers for trillium. The intent of trillium's design is that as many components as possible should be replaceable. It would make me very happy to deprecate one of the components I built in preference to a more robust alternative. All trillium-compatible crates will be linked in a section of the documentation.
- Contribute to the documentation and tests
- File issues for bugs
βPlease don't file a Pull Request, regardless of how small, without prior discussion on an Issue. All PRs without an associated issue will be immediately closed. I value your time and want to make sure that any code you write is in an aligned direction.