diff --git a/README.md b/README.md index 3628df9..2318093 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,14 @@ <img alt="Obsidian serve" src="./screenshot/serve.png" > </div> +## Get Started +```toml +[dependencies] +# add these 2 dependencies in Cargo.toml file +obsidian = "0.2.2" +tokio = "0.2.21" +``` + ## Hello World ```rust @@ -30,44 +38,32 @@ use obsidian::{context::Context, App}; #[tokio::main] async fn main() { let mut app: App = App::new(); - let addr = ([127, 0, 0, 1], 3000).into(); - app.get("/", |ctx: Context| async { ctx.build("Hello World").ok() }); - - app.listen(&addr, || { - println!("server is listening to {}", &addr); - }).await; + app.listen(3000).await; } ``` ## Hello World (with handler function) ```rust -use obsidian::{context::Context, App, ContextResult}; +use obsidian::{context::Context, handler::ContextResult, App}; async fn hello_world(ctx: Context) -> ContextResult { ctx.build("Hello World").ok() } - #[tokio::main] async fn main() { let mut app: App = App::new(); - let addr = ([127, 0, 0, 1], 3000).into(); - app.get("/", hello_world); - - app.listen(&addr, || { - println!("server is listening to {}", &addr); - }) - .await; + app.listen(3000).await; } ``` ## JSON Response ```rust -use obsidian::{context::Context, App, ContextResult}; +use obsidian::{context::Context, handler::ContextResult, App}; use serde::*; async fn get_user(ctx: Context) -> ContextResult { @@ -85,28 +81,33 @@ async fn get_user(ctx: Context) -> ContextResult { #[tokio::main] async fn main() { let mut app: App = App::new(); - let addr = ([127, 0, 0, 1], 3000).into(); - app.get("/user", get_user); - - app.listen(&addr, || { - println!("server is listening to {}", &addr); - }) - .await; + app.listen(3000).await; } ``` -## Example Files +## More Examples -Example are located in `example/main.rs`. +Examples are located in `example` folder. You can run these examples by using: +```bash +cargo run --example [name] -## Run Example +// show a list of available examples +cargo run --example -``` +// run the example cargo run --example example ``` +## App Lifecycle + + +## Contributors +<a href="https://github.com/obsidian-rs/obsidian/graphs/contributors"> + <img src="https://contributors-img.web.app/image?repo=obsidian-rs/obsidian" /> +</a> + ## Current State -NOT READY FOR PRODUCTION YET! +Under active development and **NOT READY FOR PRODUCTION YET!** diff --git a/examples/app_state.rs b/examples/app_state.rs index 9862511..500881a 100644 --- a/examples/app_state.rs +++ b/examples/app_state.rs @@ -1,4 +1,4 @@ -use obsidian::{context::Context, App, ObsidianError}; +use obsidian::{context::Context, App}; #[derive(Clone)] pub struct AppState { @@ -8,21 +8,20 @@ pub struct AppState { #[tokio::main] async fn main() { let mut app: App<AppState> = App::new(); - let addr = ([127, 0, 0, 1], 3000).into(); app.set_app_state(AppState { - db_connection_string: "localhost:1433".to_string() + db_connection_string: "localhost:1433".to_string(), }); - app.get("/", |ctx: Context| async { - let app_state = ctx.get::<AppState>().ok_or(ObsidianError::NoneError)?; - let res = Some(format!("connection string: {}", &app_state.db_connection_string)); + app.get("/", |ctx: Context| async move { + let app_state = ctx.get::<AppState>().unwrap(); + let res = Some(format!( + "connection string: {}", + &app_state.db_connection_string + )); - ctx.build(res).ok() + res }); - app.listen(&addr, || { - println!("server is listening to {}", &addr); - }) - .await; + app.listen(3000).await; } diff --git a/examples/hello.rs b/examples/hello.rs index 56d4d46..3c9f4f2 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -3,12 +3,8 @@ use obsidian::{context::Context, App}; #[tokio::main] async fn main() { let mut app: App = App::new(); - let addr = ([127, 0, 0, 1], 3000).into(); - app.get("/", |ctx: Context| async { ctx.build("Hello World").ok() }); + app.get("/", |_ctx: Context| async { "Hello, Obsidian!" }); - app.listen(&addr, || { - println!("server is listening to {}", &addr); - }) - .await; + app.listen(3000).await; } diff --git a/examples/hello_handler.rs b/examples/hello_handler.rs index d83de9a..18ac958 100644 --- a/examples/hello_handler.rs +++ b/examples/hello_handler.rs @@ -1,5 +1,6 @@ -use obsidian::{context::Context, App, ContextResult}; +use obsidian::{context::Context, handler::ContextResult, App}; +// this is handler function async fn hello_world(ctx: Context) -> ContextResult { ctx.build("Hello World").ok() } @@ -7,12 +8,8 @@ async fn hello_world(ctx: Context) -> ContextResult { #[tokio::main] async fn main() { let mut app: App = App::new(); - let addr = ([127, 0, 0, 1], 3000).into(); app.get("/", hello_world); - app.listen(&addr, || { - println!("server is listening to {}", &addr); - }) - .await; + app.listen(3000).await; } diff --git a/examples/json.rs b/examples/json.rs index 83dca39..ef1f66a 100644 --- a/examples/json.rs +++ b/examples/json.rs @@ -1,27 +1,28 @@ -use obsidian::{context::Context, App, ContextResult}; +use obsidian::{context::Context, handler::ContextResult, router::Response, App}; use serde::*; -async fn get_user(ctx: Context) -> ContextResult { - #[derive(Serialize, Deserialize)] +async fn get_user(mut ctx: Context) -> ContextResult { + #[derive(Serialize, Deserialize, Debug)] struct User { name: String, }; - let user = User { - name: String::from("Obsidian"), + #[derive(Serialize, Deserialize, Debug)] + struct UserParam { + name: String, + age: i8, }; - ctx.build_json(user).ok() + + let user: UserParam = ctx.json().await?; + + Ok(Response::ok().json(user)) } #[tokio::main] async fn main() { - let mut app: App = App::new(); - let addr = ([127, 0, 0, 1], 3000).into(); + let mut app: App = App::default(); app.get("/user", get_user); - app.listen(&addr, || { - println!("server is listening to {}", &addr); - }) - .await; + app.listen(3000).await; } diff --git a/examples/main.rs b/examples/main.rs index f69d09f..5d4982a 100644 --- a/examples/main.rs +++ b/examples/main.rs @@ -7,7 +7,7 @@ use std::{fmt, fmt::Display}; use obsidian::{ context::Context, router::{header, Responder, Response, Router}, - App, ObsidianError, StatusCode, + App, StatusCode, }; // Testing example @@ -66,20 +66,18 @@ impl Display for JsonTest { // .header("X-Custom-Header", "custom-value") // .header("X-Custom-Header-2", "custom-value-2") // .header("X-Custom-Header-3", "custom-value-3") -// .set_headers(headers) -// .status(StatusCode::CREATED) +// .set_headers(headers) .status(StatusCode::CREATED) // } #[tokio::main] async fn main() { let mut app: App = App::default(); - let addr = ([127, 0, 0, 1], 3000).into(); - app.get("/", |ctx: Context| async { + app.get("/", |ctx: Context| async move { ctx.build(Response::ok().html("<!DOCTYPE html><html><head><link rel=\"shotcut icon\" href=\"favicon.ico\" type=\"image/x-icon\" sizes=\"32x32\" /></head> <h1>Hello Obsidian</h1></html>")).ok() }); - app.get("/json", |ctx: Context| async { + app.get("/json", |ctx: Context| async move { let point = Point { x: 1, y: 2 }; ctx.build_json(point) @@ -89,7 +87,7 @@ ctx.build(Response::ok().html("<!DOCTYPE html><html><head><link rel=\"shotcut ic .ok() }); - app.get("/user", |mut ctx: Context| async { + app.get("/user", |mut ctx: Context| async move { #[derive(Serialize, Deserialize, Debug)] struct QueryString { id: String, @@ -112,11 +110,11 @@ ctx.build(Response::ok().html("<!DOCTYPE html><html><head><link rel=\"shotcut ic ctx.build("").ok() }); - app.patch("/patch-here", |ctx: Context| async { + app.patch("/patch-here", |ctx: Context| async move { ctx.build("Here is patch request").ok() }); - app.get("/json-with-headers", |ctx: Context| async { + app.get("/json-with-headers", |ctx: Context| async move { let point = Point { x: 1, y: 2 }; let custom_headers = vec![ @@ -139,7 +137,7 @@ ctx.build(Response::ok().html("<!DOCTYPE html><html><head><link rel=\"shotcut ic .ok() }); - app.get("/string-with-headers", |ctx: Context| async { + app.get("/string-with-headers", |ctx: Context| async move { let custom_headers = vec![ ("X-Custom-Header-1", "Custom header 1"), ("X-Custom-Header-2", "Custom header 2"), @@ -157,37 +155,37 @@ ctx.build(Response::ok().html("<!DOCTYPE html><html><head><link rel=\"shotcut ic .ok() }); - app.get("/empty-body", |ctx: Context| async { + app.get("/empty-body", |ctx: Context| async move { ctx.build(StatusCode::OK).ok() }); - app.get("/vec", |ctx: Context| async { + app.get("/vec", |ctx: Context| async move { ctx.build(vec![1, 2, 3]) .with_status(StatusCode::CREATED) .ok() }); - app.get("/String", |ctx: Context| async { + app.get("/String", |ctx: Context| async move { ctx.build("<h1>This is a String</h1>".to_string()).ok() }); - app.get("/test/radix", |ctx: Context| async { + app.get("/test/radix", |ctx: Context| async move { ctx.build("<h1>Test radix</h1>".to_string()).ok() }); - app.get("/team/radix", |ctx: Context| async { + app.get("/team/radix", |ctx: Context| async move { ctx.build("Team radix".to_string()).ok() }); - app.get("/test/radix2", |ctx: Context| async { + app.get("/test/radix2", |ctx: Context| async move { ctx.build("<h1>Test radix2</h1>".to_string()).ok() }); - app.get("/jsontest", |ctx: Context| async { + app.get("/jsontest", |ctx: Context| async move { ctx.build_file("./testjson.html").await.ok() }); - app.get("/jsan", |ctx: Context| async { + app.get("/jsan", |ctx: Context| async move { ctx.build("<h1>jsan</h1>".to_string()).ok() }); @@ -202,10 +200,7 @@ ctx.build(Response::ok().html("<!DOCTYPE html><html><head><link rel=\"shotcut ic }); app.get("router/test", |ctx: Context| async move { - let result = ctx - .extensions() - .get::<LoggerExampleData>() - .ok_or(ObsidianError::NoneError)?; + let result = ctx.extensions().get::<LoggerExampleData>().unwrap(); dbg!(&result.0); @@ -300,11 +295,11 @@ ctx.build(Response::ok().html("<!DOCTYPE html><html><head><link rel=\"shotcut ic app.use_service(logger_example); app.scope("params", |router: &mut Router| { - router.get("/test-next-wild/*", |ctx: Context| async { + router.get("/test-next-wild/*", |ctx: Context| async move { ctx.build("<h1>test next wild</h1>".to_string()).ok() }); - router.get("/*", |ctx: Context| async { + router.get("/*", |ctx: Context| async move { ctx.build( "<h1>404 Not Found</h1>" .to_string() @@ -316,5 +311,5 @@ ctx.build(Response::ok().html("<!DOCTYPE html><html><head><link rel=\"shotcut ic app.use_static_to("/files/", "/assets/"); - app.listen(&addr, || {}).await; + app.listen(3000).await } diff --git a/examples/middleware/logger_example.rs b/examples/middleware/logger_example.rs index 30baefb..37c8a45 100644 --- a/examples/middleware/logger_example.rs +++ b/examples/middleware/logger_example.rs @@ -3,7 +3,9 @@ use async_trait::async_trait; #[cfg(debug_assertions)] use colored::*; -use obsidian::{context::Context, middleware::Middleware, ContextResult, EndpointExecutor}; +use obsidian::{ + context::Context, handler::ContextResult, middleware::Middleware, EndpointExecutor, +}; #[derive(Default)] pub struct LoggerExample {} diff --git a/screenshot/lifecycle.png b/screenshot/lifecycle.png new file mode 100644 index 0000000..8664f16 Binary files /dev/null and b/screenshot/lifecycle.png differ diff --git a/src/app.rs b/src/app.rs index 1ad9a64..4ab0363 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,3 @@ -use std::net::SocketAddr; use std::sync::Arc; use colored::*; @@ -9,9 +8,9 @@ use hyper::{ }; use crate::context::Context; -use crate::error::ObsidianError; +use crate::handler::{ContextResult, Handler}; use crate::middleware::Middleware; -use crate::router::{ContextResult, Handler, RouteValueResult, Router}; +use crate::router::{RouteValueResult, Router}; use crate::middleware::logger::Logger; @@ -141,7 +140,7 @@ where self.app_state = Some(app_state); } - pub async fn listen(self, addr: &SocketAddr, callback: impl Fn()) { + pub async fn listen(self, port: u16) { let app_server: AppServer = AppServer { router: self.router, }; @@ -160,6 +159,7 @@ where } }); + let addr = ([127, 0, 0, 1], port).into(); let server = Server::bind(&addr).serve(service); let logo = r#" @@ -199,8 +199,6 @@ where println!(" 🎉 {}: http://{}\n", "Served at".green().bold(), addr); - callback(); - server.await.map_err(|_| println!("Server error")).unwrap(); } } @@ -237,38 +235,41 @@ impl AppServer { let route_result = executor.next(context).await; let route_response = match route_result { - Ok(ctx) => { + Ok(response) => { let mut res = Response::builder(); - if let Some(response) = ctx.take_response() { - if let Some(headers) = response.headers() { - if let Some(response_headers) = res.headers_mut() { - headers.iter().for_each(move |(key, value)| { - response_headers - .insert(key, header::HeaderValue::from_static(value)); - }); - } + if let Some(headers) = response.headers() { + if let Some(response_headers) = res.headers_mut() { + headers.iter().for_each(move |(key, value)| { + response_headers + .insert(key, header::HeaderValue::from_static(value)); + }); } - res.status(response.status()).body(response.body()) - } else { - // No response found - res.status(StatusCode::OK).body(Body::from("")) } + res.status(response.status()).body(response.body()) } Err(err) => { - let body = Body::from(err.to_string()); - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(body) + let response = err.into_error_response(); + let mut res = Response::builder(); + if let Some(headers) = response.headers() { + if let Some(response_headers) = res.headers_mut() { + headers.iter().for_each(move |(key, value)| { + response_headers + .insert(key, header::HeaderValue::from_static(value)); + }); + } + } + res.status(response.status()).body(response.body()) } }; - Ok::<_, hyper::Error>(route_response.unwrap_or_else(|_| { - internal_server_error(ObsidianError::GeneralError( - "Error while constructing response body".to_string(), - )) - })) + // Ok::<_, hyper::Error>(route_response.unwrap_or_else(|_| { + // internal_server_error(ObsidianError::GeneralError( + // "Error while constructing response body".to_string(), + // )) + // })) + Ok::<_, hyper::Error>(route_response.unwrap()) } - _ => Ok::<_, hyper::Error>(page_not_found()), + None => Ok::<_, hyper::Error>(page_not_found()), } } } @@ -280,33 +281,33 @@ fn page_not_found() -> Response<Body> { server_response } -fn internal_server_error(err: impl std::error::Error) -> Response<Body> { - let body = Body::from(err.to_string()); - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(body) - .unwrap() -} +// fn internal_server_error(err: impl std::error::Error) -> Response<Body> { +// let body = Body::from(err.to_string()); +// Response::builder() +// .status(StatusCode::INTERNAL_SERVER_ERROR) +// .body(body) +// .unwrap() +// } pub struct EndpointExecutor<'a> { pub route_endpoint: &'a Arc<dyn Handler>, - pub middleware: &'a [Arc<dyn Middleware>], + pub middlewares: &'a [Arc<dyn Middleware>], } impl<'a> EndpointExecutor<'a> { pub fn new( route_endpoint: &'a Arc<dyn Handler>, - middleware: &'a [Arc<dyn Middleware>], + middlewares: &'a [Arc<dyn Middleware>], ) -> Self { EndpointExecutor { route_endpoint, - middleware, + middlewares, } } pub async fn next(mut self, context: Context) -> ContextResult { - if let Some((current, all_next)) = self.middleware.split_first() { - self.middleware = all_next; + if let Some((current, all_next)) = self.middlewares.split_first() { + self.middlewares = all_next; current.handle(context, self).await } else { self.route_endpoint.call(context).await diff --git a/src/context.rs b/src/context.rs index 05e72e9..ac6f540 100644 --- a/src/context.rs +++ b/src/context.rs @@ -9,8 +9,9 @@ use std::collections::HashMap; use std::convert::From; use std::str::FromStr; -use crate::router::{from_cow_map, ContextResult, Responder, Response}; -use crate::ObsidianError; +use crate::error::{InternalError, ObsidianError}; +use crate::handler::ContextResult; +use crate::router::{from_cow_map, Responder, Response}; use crate::{ header::{HeaderName, HeaderValue}, Body, HeaderMap, Method, Request, StatusCode, Uri, @@ -86,9 +87,8 @@ impl Context { /// /// ``` /// # use obsidian::StatusCode; - /// # use obsidian::ContextResult; + /// # use obsidian::handler::ContextResult; /// # use obsidian::context::Context; - /// /// // Assumming ctx contains params for id and mode /// async fn get_handler(ctx: Context) -> ContextResult { /// let id: i32 = ctx.param("id")?; @@ -102,12 +102,16 @@ impl Context { /// /// ``` /// - pub fn param<T: FromStr>(&self, key: &str) -> Result<T, ObsidianError> { + pub fn param<T: FromStr>(&self, key: &str) -> Result<T, InternalError> { self.params_data .get(key) - .ok_or(ObsidianError::NoneError)? + .ok_or(InternalError::NoneError(format!( + "The key [{}] not found", + key + )))? .parse() - .map_err(|_err| ObsidianError::ParamError(format!("Failed to parse param {}", key))) + .map_err(|_err| InternalError::ParamError(format!("Failed to parse param {}", key))) + // The error will never happen } /// Method to get the string query data from the request url. @@ -116,10 +120,8 @@ impl Context { /// # Example /// ``` /// # use serde::*; - /// /// # use obsidian::context::Context; - /// # use obsidian::{ContextResult, StatusCode}; - /// + /// # use obsidian::{handler::ContextResult, StatusCode}; /// #[derive(Deserialize, Serialize, Debug)] /// struct QueryString { /// id: i32, @@ -153,10 +155,8 @@ impl Context { /// # Example /// ``` /// # use serde::*; - /// /// # use obsidian::context::Context; - /// # use obsidian::{ContextResult, StatusCode}; - /// + /// # use obsidian::{handler::ContextResult, StatusCode}; /// #[derive(Deserialize, Serialize, Debug)] /// struct FormResult { /// id: i32, @@ -176,12 +176,7 @@ impl Context { pub async fn form<T: DeserializeOwned>(&mut self) -> Result<T, ObsidianError> { let body = self.take_body(); - let buf = match body::aggregate(body).await { - Ok(buf) => buf, - _ => { - return Err(ObsidianError::NoneError); - } - }; + let buf = body::aggregate(body).await?; Self::parse_queries(buf.bytes()) } @@ -200,10 +195,8 @@ impl Context { /// ### Handle by static type /// ``` /// # use serde::*; - /// /// # use obsidian::context::Context; - /// # use obsidian::{ContextResult, StatusCode}; - /// + /// # use obsidian::{handler::ContextResult, StatusCode}; /// #[derive(Deserialize, Serialize, Debug)] /// struct JsonResult { /// id: i32, @@ -224,10 +217,7 @@ impl Context { /// ### Handle by dynamic map /// ``` /// # use serde_json::Value; - /// - /// # use obsidian::context::Context; - /// # use obsidian::{ContextResult, StatusCode}; - /// + /// # use obsidian::{context::Context, handler::ContextResult, StatusCode}; /// // Assume ctx contains json with data {id:1, mode:'edit'} /// async fn get_handler(mut ctx: Context) -> ContextResult { /// let result: serde_json::Value = ctx.json().await?; @@ -241,12 +231,7 @@ impl Context { pub async fn json<T: DeserializeOwned>(&mut self) -> Result<T, ObsidianError> { let body = self.take_body(); - let buf = match body::aggregate(body).await { - Ok(buf) => buf, - _ => { - return Err(ObsidianError::NoneError); - } - }; + let buf = body::aggregate(body).await?; Ok(serde_json::from_slice(buf.bytes())?) } @@ -275,18 +260,18 @@ impl Context { } /// Build any kind of response which implemented Responder trait - pub fn build(self, res: impl Responder) -> ResponseBuilder { - ResponseBuilder::new(self, res.respond_to()) + pub fn build(&self, res: impl Responder) -> ResponseBuilder { + ResponseBuilder::new(res.respond_to()) } /// Build data into json format. The data must implement Serialize trait - pub fn build_json(self, body: impl Serialize) -> ResponseBuilder { - ResponseBuilder::new(self, Response::ok().json(body)) + pub fn build_json(&self, body: impl Serialize) -> ResponseBuilder { + ResponseBuilder::new(Response::ok().json(body)) } /// Build response from static file. - pub async fn build_file(self, file_path: &str) -> ResponseBuilder { - ResponseBuilder::new(self, Response::ok().file(file_path).await) + pub async fn build_file(&self, file_path: &str) -> ResponseBuilder { + ResponseBuilder::new(Response::ok().file(file_path).await) } fn parse_queries<T: DeserializeOwned>(query: &[u8]) -> Result<T, ObsidianError> { @@ -317,13 +302,12 @@ impl Context { } pub struct ResponseBuilder { - ctx: Context, response: Response, } impl ResponseBuilder { - pub fn new(ctx: Context, response: Response) -> Self { - ResponseBuilder { ctx, response } + pub fn new(response: Response) -> Self { + ResponseBuilder { response } } /// set http status code for response @@ -354,9 +338,9 @@ impl ResponseBuilder { self } - pub fn ok(mut self) -> ContextResult { - *self.ctx.response_mut() = Some(self.response); - Ok(self.ctx) + pub fn ok(self) -> ContextResult { + // *self.ctx.response_mut() = Some(self.response); + Ok(self.response) } } diff --git a/src/error.rs b/src/error.rs index 38937cb..74a1c99 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,3 @@ -mod obsidian_error; +mod error_response; -pub use obsidian_error::ObsidianError; +pub use error_response::{InternalError, IntoErrorResponse, ObsidianError}; diff --git a/src/error/error_response.rs b/src/error/error_response.rs new file mode 100644 index 0000000..0e8bdba --- /dev/null +++ b/src/error/error_response.rs @@ -0,0 +1,95 @@ +use crate::router::FormError; +use crate::router::Response; +use hyper::{Body, StatusCode}; +use serde_json::error::Error as JsonError; +use std::fmt; + +#[derive(Debug)] +pub struct ObsidianError { + inner: Box<dyn IntoErrorResponse>, +} + +#[derive(Debug)] +pub enum InternalError { + GeneralError(String), + NoneError(String), + ParamError(String), +} + +impl fmt::Display for InternalError { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let error_msg = match *self { + InternalError::GeneralError(ref msg) => msg.to_string(), + InternalError::NoneError(ref msg) => msg.to_string(), + InternalError::ParamError(ref msg) => msg.to_string(), + }; + + formatter.write_str(&error_msg) + } +} + +impl ObsidianError { + pub fn into_error_response(self) -> Response { + self.inner.into_error_response() + } +} + +impl fmt::Display for ObsidianError { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.inner, formatter) + } +} + +pub trait IntoErrorResponse: fmt::Debug + fmt::Display { + /// convert Error into error response + fn into_error_response(&self) -> Response { + let body = Body::from(self.to_string()); + Response::new_with_body(body).set_status(self.status_code()) + } + + /// status code for this error response + /// this will return Internal Server Error (500) by default + fn status_code(&self) -> StatusCode { + StatusCode::INTERNAL_SERVER_ERROR + } +} + +impl<T: IntoErrorResponse + 'static> From<T> for ObsidianError { + fn from(err: T) -> Self { + ObsidianError { + inner: Box::new(err), + } + } +} + +impl IntoErrorResponse for JsonError {} +impl IntoErrorResponse for FormError {} +impl IntoErrorResponse for hyper::error::Error {} +impl IntoErrorResponse for String {} +impl IntoErrorResponse for InternalError { + /// convert Error into error response + fn into_error_response(&self) -> Response { + let body = Body::from(self.to_string()); + Response::new_with_body(body).set_status(self.status_code()) + } + + /// status code for this error response + /// this will return Internal Server Error (500) by default + fn status_code(&self) -> StatusCode { + match self { + InternalError::GeneralError(_) => StatusCode::INTERNAL_SERVER_ERROR, + InternalError::NoneError(_) => StatusCode::NOT_FOUND, + InternalError::ParamError(_) => StatusCode::BAD_REQUEST, + } + } +} + +impl std::error::Error for InternalError {} + +impl From<std::convert::Infallible> for ObsidianError { + fn from(_: std::convert::Infallible) -> Self { + // `std::convert::Infallible` indicates an error + // that will never happen + unreachable!() + } +} diff --git a/src/error/obsidian_error.rs b/src/error/obsidian_error.rs deleted file mode 100644 index 9caccbd..0000000 --- a/src/error/obsidian_error.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::error::Error; -use std::fmt; -use std::fmt::Display; - -use serde_json::error::Error as JsonError; - -use crate::router::FormError; - -/// Errors occurs in Obsidian framework -#[derive(Debug)] -pub enum ObsidianError { - ParamError(String), - JsonError(JsonError), - FormError(FormError), - GeneralError(String), - NoneError, -} - -impl Display for ObsidianError { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - let error_msg = match *self { - ObsidianError::ParamError(ref msg) => msg.to_string(), - ObsidianError::JsonError(ref err) => err.to_string(), - ObsidianError::FormError(ref err) => err.to_string(), - ObsidianError::GeneralError(ref msg) => msg.to_string(), - ObsidianError::NoneError => "Input should not be None".to_string(), - }; - - formatter.write_str(&error_msg) - } -} - -impl From<FormError> for ObsidianError { - fn from(error: FormError) -> Self { - ObsidianError::FormError(error) - } -} - -impl From<JsonError> for ObsidianError { - fn from(error: JsonError) -> Self { - ObsidianError::JsonError(error) - } -} - -impl Error for ObsidianError { - fn description(&self) -> &str { - "Obsidian Error" - } -} diff --git a/src/handler/mod.rs b/src/handler/mod.rs new file mode 100644 index 0000000..478a194 --- /dev/null +++ b/src/handler/mod.rs @@ -0,0 +1,29 @@ +use crate::context::Context; +use crate::error::ObsidianError; +use crate::router::{Responder, Response}; + +use async_trait::async_trait; +use std::future::Future; + +/// A HTTP request handler. +/// +/// This trait is expected by router to perform function for every request. +#[async_trait] +pub trait Handler: Send + Sync + 'static { + async fn call(&self, ctx: Context) -> ContextResult; +} + +#[async_trait] +impl<T, F, R> Handler for T +where + T: Fn(Context) -> F + Send + Sync + 'static, + F: Future<Output = R> + Send + 'static, + R: Responder + 'static, +{ + async fn call(&self, ctx: Context) -> ContextResult { + Ok((self)(ctx).await.respond_to()) + } +} + +/// `Result` with the error type `ObsidianError`. +pub type ContextResult<E = ObsidianError> = Result<Response, E>; diff --git a/src/lib.rs b/src/lib.rs index cd702ac..6964e7f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,10 +4,10 @@ mod app; pub mod error; pub mod context; +pub mod handler; pub mod middleware; pub mod router; pub use app::{App, EndpointExecutor}; pub use error::ObsidianError; pub use hyper::{header, Body, HeaderMap, Method, Request, Response, StatusCode, Uri, Version}; -pub use router::ContextResult; diff --git a/src/middleware.rs b/src/middleware.rs index 88b8d17..723bc34 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -4,8 +4,40 @@ use async_trait::async_trait; use crate::app::EndpointExecutor; use crate::context::Context; -use crate::router::ContextResult; +use crate::handler::ContextResult; +/// Middleware trait provides a way to allow user to: +/// 1. alter the request data before proceeding to the next middleware or the handler +/// 2. transform the response on the way out +/// 3. perform side-effects before or after handler processing +/// +/// ``` +/// use async_trait::async_trait; +/// use obsidian::{ +/// context::Context, handler::ContextResult, middleware::Middleware, EndpointExecutor, +/// }; +/// +/// // Middleware struct +/// pub struct ExampleMiddleware {} +/// +/// #[async_trait] +/// impl Middleware for ExampleMiddleware { +/// async fn handle<'a>( +/// &'a self, +/// context: Context, +/// ep_executor: EndpointExecutor<'a>, +/// ) -> ContextResult { +/// +/// // actions BEFORE handler processing write here... +/// +/// let result = ep_executor.next(context).await?; +/// +/// // actions AFTER handler processing write here... +/// +/// Ok(result) +/// } +/// } +/// ``` #[async_trait] pub trait Middleware: Send + Sync + 'static { async fn handle<'a>( diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 5a7537d..2d364a2 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -3,8 +3,8 @@ use std::time::Instant; use crate::app::EndpointExecutor; use crate::context::Context; +use crate::handler::ContextResult; use crate::middleware::Middleware; -use crate::router::ContextResult; use colored::*; @@ -31,14 +31,11 @@ impl Middleware for Logger { println!("{} {:#?}", "[debug]".cyan(), context); match ep_executor.next(context).await { - Ok(context_after) => { + Ok(response) => { let duration = start.elapsed(); - println!( - "[info] Sent {} in {:?}", - context_after.response().as_ref().unwrap().status(), - duration - ); - Ok(context_after) + let status = response.status(); + println!("[info] Sent {} in {:?}", status, duration); + Ok(response) } Err(error) => { println!("{} {}", "[error]".red(), error); diff --git a/src/router.rs b/src/router.rs index 8eff03d..5508cbe 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,4 +1,3 @@ -mod handler; mod req_deserializer; mod resource; mod responder; @@ -9,11 +8,11 @@ mod route_trie; use self::route_trie::RouteTrie; use crate::context::Context; +use crate::handler::{ContextResult, Handler}; use crate::middleware::Middleware; use crate::Method; pub use hyper::header; -pub use self::handler::{ContextResult, Handler}; pub use self::req_deserializer::{from_cow_map, Error as FormError}; pub use self::resource::Resource; pub use self::responder::Responder; @@ -137,10 +136,7 @@ impl Router { dir_path.append(&mut relative_path); - Box::pin(async move { - ctx.build(Response::ok().file(&dir_path.join("/")).await) - .ok() - }) + Box::pin(async move { Response::ok().file(&dir_path.join("/")).await }) } } @@ -153,8 +149,7 @@ impl Router { .map(|x| x.to_string()) .collect::<Vec<String>>(); - ctx.build(Response::ok().file(&relative_path.join("/")).await) - .ok() + Ok(Response::ok().file(&relative_path.join("/")).await) } } diff --git a/src/router/handler.rs b/src/router/handler.rs deleted file mode 100644 index 3250e4c..0000000 --- a/src/router/handler.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::context::Context; -use crate::error::ObsidianError; - -use async_trait::async_trait; -use std::future::Future; - -pub type ContextResult<T = ObsidianError> = Result<Context, T>; - -#[async_trait] -pub trait Handler: Send + Sync + 'static { - async fn call(&self, ctx: Context) -> ContextResult; -} - -#[async_trait] -impl<T, F> Handler for T -where - T: Fn(Context) -> F + Send + Sync + 'static, - F: Future<Output = ContextResult> + Send + 'static, -{ - async fn call(&self, ctx: Context) -> ContextResult { - (self)(ctx).await - } -} diff --git a/src/router/responder.rs b/src/router/responder.rs index c7ba32d..6f0cd04 100644 --- a/src/router/responder.rs +++ b/src/router/responder.rs @@ -1,5 +1,7 @@ use super::Response; use super::ResponseBody; +use crate::error::IntoErrorResponse; +use crate::handler::ContextResult; use hyper::{header, StatusCode}; pub trait Responder { @@ -112,6 +114,42 @@ impl Responder for Option<&'static str> { } } +// For Result<Response, E> where E: IntoErrorResponse +impl<E> Responder for ContextResult<E> +where + E: IntoErrorResponse, +{ + fn respond_to(self) -> Response { + match self { + Ok(resp) => resp.respond_to(), + Err(err) => err.into_error_response(), + } + } +} + +// For Result<Response, ObsidianError> +impl Responder for ContextResult { + fn respond_to(self) -> Response { + match self { + Ok(resp) => resp, + Err(err) => err.to_string().respond_to(), + } + } +} + +impl<T, E> Responder for Result<T, E> +where + T: Responder + ResponseBody, + E: IntoErrorResponse, +{ + fn respond_to(self) -> Response { + match self { + Ok(resp) => resp.respond_to(), + Err(err) => err.into_error_response(), + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/src/router/response.rs b/src/router/response.rs index 974df78..2e910ed 100644 --- a/src/router/response.rs +++ b/src/router/response.rs @@ -4,6 +4,7 @@ use async_std::fs; use http::StatusCode; use hyper::{header, Body}; use serde::ser::Serialize; +use std::fmt::Debug; #[derive(Debug)] pub struct Response { @@ -21,6 +22,14 @@ impl Response { } } + pub fn new_with_body(body: Body) -> Self { + Response { + body, + status: StatusCode::OK, + headers: None, + } + } + pub fn status(&self) -> StatusCode { self.status } diff --git a/src/router/route_trie.rs b/src/router/route_trie.rs index 2eb7880..7cf94a5 100644 --- a/src/router/route_trie.rs +++ b/src/router/route_trie.rs @@ -4,10 +4,10 @@ use std::sync::Arc; use hyper::Method; +use crate::error::InternalError; use crate::middleware::Middleware; use crate::router::Resource; use crate::router::Route; -use crate::ObsidianError; #[derive(Clone, Default)] pub struct RouteValue { @@ -324,7 +324,7 @@ impl Node { } /// Process the side effects of node insertion - fn process_insertion(&mut self, key: &str) -> Result<&mut Self, ObsidianError> { + fn process_insertion(&mut self, key: &str) -> Result<&mut Self, InternalError> { let action = self.get_insertion_action(key); match action.name { @@ -388,7 +388,7 @@ impl Node { } ActionName::Error => { if let Some(node) = self.child_nodes.get(action.payload.node_index) { - return Err(ObsidianError::GeneralError(format!( + return Err(InternalError::GeneralError(format!( "ERROR: Ambigous definition between {} and {}", key, node.key )));