use zenith_api::{CanonicalRequest, CanonicalResponse};
use crate::error::WebError;
use crate::extract::ExtractError;
use crate::middleware::{Middleware, MiddlewareChain};
use crate::router::{RouteEntry, Router, RouteMatch, RouteMethod};
pub type HandlerFn =
dyn Fn(&CanonicalRequest, &RouteMatch) -> Result<CanonicalResponse, WebError>
+ Send
+ Sync
+ 'static;
pub struct HandlerEntry {
id: usize,
handler: Box<HandlerFn>,
}
impl std::fmt::Debug for HandlerEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HandlerEntry")
.field("id", &self.id)
.finish()
}
}
#[derive(Default)]
pub struct App {
router: Router,
handlers: Vec<HandlerEntry>,
middleware_chain: MiddlewareChain,
next_handler_id: usize,
}
impl App {
pub fn new() -> Self {
Self {
router: Router::new(),
handlers: Vec::new(),
middleware_chain: MiddlewareChain::new(),
next_handler_id: 0,
}
}
pub fn get<F>(&mut self, path: &str, handler: F) -> &mut Self
where
F: Fn(&CanonicalRequest, &RouteMatch) -> Result<CanonicalResponse, WebError>
+ Send
+ Sync
+ 'static,
{
self.add_route(RouteMethod::Get, path, handler)
}
pub fn post<F>(&mut self, path: &str, handler: F) -> &mut Self
where
F: Fn(&CanonicalRequest, &RouteMatch) -> Result<CanonicalResponse, WebError>
+ Send
+ Sync
+ 'static,
{
self.add_route(RouteMethod::Post, path, handler)
}
pub fn put<F>(&mut self, path: &str, handler: F) -> &mut Self
where
F: Fn(&CanonicalRequest, &RouteMatch) -> Result<CanonicalResponse, WebError>
+ Send
+ Sync
+ 'static,
{
self.add_route(RouteMethod::Put, path, handler)
}
pub fn delete<F>(&mut self, path: &str, handler: F) -> &mut Self
where
F: Fn(&CanonicalRequest, &RouteMatch) -> Result<CanonicalResponse, WebError>
+ Send
+ Sync
+ 'static,
{
self.add_route(RouteMethod::Delete, path, handler)
}
pub fn patch<F>(&mut self, path: &str, handler: F) -> &mut Self
where
F: Fn(&CanonicalRequest, &RouteMatch) -> Result<CanonicalResponse, WebError>
+ Send
+ Sync
+ 'static,
{
self.add_route(RouteMethod::Patch, path, handler)
}
pub fn any<F>(&mut self, path: &str, handler: F) -> &mut Self
where
F: Fn(&CanonicalRequest, &RouteMatch) -> Result<CanonicalResponse, WebError>
+ Send
+ Sync
+ 'static,
{
self.add_route(RouteMethod::Any, path, handler)
}
fn add_route<F>(&mut self, method: RouteMethod, path: &str, handler: F) -> &mut Self
where
F: Fn(&CanonicalRequest, &RouteMatch) -> Result<CanonicalResponse, WebError>
+ Send
+ Sync
+ 'static,
{
let id = self.next_handler_id;
self.next_handler_id += 1;
self.handlers.push(HandlerEntry {
id,
handler: Box::new(handler),
});
self.router.add_route(method, path, id);
self
}
pub fn middleware<M: Middleware>(&mut self, middleware: M) -> &mut Self {
self.middleware_chain.add(middleware);
self
}
pub fn handle(&self, request: CanonicalRequest) -> CanonicalResponse {
let method = request.method;
self.middleware_chain.run(request, |req| {
let path = req.path_bytes();
let route_match = match self.router.match_route(method, path) {
Some(m) => m,
None => {
if self.router.path_exists(path) {
return WebError::MethodNotAllowed(format!(
"Method {} not allowed for {}",
method.as_str(),
String::from_utf8_lossy(path)
))
.into_response();
}
return WebError::NotFound(format!(
"Route not found: {} {}",
method.as_str(),
String::from_utf8_lossy(path)
))
.into_response();
}
};
let handler = &self.handlers[route_match.handler_id];
crate::error::catch_panic(|| {
match (handler.handler)(req, &route_match) {
Ok(response) => response,
Err(web_error) => web_error.into_response(),
}
})
})
}
pub fn route_count(&self) -> usize {
self.router.route_count()
}
pub fn routes(&self) -> &[RouteEntry] {
self.router.routes()
}
pub fn validate(&self) -> Result<(), crate::error::RouterError> {
self.router.validate()
}
}
impl std::fmt::Debug for App {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("App")
.field("routes", &self.router.route_count())
.field("handlers", &self.handlers.len())
.field("middleware_count", &self.middleware_chain.len())
.finish()
}
}
pub fn success_response(body: impl Into<Vec<u8>>, content_type: &str) -> CanonicalResponse {
let mut response = CanonicalResponse::new(200);
let _ = response
.add_header(b"content-type", content_type.as_bytes());
response.set_body(body.into());
response
}
pub fn json_response<T: serde::Serialize>(value: &T) -> Result<CanonicalResponse, WebError> {
let json = serde_json::to_string(value)
.map_err(|e| WebError::InternalError(format!("JSON serialize error: {}", e)))?;
Ok(success_response(json, "application/json"))
}
impl From<ExtractError> for WebError {
fn from(err: ExtractError) -> Self {
match err {
ExtractError::NotFound(name) => WebError::BadRequest(format!("Missing parameter: {}", name)),
ExtractError::ParseError { name, expected } => {
WebError::BadRequest(format!("Invalid '{}', expected {}", name, expected))
}
ExtractError::OutOfRange { name, value } => {
WebError::BadRequest(format!("'{}' value '{}' out of range", name, value))
}
ExtractError::Custom(msg) => WebError::BadRequest(msg),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use zenith_api::Method;
#[test]
fn test_app_creation() {
let app = App::new();
assert_eq!(app.route_count(), 0);
}
#[test]
fn test_get_route() {
let mut app = App::new();
app.get("/hello", |_req, _params| {
Ok(success_response("Hello World", "text/plain"))
});
assert_eq!(app.route_count(), 1);
let mut request = CanonicalRequest::empty();
let _ = request.set_path("/hello");
let response = app.handle(request);
assert_eq!(response.status_code, 200);
assert_eq!(response.body(), b"Hello World");
}
#[test]
fn test_post_route() {
let mut app = App::new();
app.post("/data", |_req, _params| {
Ok(success_response("Created", "text/plain"))
});
let mut request = CanonicalRequest::empty();
request.method = Method::Post;
let _ = request.set_path("/data");
let response = app.handle(request);
assert_eq!(response.status_code, 200);
}
#[test]
fn test_param_route() {
let mut app = App::new();
app.get("/users/:id", |_req, params| {
let id = params.get("id").unwrap_or_default();
Ok(success_response(format!("User: {}", id), "text/plain"))
});
let mut request = CanonicalRequest::empty();
let _ = request.set_path("/users/42");
let response = app.handle(request);
assert_eq!(response.status_code, 200);
}
#[test]
fn test_not_found() {
let mut app = App::new();
app.get("/exists", |_req, _params| {
Ok(success_response("OK", "text/plain"))
});
let mut request = CanonicalRequest::empty();
let _ = request.set_path("/nonexistent");
let response = app.handle(request);
assert_eq!(response.status_code, 404);
}
#[test]
fn test_method_not_allowed() {
let mut app = App::new();
app.get("/api", |_req, _params| {
Ok(success_response("GET OK", "text/plain"))
});
let mut request = CanonicalRequest::empty();
request.method = Method::Post;
let _ = request.set_path("/api");
let response = app.handle(request);
assert_eq!(response.status_code, 405);
}
#[test]
fn test_middleware_integration() {
use crate::middleware::LoggingMiddleware;
let mut app = App::new();
app.middleware(LoggingMiddleware::new(false));
app.get("/test", |_req, _params| {
Ok(success_response("Test", "text/plain"))
});
let mut request = CanonicalRequest::empty();
let _ = request.set_path("/test");
let response = app.handle(request);
assert_eq!(response.status_code, 200);
}
#[test]
fn test_options_preflight_transparent_short_circuit() {
use crate::middleware::{CorsMiddleware, RequestIdMiddleware};
let mut app = App::new();
app.middleware(CorsMiddleware::new().with_origin("*"));
app.middleware(RequestIdMiddleware::new());
app.get("/api/data", |_req, _params| {
Ok(success_response("OK", "text/plain"))
});
let mut request = CanonicalRequest::empty();
request.method = Method::Options;
let _ = request.set_path("/api/data");
let response = app.handle(request);
assert_eq!(response.status_code, 204);
assert!(response.find_header("access-control-allow-origin").is_some());
assert!(response.find_header("access-control-allow-methods").is_some());
assert!(response.find_header("access-control-allow-headers").is_some());
assert!(response.find_header("access-control-max-age").is_some());
assert!(response.find_header("x-request-id").is_some());
}
#[test]
fn test_options_preflight_unregistered_path() {
use crate::middleware::CorsMiddleware;
let mut app = App::new();
app.middleware(CorsMiddleware::new().with_origin("*"));
app.get("/api/data", |_req, _params| {
Ok(success_response("OK", "text/plain"))
});
let mut request = CanonicalRequest::empty();
request.method = Method::Options;
let _ = request.set_path("/totally/unregistered/path");
let response = app.handle(request);
assert_eq!(response.status_code, 204);
assert!(response.find_header("access-control-allow-origin").is_some());
}
#[test]
fn test_not_found_still_runs_after_middleware() {
use crate::middleware::RequestIdMiddleware;
let mut app = App::new();
app.middleware(RequestIdMiddleware::new());
app.get("/exists", |_req, _params| {
Ok(success_response("OK", "text/plain"))
});
let mut request = CanonicalRequest::empty();
let _ = request.set_path("/nonexistent");
let response = app.handle(request);
assert_eq!(response.status_code, 404);
assert!(response.find_header("x-request-id").is_some());
}
#[test]
fn test_error_handler() {
let mut app = App::new();
app.get("/error", |_req, _params| {
Err(WebError::InternalError("Something went wrong".to_string()))
});
let mut request = CanonicalRequest::empty();
let _ = request.set_path("/error");
let response = app.handle(request);
assert_eq!(response.status_code, 500);
let body = response.body();
let body_str = String::from_utf8_lossy(body);
assert!(body_str.contains("Something went wrong"));
}
#[test]
fn test_multiple_routes() {
let mut app = App::new();
app.get("/", |_req, _params| {
Ok(success_response("Root", "text/plain"))
});
app.get("/api/health", |_req, _params| {
Ok(success_response("OK", "text/plain"))
});
app.post("/api/data", |_req, _params| {
Ok(success_response("Created", "text/plain"))
});
assert_eq!(app.route_count(), 3);
assert!(app.validate().is_ok());
}
#[test]
fn test_success_response() {
let response = success_response(b"test data".to_vec(), "application/octet-stream");
assert_eq!(response.status_code, 200);
assert_eq!(response.body(), b"test data");
assert_eq!(
response.find_header("content-type").unwrap().value_str(),
"application/octet-stream"
);
}
#[test]
fn test_extract_error_to_web_error() {
let extract_err = crate::extract::ExtractError::NotFound("id".to_string());
let web_err: WebError = extract_err.into();
assert_eq!(web_err.status_code(), 400);
assert!(web_err.message().contains("id"));
}
#[test]
fn test_app_debug() {
let mut app = App::new();
app.get("/test", |_req, _params| {
Ok(success_response("OK", "text/plain"))
});
let debug = format!("{:?}", app);
assert!(debug.contains("App"));
}
}