Zeke
Simple HTTP Library
Zeke is a HTTP library built on top of Tokio. Zeke values simplicitiy and minimalism.
Quickstart
This quickstart will show you how to:
- Create a Router
- Add New Routes
- Create Middleware
- Apply Middleware to Routes
- Share Context Between Middlware and Handlers
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use zeke::http::{
context::{get_context, set_context, Contextable},
handler::Handler,
middleware::{Middleware, MiddlewareGroup},
response::{new_response, set_header},
router::{Route, Router},
};
#[tokio::main]
async fn main() {
let mut r = Router::new();
pub fn handle_home() -> Handler {
return Handler::new(|request| {
let response = new_response(200, "<h1>Home</h1><a href='/about'>About</a>");
let response = set_header(response, "Content-Type", "text/html");
return (request, response);
});
}
pub fn handle_about() -> Handler {
return Handler::new(|request| {
let response = new_response(200, "<h1>About</h1><a href='/'>Home</a>");
let response = set_header(response, "Content-Type", "text/html");
return (request, response);
});
}
pub enum AppContext {
Trace,
}
impl Contextable for AppContext {
fn key(&self) -> &'static str {
match self {
AppContext::Trace => {"TRACE"},
}
}
}
pub fn mw_trace() -> Middleware {
return Middleware::new(|request| {
let trace = HttpTrace{
time_stamp: chrono::Utc::now().to_rfc3339(),
};
let trace_encoded = serde_json::to_string(&trace);
match trace_encoded {
Ok(trace_encoded) => {
set_context(request, AppContext::Trace, trace_encoded);
return None;
},
Err(_) => {
return Some(new_response(500, "failed to encode trace"));
}
}
});
}
pub fn mw_trace_log() -> Middleware {
return Middleware::new(|request| {
let trace = get_context(&request.context, AppContext::Trace);
if trace == "" {
return Some(new_response(500, "trace not found"));
}
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);
return None;
});
}
pub fn mw_group_trace() -> MiddlewareGroup {
return MiddlewareGroup::new(vec![mw_trace()], vec![mw_trace_log()]);
}
#[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();
}
}
}
r.add(Route::new("GET /", handle_home())
.middleware(mw_trace())
.outerware(mw_trace_log())
);
r.add(Route::new("GET /about", handle_about())
.group(mw_group_trace())
);
let err = r.serve("127.0.0.1:8080").await;
match err {
Some(e) => {
println!("Error: {:?}", e);
},
None => {
println!("Server closed");
},
}
}
Features
Router
Initialize a server by creating a Router:
let mut r: Router = Router::new();
Handlers
Handlers are functions that return a Handler:
pub fn handle_home() -> Handler {
return Handler::new(|request| {
let response = new_response(200, "<h1>Home</h1>");
let response = set_header(response, "Content-Type", "text/html");
return (request, response);
});
}
Routes
Routes are added to the Router:
r.add(Route::new("GET /", handle_home()))
Middleware
Middleware is any function that returns a Middleware:
pub fn mw_print_name() -> Middleware {
return Middleware::new(|request| {
let name = "Zeke";
println!("My name is {}", name);
return None;
})
}
pub fn mw_print_color() -> Middleware {
return Middleware::new(|request| {
let name = "red";
println!("My favorite color is {}", name);
return None;
})
}
Middleware can be chained:
r.add(Route::new("GET /name-and-color", handle_hello_world())
.middleware(mw_print_name(), mw_print_color())
);
Middleware can be grouped:
pub fn mw_group_name_and_color() -> MiddlewareGroup {
return MiddlewareGroup::new(vec![mw_print_name()], vec![mw_print_color()]);
}
r.add(Route::new("GET /name-and-color-group", handle_hello_world())
.group(mw_group_name_and_color())
);
If a Middlware returns a Response, the request cycle will end and the Response will be returned:
pub fn mw_stop_execution() -> Middleware {
return Middleware::new(|request| {
println!("response returned, stopping execution...");
return Some(new_response(500, "stopping execution!"));
})
}
r.add(Route::new("GET /stop", handle_hello_world())
.middleware(mw_print_name(), mw_print_color())
);
Outerware
Outerware are Middleware to be ran after processing a request:
r.add(Route::new("GET /name-then-color", handle_hello_world())
.middleware(mw_print_name())
.outerware(mw_print_color()) );
Shared State
Define a Shared Type
Start by defining a type we intend to share between middleware, handlers, and outerware. Here, we define HttpTrace which will be used to log out request details after a request is processed.
NOTE: Any type we intend to share must derive Serialize and Deserialize from serde. I am using version serde = { version = "1.0.200", features = ["derive"] } in my cargo.toml.
use serde::{Deserialize, Serialize};
#[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();
}
}
}
Define Your Context Keys
Context keys are used to encode and decode our shared types between each part of the request/response cycle.
Start by defining an enum containing our context keys:
pub enum AppContext {
Trace
}
Implement the Contextable trait on our AppContext and list our keys:
impl Contextable for AppContext {
fn key(&self) -> &'static str {
match self {
AppContext::Trace => {"TRACE"},
}
}
}
Encoding a Shared Type
Using AppContext, we can encode our shared types. Here we create a middleware which uses our HttpTrace type to track when we start processing our request:
pub fn mw_trace() -> Middleware {
return Middleware::new(|request| {
let trace = HttpTrace{
time_stamp: chrono::Utc::now().to_rfc3339(),
};
let trace_encoded = serde_json::to_string(&trace);
match trace_encoded {
Ok(trace_encoded) => {
set_context(request, AppContext::Trace, trace_encoded);
return None;
},
Err(_) => {
return Some(new_response(500, "failed to encode trace"));
}
}
});
}
Decoding a Shared Type
Using AppContext, we can decode our shared types. Here, we create a middleware which will decode our HttpTrace and log out all the request details, including how long it took the request to process:
pub fn mw_trace_log() -> Middleware {
return Middleware::new(|request| {
let trace = get_context(&request.context, AppContext::Trace);
if trace == "" {
return Some(new_response(500, "trace not found"));
}
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);
return None;
});
}