# Zeke
A set of simple http primitives used to build web services, written in Rust.
## Quickstart
### Installation
In your `cargo.toml`:
```toml
[dependencies]
zeke = '0.1.3'
```
### Create a Router
Routers are used to define and serve our http endpoints.
```rs
#[tokio::main]
async fn main() {
let r = Router::new();
}
```
### Create a Handler
Any function that returns a Handler can be associated with an endpoint:
```rs
#[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`:
```rs
#[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:
```rs
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`.
```rs
#[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:
```rs
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.
1. We initalize our trace type and then encode it into json:
```rs
let trace = HttpTrace{
time_stamp: chrono::Utc::now().to_rfc3339(),
};
let trace_encoded = serde_json::to_string(&trace);
```
2. We ensure the trace has been encoded correctly:
```rs
if trace_encoded.is_err() {
return Some(Response::new()
.status(500)
.body("failed to encode trace")
);
}
```
3. Finally (and most importantly) we call set_context on our `Request` type, using our AppContext::Trace key
```rs
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:
```rs
#[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.
```rs
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.
1. We use our `AppContext::Trace` key to get the encoded `HttpTrace` using `request.get_context`.
```rs
let trace = request.get_context(AppContext::Trace);
```
2. We ensure the trace exists:
```rs
if trace == "" {
return Some(Response::new()
.status(500)
.body("failed to get trace")
);
}
```
3. We decode the `HttpTrace`:
```rs
let trace: HttpTrace = serde_json::from_str(&trace).unwrap();
```
4. Finally, we calculate the elapsed time and log results to the terminal:
```rs
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:
```rs
#[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:
```rs
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:
```rs
#[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);
}
}
```