use crate::call::Call;
use crate::pipeline::{Endpoint, Middleware, Next, Phase};
use crate::response::Response;
use crate::router::{Match, RouteBuilder, Router};
use crate::state::StateMap;
use bytes::Bytes;
use futures_util::FutureExt;
use http::header::ALLOW;
use http::{HeaderMap, HeaderValue, Method, StatusCode, Uri};
use std::collections::VecDeque;
use std::sync::Arc;
pub trait Plugin {
fn install(self: Box<Self>, app: &mut AppBuilder);
}
#[derive(Clone, Debug)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
pub max_body_bytes: usize,
pub request_timeout_ms: u64,
pub tls: Option<crate::config::TlsSection>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".into(),
port: 8080,
max_body_bytes: 1 << 20,
request_timeout_ms: 30_000,
tls: None,
}
}
}
pub struct AppBuilder {
router: Router,
middleware: Vec<(Phase, Arc<dyn Middleware>)>,
config: ServerConfig,
state: StateMap,
}
impl AppBuilder {
fn new() -> Self {
Self {
router: Router::new(),
middleware: Vec::new(),
config: ServerConfig::default(),
state: StateMap::default(),
}
}
pub fn host(mut self, host: impl Into<String>) -> Self {
self.config.host = host.into();
self
}
pub fn port(mut self, port: u16) -> Self {
self.config.port = port;
self
}
pub fn max_body_bytes(mut self, n: usize) -> Self {
self.config.max_body_bytes = n;
self
}
pub fn with_config(mut self, cfg: crate::config::Config) -> Self {
self.config.host = cfg.server.host;
self.config.port = cfg.server.port;
self.config.max_body_bytes = cfg.server.max_body_bytes;
self.config.request_timeout_ms = cfg.server.request_timeout_ms;
self.config.tls = cfg.tls;
self
}
pub fn request_timeout_ms(mut self, ms: u64) -> Self {
self.config.request_timeout_ms = ms;
self
}
pub fn tls(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
self.config.tls = Some(crate::config::TlsSection {
cert: cert_path.into(),
key: key_path.into(),
});
self
}
pub fn state<T: Send + Sync + 'static>(mut self, value: T) -> Self {
self.state.insert(value);
self
}
pub fn install<P: Plugin + 'static>(mut self, plugin: P) -> Self {
Box::new(plugin).install(&mut self);
self
}
pub fn add_middleware_in(&mut self, phase: Phase, mw: Arc<dyn Middleware>) {
self.middleware.push((phase, mw));
}
pub fn add_middleware(&mut self, mw: Arc<dyn Middleware>) {
self.add_middleware_in(Phase::Plugins, mw);
}
pub fn routing(mut self, f: impl FnOnce(&mut RouteBuilder)) -> Self {
let mut b = RouteBuilder::new(&mut self.router);
f(&mut b);
self
}
pub async fn start(self) -> std::io::Result<()> {
self.build().start().await
}
pub fn build(self) -> App {
let mut mw = self.middleware;
mw.sort_by_key(|(phase, _)| *phase); let middleware: Vec<Arc<dyn Middleware>> = mw.into_iter().map(|(_, m)| m).collect();
App {
inner: Arc::new(AppInner {
router: self.router,
middleware,
config: self.config,
state: Arc::new(self.state),
}),
}
}
}
struct AppInner {
router: Router,
middleware: Vec<Arc<dyn Middleware>>,
config: ServerConfig,
state: Arc<StateMap>,
}
#[derive(Clone)]
pub struct App {
inner: Arc<AppInner>,
}
impl App {
pub fn config(&self) -> &ServerConfig {
&self.inner.config
}
pub async fn process(
&self,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Bytes,
) -> Response {
self.process_with_extensions(method, uri, headers, body, http::Extensions::new())
.await
}
pub async fn process_with_extensions(
&self,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Bytes,
extensions: http::Extensions,
) -> Response {
let app = self.clone();
let fut = async move {
let mut call = Call::new(method, uri, headers, body);
call.seed_extensions(extensions);
call.set_state(app.inner.state.clone());
app.run_pipeline(call).await
};
match std::panic::AssertUnwindSafe(fut).catch_unwind().await {
Ok(res) => res,
Err(_) => Response::text("Internal Server Error")
.with_status(StatusCode::INTERNAL_SERVER_ERROR),
}
}
pub async fn start(self) -> std::io::Result<()> {
let addr = format!("{}:{}", self.inner.config.host, self.inner.config.port)
.parse::<std::net::SocketAddr>()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
let shutdown = async {
let _ = tokio::signal::ctrl_c().await;
};
crate::engine::serve(self, addr, shutdown).await
}
pub async fn start_with_shutdown<F>(self, shutdown: F) -> std::io::Result<()>
where
F: std::future::Future<Output = ()> + Send + 'static,
{
let addr = format!("{}:{}", self.inner.config.host, self.inner.config.port)
.parse::<std::net::SocketAddr>()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
crate::engine::serve(self, addr, shutdown).await
}
async fn run_pipeline(&self, call: Call) -> Response {
let inner = self.inner.clone();
let endpoint: Endpoint = Arc::new(move |mut call: Call| {
let inner = inner.clone();
Box::pin(async move {
let path = call.path().to_string();
let method = call.method().clone();
let mut lookup = inner.router.route(&method, &path);
let mut synthesized_head = false;
if method == Method::HEAD && !matches!(lookup, Match::Found { .. }) {
if let m @ Match::Found { .. } = inner.router.route(&Method::GET, &path) {
lookup = m;
synthesized_head = true;
}
}
if method == Method::OPTIONS && !matches!(lookup, Match::Found { .. }) {
let mut allow = inner.router.methods_for(&path);
if !allow.is_empty() {
if allow.contains(&Method::GET) && !allow.contains(&Method::HEAD) {
allow.push(Method::HEAD);
}
allow.push(Method::OPTIONS);
let value = allow
.iter()
.map(|m| m.as_str())
.collect::<Vec<_>>()
.join(", ");
return Response::new(StatusCode::NO_CONTENT).with_header(
ALLOW,
HeaderValue::from_str(&value).unwrap_or(HeaderValue::from_static("")),
);
}
}
match lookup {
Match::Found { handler, params } => {
call.set_params(params);
let res = handler.handle(call).await;
if synthesized_head {
strip_body(res)
} else {
res
}
}
Match::MethodNotAllowed { allow } => {
let value = allow
.iter()
.map(|m| m.as_str())
.collect::<Vec<_>>()
.join(", ");
Response::new(StatusCode::METHOD_NOT_ALLOWED).with_header(
ALLOW,
HeaderValue::from_str(&value).unwrap_or(HeaderValue::from_static("")),
)
}
Match::NotFound => {
Response::text("Not Found").with_status(StatusCode::NOT_FOUND)
}
Match::BadPath => {
Response::text("Bad Request").with_status(StatusCode::BAD_REQUEST)
}
}
}) as _
});
let chain: VecDeque<Arc<dyn Middleware>> = self.inner.middleware.iter().cloned().collect();
Next::new(chain, endpoint).run(call).await
}
}
fn strip_body(mut res: Response) -> Response {
if let Some(bytes) = res.body.as_bytes() {
if let Ok(value) = HeaderValue::from_str(&bytes.len().to_string()) {
res.headers.insert(http::header::CONTENT_LENGTH, value);
}
}
res.body = crate::body::Body::empty();
res
}
pub struct Churust;
impl Churust {
pub fn server() -> AppBuilder {
AppBuilder::new()
}
pub fn from_config() -> AppBuilder {
AppBuilder::new().with_config(crate::config::Config::load_default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::Next;
use async_trait::async_trait;
#[tokio::test]
async fn middleware_runs_in_phase_order() {
use crate::pipeline::{Next, Phase};
use async_trait::async_trait;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct Recorder {
log: Arc<Mutex<Vec<&'static str>>>,
tag: &'static str,
}
#[async_trait]
impl Middleware for Recorder {
async fn handle(&self, call: Call, next: Next) -> Response {
self.log.lock().unwrap().push(self.tag);
next.run(call).await
}
}
let log = Arc::new(Mutex::new(Vec::new()));
let mut builder = Churust::server();
builder.add_middleware_in(
Phase::Fallback,
Arc::new(Recorder {
log: log.clone(),
tag: "fallback",
}),
);
builder.add_middleware_in(
Phase::Setup,
Arc::new(Recorder {
log: log.clone(),
tag: "setup",
}),
);
builder.add_middleware_in(
Phase::Monitoring,
Arc::new(Recorder {
log: log.clone(),
tag: "monitoring",
}),
);
let app = builder
.routing(|r| {
r.get("/", |_c: Call| async { "ok" });
})
.build();
let _ = get(&app, "/").await;
assert_eq!(
*log.lock().unwrap(),
vec!["setup", "monitoring", "fallback"]
);
}
fn app() -> App {
Churust::server()
.routing(|r| {
r.get("/", |_c: Call| async { "home" });
r.get("/boom", |_c: Call| async {
panic!("handler exploded");
#[allow(unreachable_code)]
""
});
})
.build()
}
async fn get(app: &App, path: &str) -> Response {
app.process(
Method::GET,
path.parse::<Uri>().unwrap(),
HeaderMap::new(),
Bytes::new(),
)
.await
}
#[tokio::test]
async fn routes_to_handler() {
let res = get(&app(), "/").await;
assert_eq!(res.status, StatusCode::OK);
assert_eq!(res.body, Bytes::from("home"));
}
#[tokio::test]
async fn unknown_path_is_404() {
let res = get(&app(), "/missing").await;
assert_eq!(res.status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn panicking_handler_yields_500_not_crash() {
let res = get(&app(), "/boom").await;
assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn process_with_extensions_seeds_call() {
#[derive(Clone)]
struct Marker(u32);
let app = Churust::server()
.routing(|r| {
r.get("/", |c: Call| async move {
format!("{}", c.get::<Marker>().map(|m| m.0).unwrap_or(0))
});
})
.build();
let mut ext = http::Extensions::new();
ext.insert(Marker(7));
let res = app
.process_with_extensions(
Method::GET,
"/".parse().unwrap(),
HeaderMap::new(),
Bytes::new(),
ext,
)
.await;
assert_eq!(res.body, Bytes::from("7"));
}
#[tokio::test]
async fn state_extractor_end_to_end() {
use crate::extract::State;
#[derive(Clone)]
struct Counter(u32);
let app = Churust::server()
.state(Counter(5))
.routing(|r| {
r.get(
"/n",
|s: State<Counter>| async move { format!("n={}", s.0 .0) },
);
})
.build();
let res = get(&app, "/n").await;
assert_eq!(res.body, Bytes::from("n=5"));
}
#[tokio::test]
async fn middleware_installed_via_plugin_runs() {
struct MarkPlugin;
struct Mark;
#[async_trait]
impl Middleware for Mark {
async fn handle(&self, call: Call, next: Next) -> Response {
let mut res = next.run(call).await;
res.headers.insert(
http::header::HeaderName::from_static("x-plugin"),
HeaderValue::from_static("on"),
);
res
}
}
impl Plugin for MarkPlugin {
fn install(self: Box<Self>, app: &mut AppBuilder) {
app.add_middleware(Arc::new(Mark));
}
}
let app = Churust::server()
.install(MarkPlugin)
.routing(|r| {
r.get("/", |_c: Call| async { "ok" });
})
.build();
let res = get(&app, "/").await;
assert_eq!(res.headers.get("x-plugin").unwrap(), "on");
}
}