7 releases
0.1.6 | May 30, 2024 |
---|---|
0.1.5 | May 30, 2024 |
#222 in HTTP server
55KB
1.5K
SLoC
Zeke
A set of simple http primitives used to build web services, written in Rust.
Quickstart
Installation
In your cargo.toml
:
[dependencies]
zeke = '0.1.3'
Create a Router
Routers are used to define and serve our http endpoints.
#[tokio::main]
async fn main() {
let r = Router::new();
}
Create a Handler
Any function that returns a Handler can be associated with an endpoint:
#[tokio::main]
async fn main() {
let r = Router::new();
r.add(Route::new("GET /", hello_world()));
}
async fn hello_world() -> Handler {
return Handler::new(|request| {
// enables our handlers to by async
Box::pin(async move {
let response = Response::new()
.status(200);
return (request, response);
})
});
}
Serving
To serve the application, called Router.serve
:
#[tokio::main]
async fn main() {
// --snip
let result = r.serve(&host).await;
if result.is_err() {
println!("Error: {:?}", err);
}
}
Context Keys
Any data shared between middleware, handlers, and outerware is referred to as context
.
Keys are required to encode and decode context. An enum which implements the Contextable
trait can be used to keep track of these keys:
pub enum AppContext {
Trace,
}
impl Contextable for AppContext {
fn key(&self) -> &'static str {
match self {
AppContext::Trace => {"TRACE"},
}
}
}
HttpTrace
HttpTrace is a context
(because it is intended to be shared between middleware, handlers, and outware) that helps us keep track of how long each request cycle takes.
You must derive Serialize
and Deserialize
for any data intended to be used as context
.
#[derive(Debug, Serialize, Deserialize)]
pub struct HttpTrace {
pub time_stamp: String,
}
impl HttpTrace {
pub fn get_time_elapsed(&self) -> String {
if let Ok(time_set) = DateTime::parse_from_rfc3339(&self.time_stamp) {
let time_set = time_set.with_timezone(&Utc);
let now = Utc::now();
let duration = now.signed_duration_since(time_set);
let micros = duration.num_microseconds();
match micros {
Some(micros) => {
if micros < 1000 {
return format!("{}µ", micros);
}
},
None => {
}
}
let millis = duration.num_milliseconds();
return format!("{}ms", millis);
} else {
return "failed to parse time_stamp".to_string();
}
}
}
Middleware
Any function that returns a Middleware
can be used as middleware in our application.
Let's make use of the HttpTrace
type we created in the previous section.
The following middleware will initialize HttpTrace
prior to calling our handler:
pub async fn mw_trace() -> Middleware {
Middleware::new(|mut request: &mut Request| {
let trace = HttpTrace {
time_stamp: chrono::Utc::now().to_rfc3339(),
};
let trace_encoded = serde_json::to_string(&trace);
if trace_encoded.is_err() {
return Some(Response::new()
.status(500)
.body("failed to encode trace")
);
}
let trace_encoded = trace_encoded.unwrap();
request.set_context(AppContext::Trace, trace_encoded);
None
})
}
Let's take a moment to notice a few key things going on here.
- We initalize our trace type and then encode it into json:
let trace = HttpTrace{
time_stamp: chrono::Utc::now().to_rfc3339(),
};
let trace_encoded = serde_json::to_string(&trace);
- We ensure the trace has been encoded correctly:
if trace_encoded.is_err() {
return Some(Response::new()
.status(500)
.body("failed to encode trace")
);
}
- Finally (and most importantly) we call set_context on our
Request
type, using our AppContext::Trace key
let trace_encoded = trace_encoded.unwrap();
request.set_context(AppContext::Trace, trace_encoded);
Now the json data for the HttpTrace
type is associated with the Request
type and can be used later in the request cycle.
We can attach our middleware to a Route
like so:
#[tokio::main]
async fn main() {
let r = Router::new();
r.add(Route::new("GET /", hello_world())
.middleware(mw_trace())
);
let result = r.serve(&host).await;
if result.is_err() {
println!("Error: {:?}", err);
}
}
Outerware
Any function that returns a Middleware
can be used as outerware in our application.
Middleware is ran before the handler is called.
Outerware is ran after the handler is called.
We can create an outerware to decode our HttpTrace
type after the request cycle is over. We can then calculate how much time it took the entire request to process and print it to the terminal.
pub async fn mw_trace_log() -> Middleware {
Middleware::new(|request: &mut Request | {
let trace = request.get_context(AppContext::Trace);
if trace.is_empty() {
return Some(Response::new()
.status(500)
.body("failed to get trace")
);
}
let trace: HttpTrace = serde_json::from_str(&trace).unwrap();
let elapsed_time = trace.get_time_elapsed();
let log_message = format!("[{:?}][{}][{}]", request.method, request.path, elapsed_time);
println!("{}", log_message);
None
})
}
Let's take a closer look at a few things.
- We use our
AppContext::Trace
key to get the encodedHttpTrace
usingrequest.get_context
.
let trace = request.get_context(AppContext::Trace);
- We ensure the trace exists:
if trace == "" {
return Some(Response::new()
.status(500)
.body("failed to get trace")
);
}
- We decode the
HttpTrace
:
let trace: HttpTrace = serde_json::from_str(&trace).unwrap();
- Finally, we calculate the elapsed time and log results to the terminal:
let elapsed_time = trace.get_time_elapsed();
let log_message = format!("[{:?}][{}][{}]", request.method, request.path, elapsed_time);
println!("{}", log_message);
We can use this outerware in our application like so:
#[tokio::main]
async fn main() {
let r = Router::new();
r.add(Route::new("GET /", hello_world())
.middleware(mw_trace().await)
.outerware(mw_trace_log().await)
);
let result = r.serve(&host).await;
if result.is_err() {
println!("Error: {:?}", err);
}
}
Middleware Groups
Any function that returns a MiddlewareGroup
can be used as a middleware group in our application.
Middleware groups enable us to group middleware together. Let's see if we can group our mw_trace
and mw_trace_log
functions together:
pub fn mw_group_trace() -> MiddlewareGroup {
return MiddlewareGroup::new(vec![mw_trace().await], vec![mw_trace_log().await]);
}
Now we can simply use the group:
#[tokio::main]
async fn main() {
let r = Router::new();
r.add(Route::new("GET /", hello_world())
.group(mw_group_trace().await)
);
let result = r.serve(&host).await;
if result.is_err() {
println!("Error: {:?}", err);
}
}
Dependencies
~9–20MB
~294K SLoC