├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── clippy.toml ├── examples ├── arg.rs ├── common │ └── mod.rs ├── extra.rs ├── hello.rs ├── log.rs ├── measurer.rs ├── measurer_minimal.rs ├── middleware.rs ├── query.rs ├── router.rs ├── stop.rs ├── subapp.rs ├── tokio_executor.rs └── urlencoded.rs ├── rustfmt.toml └── src ├── context.rs ├── executor.rs ├── lib.rs └── middleware ├── m.rs ├── mod.rs └── router ├── like.rs ├── method.rs ├── mod.rs ├── set_which.rs └── setter.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | /.idea 3 | /.vscode 4 | 5 | # Rust 6 | /target 7 | Cargo.lock 8 | 9 | # macOS 10 | **/.DS_Store 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amiya" 3 | version = "0.0.6" 4 | authors = ["7sDream "] 5 | edition = "2018" 6 | description = "experimental middleware-based minimalism async HTTP server framework" 7 | document = "https://docs.rs/amiya" 8 | readme = "README.md" 9 | homepage = "https://github.com/7sDream/amiya" 10 | repository = "https://github.com/7sDream/amiya" 11 | license = "BSD-3-Clause-Clear" 12 | keywords = ["async", "web", "http-server", "framework"] 13 | categories = ["network-programming", "asynchronous", "web-programming::http-server"] 14 | 15 | [dependencies] 16 | async-net = "1" 17 | http-types = { version = "2", default-features = false } 18 | async-h1 = "2" 19 | async-trait = "0.1" 20 | futures-lite = "1" 21 | async-channel = "1" 22 | log = "0.4" 23 | 24 | # Built-in executor dependencies 25 | async-executor = { version = "1", optional = true } 26 | async-io = { version = "1", optional = true } 27 | once_cell = { version = "1", optional = true } 28 | num_cpus = { version = "1", optional = true } 29 | 30 | [features] 31 | default = ["built-in-executor"] 32 | built-in-executor = ["async-executor", "async-io", "once_cell", "num_cpus"] 33 | 34 | [dev-dependencies] 35 | tokio = { version = "1", features = ["rt-multi-thread"] } 36 | env_logger = "0.8" 37 | serde = "1" 38 | serde_json = "1" 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Clear BSD License Copyright (c) 2020 7sDream 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted (subject to the limitations in the disclaimer below) provided 7 | that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of 7sDream nor the names of its contributors 17 | may be used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY 21 | THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 24 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 25 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 30 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amiya 2 | 3 | [![Badge with github icon][github-badge-img]][github-home] [![Badge with document icon][doc-badge-img]][doc-home] 4 | 5 | Amiya is a experimental middleware-based minimalism async HTTP server framework, 6 | built up on [`smol-rs`] related asynchronous components. 7 | 8 | It's currently still working in progress and in a very early alpha stage. 9 | 10 | API design may changes, **DO NOT** use it in any condition except for test or study! 11 | 12 | ## Goal 13 | 14 | The goal of this project is try to build a (by importance order): 15 | 16 | - Safe, with `#![forbid(unsafe_code)]` 17 | - Async 18 | - Middleware-based 19 | - Minimalism 20 | - Easy to use 21 | - Easy to extend 22 | 23 | HTTP framework for myself to write simple web services. 24 | 25 | Amiya uses [`async-h1`] to parse and process requests, so only HTTP version 1.1 is supported for 26 | now. HTTP 1.0 or 2.0 is not in goal list, at least in the near future. 27 | 28 | Performance is **NOT** in the list too, after all, Amiya is just a experimental for now, it uses 29 | many heap alloc (Box) and dynamic dispatch (Trait Object) so there may be some performance loss 30 | compare to use [`async-h1`] directly. 31 | 32 | ## Have a Taste 33 | 34 | To start a very simple HTTP service that returns `Hello World` to the client in all paths: 35 | 36 | ```rust 37 | use amiya::m; 38 | 39 | fn main() { 40 | let app = amiya::new().uses(m!(ctx => 41 | ctx.resp.set_body(format!("Hello World from: {}", ctx.path())); 42 | )); 43 | 44 | app.listen("[::]:8080").unwrap(); 45 | 46 | // ... do other things you want ... 47 | // ... Amiya server will not block your thread ... 48 | } 49 | ``` 50 | 51 | Amiya has a built-in multi-thread async executor powered by `async-executor` and `async-io`, http server will run 52 | in it. So `Amiya::listen` is just a normal non-async method, and do not block your thread. 53 | 54 | ## Examples 55 | 56 | To run examples, run 57 | 58 | ```bash 59 | cargo run --example # show example list 60 | cargo run --example hello # run hello 61 | ``` 62 | 63 | Top level document of crate has [a brief description of concepts][doc-concepts] used in this 64 | framework, I recommend give it a read first, and then check those examples to get a more intuitive 65 | understanding: 66 | 67 | - Understand onion model of Amiya middleware system: [`examples/middleware.rs`] 68 | - Use a custom type as middleware: [`examples/measurer.rs`] 69 | - Store extra data in context: [`examples/extra.rs`] 70 | - Use `Router` middleware for request diversion: [`examples/router.rs`] 71 | - Parse query string to json value or custom struct: [`examples/query.rs`] 72 | - Parse body(www-form-urlencoded) to json value or custom struct: [`examples/urlencoded.rs`] 73 | - Match part of path as an argument: [`examples/arg.rs`] 74 | - Use another Amiya app as middleware: [`examples/subapp.rs`] 75 | - Stop Amiya server by using `listen` returned signal sender: [`examples/stop.rs`] 76 | 77 | Most of those example will use builtin executor, see [`example/tokio_executor.rs`] for how to use a custom executor with Amiya. 78 | 79 | ## License 80 | 81 | BSD 3-Clause Clear License, See [`LICENSE`]. 82 | 83 | [github-badge-img]: https://img.shields.io/badge/Github-7sDream%2Famiya-8da0cb?style=for-the-badge&labelColor=555555&logo=github 84 | [github-home]: https://github.com/7sDream/amiya 85 | [doc-badge-img]: https://img.shields.io/badge/docs-on_docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=read-the-docs 86 | [doc-home]: https://docs.rs/amiya/latest/amiya/ 87 | [doc-concepts]: https://docs.rs/amiya/latest/amiya/#concepts 88 | [`smol-rs`]: https://github.com/smol-rs 89 | [`async-h1`]: https://github.com/http-rs/async-h1 90 | [`examples/hello.rs`]: https://github.com/7sDream/amiya/blob/master/examples/hello.rs 91 | [`examples/middleware.rs`]: https://github.com/7sDream/amiya/blob/master/examples/middleware.rs 92 | [`examples/measurer.rs`]: https://github.com/7sDream/amiya/blob/master/examples/measurer.rs 93 | [`examples/extra.rs`]: https://github.com/7sDream/amiya/blob/master/examples/extra.rs 94 | [`examples/query.rs`]: https://github.com/7sDream/amiya/blob/master/examples/query.rs 95 | [`examples/urlencoded.rs`]: https://github.com/7sDream/amiya/blob/master/examples/urlencoded.rs 96 | [`examples/router.rs`]: https://github.com/7sDream/amiya/blob/master/examples/router.rs 97 | [`examples/arg.rs`]: https://github.com/7sDream/amiya/blob/master/examples/arg.rs 98 | [`examples/subapp.rs`]: https://github.com/7sDream/amiya/blob/master/examples/subapp.rs 99 | [`examples/stop.rs`]: https://github.com/7sDream/amiya/blob/master/examples/stop.rs 100 | [`example/tokio_executor.rs`]: https://github.com/7sDream/amiya/blob/master/examples/tokio_executor.rs 101 | [`LICENSE`]: https://github.com/7sDream/amiya/blob/master/LICENSE 102 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | cognitive-complexity-threshold = 25 2 | literal-representation-threshold = 0 3 | enum-variant-name-threshold = 0 4 | enum-variant-size-threshold = 32 5 | single-char-binding-names-threshold = 4 6 | too-many-arguments-threshold = 5 7 | too-many-lines-threshold = 50 8 | trivial-copy-size-limit = 8 9 | verbose-bit-mask-threshold = 1 10 | -------------------------------------------------------------------------------- /examples/arg.rs: -------------------------------------------------------------------------------- 1 | use { 2 | amiya::{m, middleware::Router, Context, Result, StatusCode}, 3 | std::convert::TryInto, 4 | }; 5 | 6 | async fn return_status_code(mut ctx: Context<'_, ()>) -> Result { 7 | ctx.next().await?; 8 | 9 | let code_arg = ctx.arg("status_code").unwrap(); 10 | 11 | if let Ok(code_num) = code_arg.parse::() { 12 | if let Ok(code) = code_num.try_into() { 13 | ctx.resp.set_status(code); 14 | return Ok(()); 15 | } 16 | } 17 | 18 | ctx.resp.set_status(StatusCode::BadRequest); 19 | Ok(()) 20 | } 21 | 22 | fn main() { 23 | // Any path matches /status/{status_code} 24 | #[rustfmt::skip] 25 | let router = Router::new() 26 | .at("status") 27 | .at("{status_code}").uses(m!(return_status_code)).done() 28 | .done(); 29 | 30 | let app = amiya::new().uses(router); 31 | 32 | app.listen("[::]:8080").unwrap(); 33 | 34 | std::thread::park(); 35 | } 36 | 37 | // visit /status/200 => http status 200 38 | // visit /status/502 => http status 502 39 | // ... etc ... 40 | // visit /status/ => http status 400(Bad Request) 41 | // visit other path => http status 404 42 | -------------------------------------------------------------------------------- /examples/common/mod.rs: -------------------------------------------------------------------------------- 1 | use amiya::{Context, Result}; 2 | 3 | #[allow(dead_code)] 4 | pub async fn response>(data: D, mut ctx: Context<'_, T>) -> Result 5 | where 6 | T: Send + Sync + 'static, 7 | { 8 | ctx.next().await?; 9 | ctx.resp.set_body(data.as_ref()); 10 | Ok(()) 11 | } 12 | -------------------------------------------------------------------------------- /examples/extra.rs: -------------------------------------------------------------------------------- 1 | use amiya::m; 2 | 3 | // Extra data of Amiya must be Default + Send + Sync, and can't contain reference 4 | #[derive(Default)] 5 | struct ExData { 6 | header_ext_message: Option, 7 | } 8 | 9 | fn main() { 10 | let app = amiya::with_ex() 11 | // Amiya support extra data attach in context, just set it's type as second argument 12 | .uses(m!(ctx: ExData => { 13 | println!( 14 | "Request {} from {}", 15 | ctx.req.url(), 16 | ctx.req.remote().unwrap_or("unknown address") 17 | ); 18 | // then you can use ctx.ex to communicate with other middleware 19 | ctx.ex.header_ext_message.replace(String::from("Amiya Middleware ExData Test")); 20 | let result = ctx.next().await; 21 | if let Err(ref err) = result { 22 | eprintln!("Request process error: {}", err); 23 | } 24 | result 25 | })) 26 | .uses(m!(ctx: ExData => { 27 | ctx.next().await?; 28 | ctx.resp.set_body("Hello from Amiya!"); 29 | // get message set by other middleware and use it 30 | if let Some(message) = ctx.ex.header_ext_message.take() { 31 | ctx.resp.insert_header("X-Amiya-Ext", message); 32 | } 33 | Ok(()) 34 | })); 35 | 36 | app.listen("[::]:8080").unwrap(); 37 | 38 | std::thread::park(); 39 | } 40 | -------------------------------------------------------------------------------- /examples/hello.rs: -------------------------------------------------------------------------------- 1 | // m is a macro to let you easily write middleware use closure like Javascript's arrow function 2 | // it can also convert a async fn to a middleware use the `m!(async_func_name)` syntax. 3 | use amiya::m; 4 | 5 | fn main() { 6 | // Only this stmt is Amiya related code, it sets response to some hello world texts 7 | let app = amiya::new().uses(m!(ctx => 8 | ctx.resp.set_body("Hello World"); 9 | )); 10 | 11 | let _ = app.listen("[::]:8080").unwrap(); 12 | 13 | std::thread::park(); 14 | } 15 | -------------------------------------------------------------------------------- /examples/log.rs: -------------------------------------------------------------------------------- 1 | use { 2 | amiya::{m, Error}, 3 | std::time::Duration, 4 | }; 5 | 6 | fn main() { 7 | env_logger::init(); 8 | 9 | // Only this stmt is Amiya related code, it sets response to some hello world texts 10 | let app = amiya::new().uses(m!(ctx => 11 | Err(Error::from_str(500, "o_O")) 12 | )); 13 | 14 | let stop = app.listen("[::]:8080").unwrap(); 15 | 16 | std::thread::sleep(Duration::from_secs(10)); 17 | 18 | let _ = stop.try_send(()); 19 | 20 | std::thread::sleep(Duration::from_secs(1)); 21 | } 22 | 23 | /* 24 | [2021-03-27T14:45:34Z INFO amiya] Amiya server start listening Ok([::]:8080) 25 | [2021-03-27T14:45:37Z ERROR amiya] Request handle error: code = 500, type = Unknown, detail = o_O 26 | [2021-03-27T14:45:44Z INFO amiya] Amiya server stop listening Ok([::]:8080) 27 | */ 28 | -------------------------------------------------------------------------------- /examples/measurer.rs: -------------------------------------------------------------------------------- 1 | use { 2 | amiya::{async_trait, Context, Middleware, Result}, 3 | std::time::Instant, 4 | }; 5 | 6 | struct TimeMeasurer; 7 | 8 | #[async_trait] 9 | impl Middleware for TimeMeasurer 10 | where 11 | Ex: Send + Sync + 'static, 12 | { 13 | async fn handle(&self, mut ctx: Context<'_, Ex>) -> Result { 14 | let start = Instant::now(); 15 | ctx.next().await?; 16 | let measure = format!("req; dur={}us", start.elapsed().as_micros()); 17 | ctx.resp.append_header("server-timing", measure); 18 | Ok(()) 19 | } 20 | } 21 | 22 | struct RequestHandler; 23 | 24 | #[async_trait] 25 | impl Middleware for RequestHandler 26 | where 27 | Ex: Send + Sync + 'static, 28 | { 29 | async fn handle(&self, mut ctx: Context<'_, Ex>) -> Result { 30 | ctx.next().await?; 31 | ctx.resp.set_body("Finish"); 32 | Ok(()) 33 | } 34 | } 35 | 36 | fn main() { 37 | let app = amiya::new().uses(TimeMeasurer).uses(RequestHandler); 38 | 39 | app.listen("[::]:8080").unwrap(); 40 | 41 | std::thread::park(); 42 | } 43 | 44 | // < HTTP/1.1 200 OK 45 | // < content-length: 6 46 | // < date: Thu, 23 Jul 2020 15:50:07 GMT 47 | // < content-type: text/plain;charset=utf-8 48 | // < server-timing: req;dur=9us <------------- Added by TimeMeasurer 49 | // < 50 | // * Connection #0 to host localhost left intact 51 | // Finish* Closing connection 0 <------------- Set by RequestHandler 52 | 53 | // Referer to `examples/measurer_minimal.rs` to see how to macro `m` to achieve the same result 54 | -------------------------------------------------------------------------------- /examples/measurer_minimal.rs: -------------------------------------------------------------------------------- 1 | use {amiya::m, std::time::Instant}; 2 | 3 | fn main() { 4 | let app = amiya::new() 5 | .uses(m!(ctx => 6 | let start = Instant::now(); 7 | ctx.next().await?; 8 | let measure = format!("req; dur={}us", start.elapsed().as_micros()); 9 | ctx.resp.append_header("server-timing", measure); 10 | )) 11 | .uses(m!(ctx => ctx.resp.set_body("Finish");)); 12 | 13 | app.listen("[::]:8080").unwrap(); 14 | 15 | std::thread::park(); 16 | } 17 | -------------------------------------------------------------------------------- /examples/middleware.rs: -------------------------------------------------------------------------------- 1 | use amiya::m; 2 | 3 | fn main() { 4 | // The middleware system of Amiya uses onion model, just as NodeJs's koa framework. 5 | // The executed order is: 6 | // - `Logger`'s code before `next()`, which print a log about request in 7 | // - `Respond`'s code before `next()`, which do nothing 8 | // - `Respond`'s code after `next()`, which set the response body 9 | // - `Logger`'s code after `next()`, which read the response body and log it 10 | let app = amiya::new() 11 | // Let's call This middleware `Logger` 12 | // `ctx.next().await` will return after all inner middleware be executed 13 | // so the `content` will be "Hello World" , which is set by next middleware. 14 | .uses(m!(ctx => 15 | println!("new request at"); 16 | ctx.next().await?; 17 | let content = ctx.resp.take_body().into_string().await.unwrap(); 18 | println!("finish, response is: {}", content); 19 | ctx.resp.set_body(content); 20 | )) 21 | // Let's call This middleware `Respond` 22 | // This middleware set tht response 23 | .uses(m!(ctx => 24 | ctx.next().await?; 25 | ctx.resp.set_body("Hello World!"); 26 | )); 27 | 28 | app.listen("[::]:8080").unwrap(); 29 | 30 | std::thread::park(); 31 | } 32 | -------------------------------------------------------------------------------- /examples/query.rs: -------------------------------------------------------------------------------- 1 | use { 2 | amiya::{m, middleware::Router, Context, Result, StatusCode}, 3 | serde::{Deserialize, Serialize}, 4 | serde_json::{Map, Value}, 5 | }; 6 | 7 | async fn parse_query_object(mut ctx: Context<'_, ()>) -> Result { 8 | let qm: Map = ctx.req.query()?; 9 | 10 | ctx.next().await?; 11 | 12 | ctx.resp.set_body(Value::Object(qm)); 13 | 14 | Ok(()) 15 | } 16 | 17 | #[derive(Debug, Deserialize, Serialize)] 18 | struct SearchQuery { 19 | keyword: String, 20 | city: Option, 21 | offset: Option, 22 | } 23 | 24 | async fn parse_query_struct(mut ctx: Context<'_, ()>) -> Result { 25 | let query: SearchQuery = if let Ok(query) = ctx.req.query() { 26 | query 27 | } else { 28 | ctx.resp.set_status(StatusCode::BadRequest); 29 | return Ok(()); 30 | }; 31 | 32 | ctx.next().await?; 33 | 34 | ctx.resp.set_body(serde_json::to_value(query)?); 35 | 36 | Ok(()) 37 | } 38 | 39 | fn main() { 40 | #[rustfmt::skip] 41 | let router = Router::new() 42 | .at("object").get(m!(parse_query_object)).done() 43 | .at("struct").get(m!(parse_query_struct)).done(); 44 | 45 | let app = amiya::new().uses(router); 46 | 47 | app.listen("[::]:8080").unwrap(); 48 | 49 | std::thread::park(); 50 | } 51 | 52 | // $ curl 'http://127.0.0.1:8080/object?key=value&arr[]=a1&c=d&arr[]=a2&object[one]=1&object[two]=2' 53 | // {"arr":["a1","a2"],"c":"d","key":"value","object":{"one":"1","two":"2"}} 54 | 55 | // $ curl 'http://127.0.0.1:8080/struct?keyword=hello&city=beijing¬exist=haha' 56 | // {"keyword":"hello","city":"beijing","offset":null} 57 | 58 | // $ curl 'http://127.0.0.1:8080/struct?keyword=hello&city=beijing&offset=20' 59 | // {"keyword":"hello","city":"beijing","offset":20} 60 | -------------------------------------------------------------------------------- /examples/router.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use { 4 | amiya::{m, middleware::Router}, 5 | common::response, 6 | }; 7 | 8 | fn main() { 9 | #[rustfmt::skip] 10 | let app = amiya::new().uses(Router::new() 11 | // `at` let you set the handler when exact meets the path, 12 | // `get` let you limit this path only accept get request and set the handler 13 | // `done` finish router setting for "/api/v1/hello" 14 | .at("api/v1/hello").get(m!(ctx => response("Call version 1 hello API\n", ctx).await)).done() 15 | .at("static") 16 | // As above, we give request to exact "/static" a clear response message that 17 | // we do support list dir content 18 | .get(m!(ctx => response("We do not allow list dir\n", ctx).await)) 19 | // but we not finish this setting here, a call to `fallback` let you set the 20 | // handler of all other request just except exact match 21 | // `get` limit we only support get method for static files 22 | // 23 | // the `ctx.path()` here will return path after `/static`, for example: 24 | // response of request GET "/static/sub/folder/a.png" 25 | // will be "Get file /sub/folder/a.png" 26 | .fallback().get(m!(ctx => response(format!("Get file {}\n", ctx.path()), ctx).await)) 27 | // and we finish "/static" router setting 28 | .done()); 29 | 30 | app.listen("[::]:8080").unwrap(); 31 | 32 | std::thread::park(); 33 | } 34 | -------------------------------------------------------------------------------- /examples/stop.rs: -------------------------------------------------------------------------------- 1 | // m is a macro to let you easily write middleware use closure like JavaScript's arrow function 2 | // it can also convert a async fn to a middleware use the `m!(async_func_name)` syntax. 3 | use amiya::m; 4 | 5 | fn main() { 6 | // Only this stmt is Amiya related code, it sets response to some hello world texts 7 | let app = amiya::new().uses(m!(ctx => 8 | ctx.resp.set_body("Hello World"); 9 | )); 10 | 11 | let _ = app.listen("[::]:8080").unwrap(); 12 | 13 | std::thread::park(); 14 | } 15 | -------------------------------------------------------------------------------- /examples/subapp.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use { 4 | amiya::{m, middleware::Router}, 5 | common::response, 6 | }; 7 | 8 | fn main() { 9 | #[rustfmt::skip] 10 | let api_server = Router::new().at("v1") 11 | // When we do not want to set the exact match handler, you can directly call a new `at` 12 | // to start sub router table setting. 13 | // Then a call to `get` means we set the exact match handler for "/v1/login" 14 | .at("login").get(m!(ctx => response("Login V1 called\n", ctx).await)).done() 15 | // As the same as login, we set the exact match handler for "/v1/logout" 16 | .at("logout").get(m!(ctx => response("Logout V1 called\n", ctx).await)).done() 17 | // Finish "/v1" sub router setting 18 | .done(); 19 | 20 | #[rustfmt::skip] 21 | let static_files_server = amiya::new() 22 | .uses(m!(ctx => 23 | println!("someone visit static file server"); 24 | ctx.next().await?; 25 | )) 26 | .uses(Router::new() 27 | // `endpoint` enter exact match handler setting context for new router. For sub router 28 | // (when use `at`) we do not call it explicit 29 | .endpoint().get(m!(ctx => response("We do not allow list dir", ctx).await)) 30 | .fallback().get(m!(ctx => response(format!("Get file {}\n", ctx.path()), ctx).await)) 31 | // Do not needs a `done` here, because we are setting router itself, not sub router 32 | ); 33 | 34 | #[rustfmt::skip] 35 | let app = amiya::new().uses(Router::new() 36 | // `is` use the middleware you give as the path's handler, no matter exact match or sub match 37 | .at("api").is(api_server) 38 | // You can use another Amiya server as a middleware too, 39 | // so the static files server handler all request under "/static" path 40 | .at("static").is(static_files_server)); 41 | 42 | app.listen("[::]:8080").unwrap(); 43 | 44 | std::thread::park(); 45 | } 46 | -------------------------------------------------------------------------------- /examples/tokio_executor.rs: -------------------------------------------------------------------------------- 1 | use { 2 | amiya::{m, Executor}, 3 | tokio::runtime::Runtime, 4 | }; 5 | 6 | // Due to orphan rule, we need a wrapper. 7 | // See: https://github.com/Ixrec/rust-orphan-rules 8 | struct TokioExecutor(Runtime); 9 | 10 | // Implement this trait for use your custom async executor with Amiya. 11 | impl Executor for TokioExecutor { 12 | fn spawn( 13 | &self, future: impl futures_lite::Future + Send + 'static, 14 | ) { 15 | self.0.spawn(future); 16 | } 17 | 18 | fn block_on(&self, future: impl std::future::Future) -> T { 19 | self.0.block_on(future) 20 | } 21 | } 22 | 23 | fn main() { 24 | #[rustfmt::skip] 25 | let app = amiya::new() 26 | // With your custom executor, we can disable the "builtin-executor" feature 27 | // you can run this file with `--no-default-features`, try it. 28 | .uses(m!(ctx => 29 | ctx.resp.set_body(format!("Hello World from: {}", ctx.path())); 30 | )); 31 | 32 | // Start the task in the multi-thread executor too 33 | app.listen("[::]:8080").unwrap(); 34 | 35 | std::thread::park(); 36 | } 37 | -------------------------------------------------------------------------------- /examples/urlencoded.rs: -------------------------------------------------------------------------------- 1 | use { 2 | amiya::{m, middleware::Router, Context, Result, StatusCode}, 3 | serde::{Deserialize, Serialize}, 4 | serde_json::{Map, Value}, 5 | }; 6 | 7 | async fn parse_body_urlencoded(mut ctx: Context<'_, ()>) -> Result { 8 | if let Some(ct) = ctx.req.header("content-type") { 9 | if ct.as_str().starts_with("application/x-www-form-urlencoded") { 10 | let body: Map = ctx.body().unwrap().into_form().await?; 11 | ctx.resp.set_body(Value::Object(body)); 12 | return Ok(()); 13 | } 14 | } 15 | 16 | ctx.resp.set_status(StatusCode::BadRequest); 17 | Ok(()) 18 | } 19 | 20 | #[derive(Debug, Deserialize, Serialize)] 21 | struct SendCommentBody { 22 | uid: String, 23 | attitude: Option, 24 | comment: String, 25 | } 26 | 27 | async fn parse_body_struct(mut ctx: Context<'_, ()>) -> Result { 28 | if let Some(ct) = ctx.req.header("content-type") { 29 | if ct.as_str().starts_with("application/x-www-form-urlencoded") { 30 | let body: SendCommentBody = if let Ok(body) = ctx.body().unwrap().into_form().await { 31 | body 32 | } else { 33 | ctx.resp.set_status(StatusCode::BadRequest); 34 | return Ok(()); 35 | }; 36 | ctx.resp.set_body(serde_json::to_value(body)?); 37 | return Ok(()); 38 | } 39 | } 40 | 41 | ctx.resp.set_status(StatusCode::BadRequest); 42 | Ok(()) 43 | } 44 | 45 | fn main() { 46 | #[rustfmt::skip] 47 | let router = Router::new() 48 | .at("object").post(m!(parse_body_urlencoded)).done() 49 | .at("struct").post(m!(parse_body_struct)).done(); 50 | 51 | let app = amiya::new().uses(router); 52 | 53 | app.listen("[::]:8080").unwrap(); 54 | 55 | std::thread::park(); 56 | } 57 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | fn_args_layout = "Compressed" 3 | newline_style = "Unix" 4 | use_field_init_shorthand = true 5 | use_small_heuristics = "Max" 6 | use_try_shorthand = true 7 | -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{Middleware, Request, Response, Result}, 3 | http_types::Body, 4 | std::{borrow::Cow, collections::HashMap, sync::Arc}, 5 | }; 6 | 7 | /// The context middleware works on. 8 | #[allow(missing_debug_implementations)] 9 | pub struct Context<'x, Ex> { 10 | /// The incoming http request, without body. You can use [`Context::body`] method to get body. 11 | /// 12 | /// [`Context::body`]: #method.body 13 | pub req: &'x Request, 14 | /// The output http response, you can directly edit it 15 | pub resp: &'x mut Response, 16 | /// User defined extra data 17 | pub ex: &'x mut Ex, 18 | pub(crate) body: &'x mut Option, 19 | pub(crate) remain_path: &'x str, 20 | pub(crate) router_matches: &'x mut HashMap, String>, 21 | pub(crate) tail: &'x [Arc>], 22 | } 23 | 24 | impl<'x, Ex> Context<'x, Ex> 25 | where 26 | Ex: Send + Sync + 'static, 27 | { 28 | /// Run all inner middleware, this method drives the middleware system. 29 | /// 30 | /// Notice that you are **not must** call this func in all middleware. if you do not call it 31 | /// inner middleware will simply not be executed. 32 | /// 33 | /// A second call to this method on the same instance will do nothing and directly returns a 34 | /// `Ok(())`. 35 | /// 36 | /// ## Errors 37 | /// 38 | /// it returns inner middleware execute result. 39 | pub async fn next(&mut self) -> Result { 40 | if let Some((current, tail)) = self.tail.split_first() { 41 | self.tail = tail; 42 | let next_ctx = Context { 43 | req: self.req, 44 | body: self.body, 45 | resp: self.resp, 46 | ex: self.ex, 47 | remain_path: self.remain_path, 48 | router_matches: self.router_matches, 49 | tail, 50 | }; 51 | current.handle(next_ctx).await 52 | } else { 53 | Ok(()) 54 | } 55 | } 56 | 57 | /// Get incoming request body data. Only the first call will return `Some`. 58 | pub fn body(&mut self) -> Option { 59 | self.body.take() 60 | } 61 | 62 | /// The path the next router can match. 63 | /// 64 | /// It's differ from `Context.req.url().path()`, path returned by this method will only contains 65 | /// sub paths that haven't matched by any [`Router`] middleware. 66 | /// 67 | /// See [`examples/router.rs`] for a example. 68 | /// 69 | /// [`Router`]: middleware/struct.Router.html 70 | /// [`examples/router.rs`]: https://github.com/7sDream/amiya/blob/master/examples/router.rs 71 | #[must_use] 72 | pub fn path(&self) -> &str { 73 | self.remain_path 74 | } 75 | 76 | /// The path argument of `name`. 77 | /// 78 | /// Will be set if a router's any item `{name}` is matched. 79 | /// 80 | /// See *[Router - Any Item]* for more detail. 81 | /// 82 | /// ## Examples 83 | /// 84 | /// See [`examples/arg.rs`] for a example. 85 | /// 86 | /// [Router - Any Item]: middleware/struct.Router.html#any-item 87 | /// [`examples/arg.rs`]: https://github.com/7sDream/amiya/blob/master/examples/arg.rs 88 | pub fn arg>(&self, name: K) -> Option<&str> { 89 | self.router_matches.get(name.as_ref()).map(String::as_str) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/executor.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | 3 | #[cfg(feature = "built-in-executor")] 4 | use {async_executor::Executor as AsyncExecutor, async_io::block_on, once_cell::sync::Lazy}; 5 | 6 | /// Provide you custom async executor to Amiya by impl this trait. 7 | /// 8 | /// Amiya instance will use it's [`block_on`] method when listen socket and use [`spawn`] method to 9 | /// start new request handler task. 10 | /// 11 | /// See [`Amiya::executor()`]. 12 | /// 13 | /// [`spawn`]: #method.spawn 14 | //. [`block_on`]: #method.block_on 15 | /// [`Amiya::executor()`]: struct.Amiya.html#method.executor 16 | pub trait Executor: Send + Sync { 17 | /// Spawn a new task to your executor, let it run in background. 18 | fn spawn(&self, future: impl Future + Send + 'static); 19 | 20 | /// Run a future until complete and returns it's result. 21 | fn block_on(&self, future: impl Future) -> T; 22 | } 23 | 24 | #[cfg(feature = "built-in-executor")] 25 | static BUILTIN_EXECUTOR: Lazy> = Lazy::new(|| { 26 | let ex = AsyncExecutor::new(); 27 | for n in 1..=num_cpus::get() { 28 | std::thread::Builder::new() 29 | .name(format!("amiya-builtin-executor-{}", n)) 30 | .spawn(|| loop { 31 | std::panic::catch_unwind(|| { 32 | async_io::block_on(BUILTIN_EXECUTOR.run(std::future::pending::<()>())) 33 | }) 34 | .ok(); 35 | }) 36 | .expect("cannot spawn executor thread"); 37 | } 38 | ex 39 | }); 40 | 41 | /// Amiya built-in multi-thread async executor. 42 | /// 43 | /// All instances of this type share one static executor under the hood. 44 | /// The inner executor starts `N` threads to run async task, `N` is count of your cpu cores. 45 | /// 46 | /// In most case, you do not used this type directly, all created Amiya server have a instance of 47 | /// it by default. 48 | /// 49 | /// ## Notice 50 | /// 51 | /// If you disable the `built-in-executor` default feature, you need to call [`Amiya::executor()`] 52 | /// with your custom executor. Otherwise `Amiya::listen()` will not compile. 53 | /// 54 | /// [`Amiya::executor()`]: struct.Amiya.html#method.executor 55 | #[derive(Debug, Default, Clone)] 56 | pub struct BuiltInExecutor; 57 | 58 | #[cfg(feature = "built-in-executor")] 59 | impl Executor for BuiltInExecutor { 60 | fn spawn(&self, future: impl Future + Send + 'static) { 61 | BUILTIN_EXECUTOR.spawn(future).detach() 62 | } 63 | 64 | fn block_on(&self, future: impl Future) -> T { 65 | block_on(BUILTIN_EXECUTOR.run(future)) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Amiya is a experimental middleware-based minimalism async HTTP server framework, 2 | //! built up on [`smol-rs`] related asynchronous components. 3 | //! 4 | //! It's currently still working in progress and in a very early alpha stage. 5 | //! 6 | //! API design may changes every day, **DO NOT** use it in any condition except for test or study! 7 | //! 8 | //! ## Goal 9 | //! 10 | //! The goal of this project is try to build a (by importance order): 11 | //! 12 | //! - Safe, with `#![forbid(unsafe_code)]` 13 | //! - Async 14 | //! - Middleware-based 15 | //! - Minimalism 16 | //! - Easy to use 17 | //! - Easy to extend 18 | //! 19 | //! HTTP framework for myself to write simple web services. 20 | //! 21 | //! Amiya uses [`async-h1`] to parse and process requests, so only HTTP version 1.1 is supported for 22 | //! now. HTTP 1.0 or 2.0 is not in goal list, at least in the near future. 23 | //! 24 | //! Performance is **NOT** in the list too, after all, Amiya is just a experimental for now, it uses 25 | //! many heap alloc (Box) and dynamic dispatch (Trait Object) so there may be some performance loss 26 | //! compare to use `async-h1` directly. 27 | //! 28 | //! ## Concepts 29 | //! 30 | //! To understand how this framework works, there are some concept need to be described first. 31 | //! 32 | //! ### Request, Response and the process pipeline 33 | //! 34 | //! For every HTTP request comes to a Amiya server, the framework will create a [`Request`] struct 35 | //! to represent it. It's immutable in the whole request process pipeline. 36 | //! 37 | //! And a [`Response`] is created at the same time. It's a normal `200 OK` empty header empty body 38 | //! response at first, but it's mutable and can be edit by middleware. 39 | //! 40 | //! After all middleware has been executed, the [`Response`] maybe edited by many middleware, and 41 | //! as the final result we will send to the client. 42 | //! 43 | //! ### [`Middleware`] 44 | //! 45 | //! For ease of understanding, you can think this word is a abbreviation of "A function read 46 | //! some property of [`Request`] and edit [`Response`]" or, a request handler, for now. 47 | //! 48 | //! ### [`Context`] 49 | //! 50 | //! But middleware do not works on [`Request`] and [`Response`] directly. [`Context`] wraps the 51 | //! immutable [`Request`] and mutable [`Response`] with some other information and shortcut 52 | //! methods. 53 | //! 54 | //! ### Onion model 55 | //! 56 | //! The execution process of middleware uses the onion model: 57 | //! 58 | //! ![][img-onion-model] 59 | //! 60 | //! *We reuse this famous picture from [Python's Pylons framework][Pylons-concept-middleware].* 61 | //! 62 | //! If we add middleware A, B and C to Amiya server, the running order(if not interrupted in the 63 | //! middle) will be: A -> B -> C -> C -> B -> A 64 | //! 65 | //! So every middleware will be executed twice, but this does not mean same code is executed twice. 66 | //! 67 | //! That's why [`next`] method exists. 68 | //! 69 | //! ### [`next`] 70 | //! 71 | //! The most important method [`Context`] gives us is [`next`]. 72 | //! 73 | //! When a middleware calls `ctx.next().await`, the method will return after all inner middleware 74 | //! finish, or, some of them returns a Error. 75 | //! 76 | //! there is a simplest example: 77 | //! 78 | //! ``` 79 | //! use amiya::{Context, Result, m}; 80 | //! 81 | //! async fn a(mut ctx: Context<'_, ()>) -> Result { 82 | //! println!("A - before"); 83 | //! ctx.next().await?; 84 | //! println!("A - out"); 85 | //! Ok(()) 86 | //! } 87 | //! 88 | //! async fn b(mut ctx: Context<'_, ()>) -> Result { 89 | //! println!("B - before"); 90 | //! ctx.next().await?; 91 | //! println!("B - out"); 92 | //! Ok(()) 93 | //! } 94 | //! 95 | //! async fn c(mut ctx: Context<'_, ()>) -> Result { 96 | //! println!("C - before"); 97 | //! ctx.next().await?; 98 | //! println!("C - out"); 99 | //! Ok(()) 100 | //! } 101 | //! 102 | //! let amiya = amiya::new().uses(m!(a)).uses(m!(b)).uses(m!(c)); 103 | //! ``` 104 | //! 105 | //! When a request in, the output will be: 106 | //! 107 | //! ```console 108 | //! A - before 109 | //! B - before 110 | //! C - before 111 | //! C - after 112 | //! B - after 113 | //! A - after 114 | //! ``` 115 | //! 116 | //! You can referrer to [`examples/middleware.rs`] for a more meaningful example. 117 | //! 118 | //! ### Middleware, the truth 119 | //! 120 | //! So with the help of [`next`] method, a middleware can not only be a request handler, it can be: 121 | //! 122 | //! - a error handler, by capture inner middleware returned [`Result`] 123 | //! - a [`Router`], by looking the path then delegate [`Context`] to other corresponding middleware 124 | //! - a access logger or [time measurer], by print log before and after the [`next`] call 125 | //! - etc... 126 | //! 127 | //! A middleware even does not have to call [`next`], in that situation no inner middleware will 128 | //! be executed. Middleware like [`Router`] or login state checker can use this mechanism to make 129 | //! invalid requests respond early. 130 | //! 131 | //! You can create you own [`Middleware`] by implement the trait for your type, or using the [`m`] 132 | //! macro, see their document for detail. 133 | //! 134 | //! ## Examples 135 | //! 136 | //! To start a very simple HTTP service that returns `Hello World` to the client in all paths: 137 | //! 138 | //! ``` 139 | //! use amiya::m; 140 | //! 141 | //! let app = amiya::new().uses(m!(ctx => 142 | //! ctx.resp.set_body(format!("Hello World from: {}", ctx.path())); 143 | //! )); 144 | //! 145 | //! app.listen("[::]:8080").unwrap(); 146 | //! 147 | //! // ... do other things ... 148 | //! ``` 149 | //! 150 | //! Amiya has a built-in multi-thread async executor powered by `async-executor` and `async-io`, 151 | //! amiya server will run in it. So `Amiya::listen` do not block your thread. 152 | //! 153 | //! See *[Readme - Examples]* section for more examples to check. 154 | //! 155 | //! [`Request`]: struct.Request.html 156 | //! [`Response`]: struct.Response.html 157 | //! [`Middleware`]: middleware/trait.Middleware.html 158 | //! [`Context`]: struct.Context.html 159 | //! [`next`]: struct.Context.html#method.next 160 | //! [`Result`]: type.Result.html 161 | //! [`Router`]: middleware/struct.Router.html 162 | //! [`m`]: macro.m.html 163 | //! 164 | //! [`smol-rs`]: https://github.com/smol-rs 165 | //! [`async-h1`]: https://github.com/http-rs/async-h1 166 | //! [img-onion-model]: https://rikka.7sdre.am/files/774eff6f-9368-48d6-8bd2-1b547a74bc23.jpeg 167 | //! [Pylons-concept-middleware]: https://docs.pylonsproject.org/projects/pylons-webframework/en/latest/concepts.html#wsgi-middleware 168 | //! [`examples/middleware.rs`]: https://github.com/7sDream/amiya/blob/master/examples/middleware.rs 169 | //! [time measurer]: https://github.com/7sDream/amiya/blob/master/examples/measurer.rs 170 | //! [Readme - Examples]: https://github.com/7sDream/amiya#examples 171 | 172 | #![deny(warnings)] 173 | #![deny(clippy::all, clippy::pedantic, clippy::nursery)] 174 | #![deny(missing_debug_implementations, rust_2018_idioms)] 175 | #![forbid(unsafe_code, missing_docs)] 176 | #![allow(clippy::module_name_repetitions)] 177 | 178 | mod context; 179 | mod executor; 180 | pub mod middleware; 181 | 182 | use { 183 | async_channel::{Receiver, Sender}, 184 | async_net::TcpListener, 185 | std::{collections::HashMap, io, net::ToSocketAddrs, sync::Arc}, 186 | }; 187 | 188 | pub use { 189 | async_trait::async_trait, 190 | context::Context, 191 | executor::{BuiltInExecutor, Executor}, 192 | http_types::{Method, Mime, Request, Response, StatusCode}, 193 | middleware::Middleware, 194 | }; 195 | 196 | /// The Result type all middleware should returns. 197 | pub type Result = http_types::Result; 198 | 199 | /// The Error type of middleware result type. 200 | pub type Error = http_types::Error; 201 | 202 | type MiddlewareList = Vec>>; 203 | 204 | /// Create a [`Amiya`] instance with extra data type `()`. 205 | /// 206 | /// [`Amiya`]: struct.Amiya.html 207 | #[must_use] 208 | pub fn new() -> Amiya { 209 | Amiya::default() 210 | } 211 | 212 | /// Create a [`Amiya`] instance with user defined extra data. 213 | /// 214 | /// [`Amiya`]: struct.Amiya.html 215 | #[must_use] 216 | pub fn with_ex() -> Amiya { 217 | Amiya::default() 218 | } 219 | 220 | /// Amiya HTTP Server. 221 | /// 222 | /// Amiya itself also implement the [`Middleware`] trait and can be added to another Amiya 223 | /// instance, see [`examples/subapp.rs`] for a example. 224 | /// 225 | /// [`Middleware`]: middleware/trait.Middleware.html 226 | /// [`examples/subapp.rs`]: https://github.com/7sDream/amiya/blob/master/examples/subapp.rs 227 | #[allow(missing_debug_implementations)] 228 | pub struct Amiya { 229 | executor: Exec, 230 | middleware_list: MiddlewareList, 231 | } 232 | 233 | impl Default for Amiya { 234 | fn default() -> Self { 235 | Self::new() 236 | } 237 | } 238 | 239 | impl Amiya { 240 | /// Create a [`Amiya`] instance. 241 | /// 242 | /// [`Amiya`]: struct.Amiya 243 | #[must_use] 244 | pub fn new() -> Self { 245 | Self { executor: BuiltInExecutor, middleware_list: MiddlewareList::default() } 246 | } 247 | } 248 | 249 | // `executor`'s return type muse use type name `Amiya`, there are some false 250 | // positive in `clippy:use_self` lint. 251 | // See: https://rust-lang.github.io/rust-clippy/master/index.html#use_self 252 | // TODO: remove after this false positive is fixed 253 | #[allow(clippy::use_self)] 254 | impl Amiya 255 | where 256 | Ex: Send + Sync + 'static, 257 | { 258 | /// Add a middleware to the end, middleware will be executed as the order of be added. 259 | /// 260 | /// You can create middleware by implement the [`Middleware`] trait 261 | /// for your custom type or use the [`m`] macro to convert a async func or closure. 262 | /// 263 | /// ## Examples 264 | /// 265 | /// ``` 266 | /// use amiya::m; 267 | /// 268 | /// amiya::new().uses(m!(ctx => ctx.next().await)); 269 | /// ``` 270 | /// 271 | /// ``` 272 | /// use amiya::{m, middleware::Router}; 273 | /// 274 | /// let router = Router::new().endpoint().get(m!( 275 | /// ctx => ctx.resp.set_body("Hello world!"); 276 | /// )); 277 | /// 278 | /// amiya::new().uses(router); 279 | /// ``` 280 | /// 281 | /// [`Middleware`]: middleware/trait.Middleware.html 282 | /// [`m`]: macro.m.html 283 | pub fn uses + 'static>(mut self, middleware: M) -> Self { 284 | self.middleware_list.push(Arc::new(middleware)); 285 | self 286 | } 287 | 288 | /// Set the executor. 289 | /// 290 | /// Normal users do not need to call this method because Amiya has a built-in multi-thread 291 | /// executor [`BuiltInExecutor`]. This method let you change it to your custom one. 292 | /// 293 | /// Your executor needs to implement the [`Executor`] trait. 294 | /// 295 | /// See [`examples/tokio_executor.rs`] for an example of use tokio async runtime. 296 | /// 297 | /// [`BuiltInExecutor`]: struct.BuiltInExecutor.html 298 | /// [`Executor`]: trait.Executor.html 299 | /// [`examples/tokio_executor.rs`]: https://github.com/7sDream/amiya/blob/master/examples/tokio_executor.rs 300 | pub fn executor(self, executor: NewExec) -> Amiya { 301 | Amiya { executor, middleware_list: self.middleware_list } 302 | } 303 | } 304 | 305 | impl Amiya 306 | where 307 | Exec: Executor + 'static, 308 | Ex: Default + Send + Sync + 'static, 309 | { 310 | async fn serve(tail: Arc>, mut req: Request) -> Result { 311 | let mut ex = Ex::default(); 312 | let mut resp = Response::new(StatusCode::Ok); 313 | let mut router_matches = HashMap::new(); 314 | let mut body = Some(req.take_body()); 315 | let mut ctx = Context { 316 | req: &req, 317 | body: &mut body, 318 | resp: &mut resp, 319 | ex: &mut ex, 320 | tail: &tail, 321 | remain_path: req.url().path(), 322 | router_matches: &mut router_matches, 323 | }; 324 | ctx.next().await?; 325 | Ok(resp) 326 | } 327 | 328 | async fn accepter( 329 | listener: TcpListener, executor: Arc, middleware_list: MiddlewareList, 330 | stop: Receiver<()>, 331 | ) { 332 | let middleware_list = Arc::new(middleware_list); 333 | let mut forever = false; 334 | loop { 335 | let check_stop = if forever { 336 | Ok(listener.accept().await) 337 | } else { 338 | let stop_fut = async { Err(stop.recv().await) }; 339 | let accept_fut = async { Ok(listener.accept().await) }; 340 | futures_lite::future::or(stop_fut, accept_fut).await 341 | }; 342 | match check_stop { 343 | // accept wins 344 | Ok(listener_result) => match listener_result { 345 | Ok((stream, client_addr)) => { 346 | let middleware_list = Arc::clone(&middleware_list); 347 | let serve = async_h1::accept(stream, move |mut req| { 348 | req.set_peer_addr(Some(client_addr)); 349 | Self::serve(Arc::clone(&middleware_list), req) 350 | }); 351 | executor.spawn(async move { 352 | if let Err(e) = serve.await { 353 | log::error!( 354 | "Request handle error: code = {}, type = {}, detail = {}", 355 | e.status(), 356 | e.type_name().unwrap_or("Unknown"), 357 | e, 358 | ); 359 | } 360 | }); 361 | } 362 | Err(e) => { 363 | log::warn!("Accept connection failed: {:?}", e); 364 | } 365 | }, 366 | // stop signal wins 367 | Err(signal) => { 368 | if signal.is_err() { 369 | // channel closed, user want the server runs forever 370 | forever = true; 371 | } else { 372 | // received stop signal 373 | log::info!("Amiya server stop listening {:?}", listener.local_addr()); 374 | return; 375 | } 376 | } 377 | } 378 | } 379 | } 380 | 381 | /// start Amiya server on given `addr`. 382 | /// 383 | /// ## Return 384 | /// 385 | /// A bounded 1 capacity channel for stop the server. 386 | /// 387 | /// Amiya server will stop listening the `addr` when receive message from this channel. 388 | /// 389 | /// ## Examples 390 | /// 391 | /// ``` 392 | /// amiya::new().listen("127.0.0.1:8080"); 393 | /// ``` 394 | /// 395 | /// ``` 396 | /// amiya::new().listen(("127.0.0.1", 8080)); 397 | /// ``` 398 | /// 399 | /// ``` 400 | /// use std::net::Ipv4Addr; 401 | /// 402 | /// amiya::new().listen((Ipv4Addr::new(127, 0, 0, 1), 8080)); 403 | /// ``` 404 | /// 405 | /// ``` 406 | /// use std::net::{SocketAddrV4, Ipv4Addr}; 407 | /// 408 | /// let socket = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 8080); 409 | /// amiya::new().listen(socket); 410 | /// ``` 411 | /// 412 | /// ``` 413 | /// amiya::new().listen("[::]:8080"); 414 | /// ``` 415 | /// 416 | /// ``` 417 | /// let stop = amiya::new().listen("[::]:8080").unwrap(); 418 | /// // do other things 419 | /// stop.try_send(()); // amiya http server will stop 420 | /// ``` 421 | /// 422 | /// # Errors 423 | /// 424 | /// When listen provided address and port failed. 425 | pub fn listen(self, addr: A) -> io::Result> { 426 | let addr = addr.to_socket_addrs()?.next().unwrap(); 427 | let listener = self.executor.block_on(TcpListener::bind(addr))?; 428 | 429 | log::info!("Amiya server start listening {:?}", listener.local_addr()); 430 | 431 | let executor = Arc::new(self.executor); 432 | 433 | let (tx, rx) = async_channel::bounded::<()>(1); 434 | executor.spawn(Self::accepter(listener, Arc::clone(&executor), self.middleware_list, rx)); 435 | Ok(tx) 436 | } 437 | } 438 | 439 | #[async_trait] 440 | impl Middleware for Amiya 441 | where 442 | Exec: Send + Sync, 443 | Ex: Send + Sync + 'static, 444 | { 445 | async fn handle(&self, mut ctx: Context<'_, Ex>) -> Result { 446 | let mut self_ctx = Context { 447 | req: ctx.req, 448 | body: ctx.body, 449 | resp: ctx.resp, 450 | ex: ctx.ex, 451 | tail: &self.middleware_list[..], 452 | remain_path: ctx.remain_path, 453 | router_matches: ctx.router_matches, 454 | }; 455 | self_ctx.next().await?; 456 | ctx.next().await 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /src/middleware/m.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{async_trait, Context, Middleware, Result}, 3 | std::{future::Future, pin::Pin}, 4 | }; 5 | 6 | type BoxedResultFut<'x> = Pin + Send + 'x>>; 7 | 8 | /// The wrapper for use async function or closure as a middleware. 9 | /// 10 | /// This is the type when you use macro [`m`] , **Do Not** use this type directly! 11 | /// 12 | /// [`m`]: ../macro.m.html 13 | #[allow(missing_debug_implementations)] 14 | pub struct M { 15 | /// the code in macro [`m`], converted to a boxed async func. 16 | /// 17 | /// **Do Not** set this field by hand, use macro [`m`] instead! 18 | /// 19 | /// [`m`]: ../macro.m.html 20 | pub func: Box) -> BoxedResultFut<'_> + Send + Sync>, 21 | } 22 | 23 | #[async_trait] 24 | impl Middleware for M 25 | where 26 | Ex: Send + Sync + 'static, 27 | { 28 | async fn handle(&self, ctx: Context<'_, Ex>) -> Result<()> { 29 | (self.func)(ctx).await 30 | } 31 | } 32 | 33 | /// Writer middleware easily. 34 | /// 35 | /// It's a macro to let you easily write middleware use closure and syntax like JavaScript's 36 | /// arrow function, or convert a async fn to a middleware use the `m!(async_func_name)` syntax. 37 | /// 38 | /// It returns a [`M`] instance, which implement [`Middleware`] trait. 39 | /// 40 | /// ## Examples 41 | /// 42 | /// ### Convert a async function to middleware 43 | /// 44 | /// ``` 45 | /// # use amiya::{Context, Result, m}; 46 | /// async fn response(mut ctx: Context<'_, ()>) -> Result { 47 | /// ctx.next().await?; 48 | /// ctx.resp.set_body("Hello world"); 49 | /// Ok(()) 50 | /// } 51 | /// 52 | /// let app = amiya::new().uses(m!(response)); 53 | /// ``` 54 | /// 55 | /// ### Convert a block to middleware 56 | /// 57 | /// Syntax: ` [: Extra data type] => { }` 58 | /// 59 | /// Default extra data type is `()`, same bellow. 60 | /// 61 | /// ``` 62 | /// # use amiya::m; 63 | /// // | this `: ()` can be omitted 64 | /// // v 65 | /// let app = amiya::new().uses(m!(ctx: () => { 66 | /// ctx.next().await?; 67 | /// ctx.resp.set_body("Hello world"); 68 | /// Ok(()) 69 | /// })); 70 | /// ``` 71 | /// 72 | /// ### Convert a expr to middleware 73 | /// 74 | /// Syntax: ` [: Extra data type] => ` 75 | /// 76 | /// ``` 77 | /// # use amiya::{Context, Result, m}; 78 | /// async fn response(msg: &'static str, mut ctx: Context<'_, ()>) -> Result { 79 | /// ctx.next().await?; 80 | /// ctx.resp.set_body(msg); 81 | /// Ok(()) 82 | /// } 83 | /// 84 | /// let app = amiya::new().uses(m!(ctx => response("Hello World", ctx).await)); 85 | /// ``` 86 | /// 87 | /// ### Convert statements to middleware 88 | /// 89 | /// Syntax: ` [: Extra data type] => ` 90 | /// 91 | /// ``` 92 | /// # use amiya::m; 93 | /// let app = amiya::new().uses(m!(ctx => ctx.resp.set_body("Hello World");)); 94 | /// ``` 95 | /// 96 | /// Notice you do not return a value here, because a `Ok(())` is auto added. 97 | /// 98 | /// This is expand to: 99 | /// 100 | /// ```text 101 | /// ctx.resp.set_body("Hello World"); 102 | /// Ok(()) 103 | /// ``` 104 | /// 105 | /// [`M`]: middleware/struct.M.html 106 | /// [`Middleware`]: middleware/trait.Middleware.html 107 | #[macro_export] 108 | macro_rules! m { 109 | // Convert a async function to middleware by function name 110 | 111 | ($func: ident) => { 112 | $crate::middleware::M { func: Box::new(|ctx| Box::pin($func(ctx))) } 113 | }; 114 | 115 | // Convert a block 116 | 117 | ($ctx: ident : $ex: ty => $body: block ) => { 118 | $crate::middleware::M { 119 | func: Box::new(move |mut $ctx: $crate::Context<'_, $ex>| { 120 | Box::pin(async move { $body }) 121 | }), 122 | } 123 | }; 124 | 125 | ($ctx: ident => $body: block) => { 126 | m!($ctx: () => $body) 127 | }; 128 | 129 | // Convert one expr 130 | 131 | ($ctx: ident : $ex: ty => $body: expr) => { 132 | m!($ctx: $ex => { $body }) 133 | }; 134 | 135 | ($ctx: ident => $body: expr) => { 136 | m!($ctx => { $body }) 137 | }; 138 | 139 | // Convert statements 140 | 141 | ($ctx: ident : $ex: ty => $($body: tt)+) => { 142 | m!($ctx: $ex => { $($body)+ ; Ok(()) }) 143 | }; 144 | 145 | ($ctx: ident => $($body: tt)+) => { 146 | m!($ctx => { $($body)+ ; Ok(()) }) 147 | }; 148 | } 149 | -------------------------------------------------------------------------------- /src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | //! Built-in middleware. 2 | 3 | mod m; 4 | mod router; 5 | 6 | use { 7 | crate::{Context, Result}, 8 | async_trait::async_trait, 9 | }; 10 | 11 | pub use { 12 | m::M, 13 | router::{MethodRouter, Router, RouterSetter}, 14 | }; 15 | 16 | /// Use your custom type as a middleware by implement this trait. 17 | /// 18 | /// You need [`async_trait`] to implement this. 19 | /// 20 | /// See [`examples/measurer.rs`] for a example of process time usage measurer middleware. 21 | /// 22 | /// [`async_trait`]: https://github.com/dtolnay/async-trait 23 | /// [`examples/measurer.rs`]: https://github.com/7sDream/amiya/blob/master/examples/measurer.rs 24 | #[async_trait] 25 | pub trait Middleware: Send + Sync { 26 | /// Your middleware handler function, it will be called when request reach this middleware 27 | async fn handle(&self, ctx: Context<'_, Ex>) -> Result; 28 | } 29 | -------------------------------------------------------------------------------- /src/middleware/router/like.rs: -------------------------------------------------------------------------------- 1 | use {crate::Middleware, std::borrow::Cow}; 2 | 3 | #[doc(hidden)] 4 | pub trait RouterLike: Sized { 5 | fn set_endpoint + 'static>(&mut self, middleware: M); 6 | fn set_fallback + 'static>(&mut self, middleware: M); 7 | fn insert_to_router_table(&mut self, path: P, middleware: M) 8 | where 9 | P: Into>, 10 | M: Middleware + 'static; 11 | } 12 | 13 | #[doc(hidden)] 14 | #[macro_export] 15 | macro_rules! impl_router_like_pub_fn { 16 | ($ex: ty) => { 17 | /// Enter endpoint edit environment. 18 | #[must_use] 19 | pub fn endpoint( 20 | self, 21 | ) -> $crate::middleware::router::setter::RouterSetter< 22 | Self, 23 | $crate::middleware::router::set_which::SetEndpoint, 24 | $ex, 25 | > { 26 | $crate::middleware::router::setter::RouterSetter::new_endpoint_setter(self) 27 | } 28 | 29 | /// Add a new item for `path` to router table and enter endpoint edit environment of 30 | /// that item. 31 | pub fn at>>( 32 | self, path: P, 33 | ) -> $crate::middleware::router::setter::RouterSetter< 34 | $crate::middleware::router::setter::RouterSetter< 35 | Self, 36 | $crate::middleware::router::set_which::SetTableItem, 37 | $ex, 38 | >, 39 | $crate::middleware::router::set_which::SetEndpoint, 40 | $ex, 41 | > { 42 | $crate::middleware::router::setter::RouterSetter::new_router_table_setter(self, path) 43 | .endpoint() 44 | } 45 | 46 | /// Enter fallback edit environment. 47 | #[must_use] 48 | pub fn fallback( 49 | self, 50 | ) -> $crate::middleware::router::setter::RouterSetter< 51 | Self, 52 | $crate::middleware::router::set_which::SetFallback, 53 | $ex, 54 | > { 55 | $crate::middleware::router::setter::RouterSetter::new_fallback_setter(self) 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/middleware/router/method.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{Context, Method, Middleware, Result, StatusCode}, 3 | async_trait::async_trait, 4 | std::{ 5 | collections::HashMap, 6 | fmt::{self, Debug, Formatter}, 7 | sync::Arc, 8 | }, 9 | }; 10 | 11 | static ALL_METHODS: &[Method] = &[ 12 | Method::Get, 13 | Method::Head, 14 | Method::Post, 15 | Method::Put, 16 | Method::Delete, 17 | Method::Connect, 18 | Method::Options, 19 | Method::Trace, 20 | Method::Patch, 21 | ]; 22 | 23 | #[doc(hidden)] 24 | #[macro_export] 25 | macro_rules! impl_method { 26 | ($(#[$outer:meta])* 27 | $func_name: ident : $method: expr => $ret: ty) => { 28 | $(#[$outer])* 29 | pub fn $func_name + 'static>(self, middleware: M) -> $ret { 30 | self.method($method, middleware) 31 | } 32 | }; 33 | 34 | ($($(#[$outer:meta])* 35 | $func_name: ident : $method: expr),* $(,)? => $ret: ty) => { 36 | $(impl_method!{$(#[$outer])* $func_name: $method => $ret})+ 37 | }; 38 | } 39 | 40 | #[doc(hidden)] 41 | #[macro_export(local_inner_macros)] 42 | macro_rules! impl_all_http_method { 43 | ($ret: ty) => { 44 | impl_method! { 45 | /// A shortcut of `self.method(Method::Get, middleware)`, see [`Self::method`]. 46 | /// 47 | /// [`Self::method`]: #method.method 48 | get: Method::Get, 49 | /// A shortcut of `self.method(Method::Head, middleware)`, see [`Self::method`]. 50 | /// 51 | /// [`Self::method`]: #method.method 52 | head: Method::Head, 53 | /// A shortcut of `self.method(Method::Post, middleware)`, see [`Self::method`]. 54 | /// 55 | /// [`Self::method`]: #method.method 56 | post: Method::Post, 57 | /// A shortcut of `self.method(Method::Put, middleware)`, see [`Self::method`]. 58 | /// 59 | /// [`Self::method`]: #method.method 60 | put: Method::Put, 61 | /// A shortcut of `self.method(Method::Delete, middleware)`, see [`Self::method`]. 62 | /// 63 | /// [`Self::method`]: #method.method 64 | delete: Method::Delete, 65 | /// A shortcut of `self.method(Method::Connect, middleware)`, see [`Self::method`]. 66 | /// 67 | /// [`Self::method`]: #method.method 68 | connect: Method::Connect, 69 | /// A shortcut of `self.method(Method::Options, middleware)`, see [`Self::method`]. 70 | /// 71 | /// [`Self::method`]: #method.method 72 | options: Method::Options, 73 | /// A shortcut of `self.method(Method::Trace, middleware)`, see [`Self::method`]. 74 | /// 75 | /// [`Self::method`]: #method.method 76 | trace: Method::Trace, 77 | /// A shortcut of `self.method(Method::Patch, middleware)`, see [`Self::method`]. 78 | /// 79 | /// [`Self::method`]: #method.method 80 | patch: Method::Patch, 81 | => $ret 82 | } 83 | }; 84 | } 85 | 86 | macro_rules! impl_methods { 87 | ($(#[$outer:meta])* $func_name: ident : $methods: expr) => { 88 | $(#[$outer])* 89 | pub fn $func_name + 'static>(self, middleware: M) -> Self { 90 | self.methods($methods, middleware) 91 | } 92 | }; 93 | 94 | ($($(#[$outer:meta])* $func_name: ident : $methods: expr),* $(,)?) => { 95 | $(impl_methods!{$(#[$outer])* $func_name: $methods})+ 96 | }; 97 | } 98 | 99 | /// The middleware for request diversion by HTTP method. 100 | /// 101 | /// ## Examples 102 | /// 103 | /// ``` 104 | /// # use amiya::{middleware::MethodRouter, m}; 105 | /// // Other HTTP methods that are not set will set resp to `405 Method Not Allowed`. 106 | /// let router = MethodRouter::new() 107 | /// .get(m!(ctx => ctx.resp.set_body("GET method");)) 108 | /// .post(m!(ctx => ctx.resp.set_body("POST method");)); 109 | /// ``` 110 | /// 111 | /// You can set same middleware for different methods by using [`methods`] method. 112 | /// 113 | /// ``` 114 | /// # use amiya::{middleware::MethodRouter, m, Method}; 115 | /// let router = MethodRouter::new() 116 | /// .methods([Method::Get, Method::Post], m!(ctx => ctx.resp.set_body("Hello World");)); 117 | /// ``` 118 | /// 119 | /// [`methods`]: #method.methods 120 | pub struct MethodRouter { 121 | table: HashMap>>, 122 | } 123 | 124 | impl Default for MethodRouter { 125 | fn default() -> Self { 126 | Self { table: HashMap::new() } 127 | } 128 | } 129 | 130 | impl Debug for MethodRouter { 131 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 132 | f.write_str("Method Router { ")?; 133 | for method in self.table.keys() { 134 | ::fmt(method, f)?; 135 | f.write_str(" ")?; 136 | } 137 | f.write_str("}") 138 | } 139 | } 140 | 141 | impl MethodRouter { 142 | /// Create a new `MethodRouter`. 143 | #[must_use] 144 | pub fn new() -> Self { 145 | Self::default() 146 | } 147 | 148 | /// Set given `middleware` as the handler of specific HTTP method when request hit this router. 149 | pub fn method + 'static>(mut self, method: Method, middleware: M) -> Self { 150 | let middleware: Arc> = Arc::new(middleware); 151 | self.table.insert(method, Arc::clone(&middleware)); 152 | self 153 | } 154 | 155 | /// Set given `middleware` as the handler of several HTTP methods when request hit this router. 156 | pub fn methods, M: Middleware + 'static>( 157 | mut self, methods: H, middleware: M, 158 | ) -> Self { 159 | let middleware: Arc> = Arc::new(middleware); 160 | methods.as_ref().iter().for_each(|method| { 161 | self.table.insert(*method, Arc::clone(&middleware)); 162 | }); 163 | self 164 | } 165 | 166 | impl_all_http_method! { Self } 167 | 168 | impl_methods! { 169 | /// Set given `middleware` as the handler of all HTTP method, this method is almost useless 170 | /// because in this case you can use that `middleware` directly and do not need a 171 | /// MethodRouter. 172 | all: ALL_METHODS, 173 | } 174 | } 175 | 176 | #[async_trait] 177 | impl Middleware for MethodRouter 178 | where 179 | Ex: Send + Sync + 'static, 180 | { 181 | async fn handle(&self, ctx: Context<'_, Ex>) -> Result { 182 | if let Some(middleware) = self.table.get(&ctx.req.method()) { 183 | middleware.handle(ctx).await 184 | } else { 185 | ctx.resp.set_status(StatusCode::MethodNotAllowed); 186 | ctx.resp.take_body(); 187 | Ok(()) 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/middleware/router/mod.rs: -------------------------------------------------------------------------------- 1 | //! Router middleware inner types, pub only for document reason. 2 | 3 | use { 4 | crate::{ 5 | impl_router_like_pub_fn, middleware::router::like::RouterLike, Context, Middleware, Result, 6 | StatusCode, 7 | }, 8 | async_trait::async_trait, 9 | std::{borrow::Cow, collections::HashMap}, 10 | }; 11 | 12 | mod like; 13 | mod method; 14 | mod set_which; 15 | mod setter; 16 | 17 | pub use {method::MethodRouter, setter::RouterSetter}; 18 | 19 | /// The middleware for request diversion by path. 20 | /// 21 | /// ## Concepts 22 | /// 23 | /// There also are some important concepts need to be described first. 24 | /// 25 | /// ### [`Router`] 26 | /// 27 | /// A [`Router`] is some component that dispatch your [`Request`] to different handler(inner 28 | /// middleware) by looking it's [`path`]. 29 | /// 30 | /// So a router may store several middleware and choose zero or one of them for you when a request 31 | /// comes. If [`Request`]'s path match a router table item, it delegate [`Context`] to that 32 | /// corresponding middleware and do nothing else. If no item matches, it set [`Response`] to `404 33 | /// Not Found` and no stored middleware will be executed. 34 | /// 35 | /// ### Router Table 36 | /// 37 | /// [`Router`] has a `Path => Middleware` table to decided which middleware is respond for a 38 | /// request. 39 | /// 40 | /// Each table item has different path, if you set a path twice, the new one will replace the 41 | /// first. 42 | /// 43 | /// Each path, is a full part in path when split by `/`, that is, if you set a item `"abc" => 44 | /// middleware A`, the path `/abcde/somesub` will not be treated as a match. Only `/abc`, `/abc/`, 45 | /// `/abc/xxxxx/yyyy/zzzz` will. 46 | /// 47 | /// You can use [`at`] method to edit router table, for example: `at("abc")` will start a router 48 | /// table item edit environment for sub path `/abc`. 49 | /// 50 | /// ### Any Item 51 | /// 52 | /// There is a special router item called `any`, you can set it by use `at("{arg_name}")`. 53 | /// 54 | /// This item will match when all router table items do not match the remain path, and remain path 55 | /// is not just `/`. That is, remain path have sub folder. 56 | /// 57 | /// At this condition, the `any` item will match next sub folder, and store this folder name as a 58 | /// match result in [`Context`], you can use [`Context::arg`] to get it. 59 | /// 60 | /// see [`examples/arg.rs`] for a example code. 61 | /// 62 | /// ### Endpoint 63 | /// 64 | /// Except router table, [`Router`] has a endpoint middleware to handler condition that no more remain 65 | /// path can be used to determine which table item should be used. 66 | /// 67 | /// A example: 68 | /// 69 | /// ``` 70 | /// # use amiya::{middleware::Router, m}; 71 | /// let router = Router::new() 72 | /// .endpoint() 73 | /// .get(m!(ctx => ctx.resp.set_body("hit endpoint");)) 74 | /// .at("v1").is(m!(ctx => ctx.resp.set_body("hit v1");)); 75 | /// let main_router = Router::new().at("api").is(router); 76 | /// 77 | /// let amiya = amiya::new().uses(main_router); 78 | /// ``` 79 | /// 80 | /// The `hit endpoint` will only be returned when request path is exactly `/api`, because after 81 | /// first match by `main_router`, remain path is empty, we can't match sub path on empty string. 82 | /// 83 | /// ### Fallback 84 | /// 85 | /// With the example above, if request path is `/api/`, the endpoint is not called, and because 86 | /// the `v1` item do not match remain path `/` too, so there is a mismatch. 87 | /// 88 | /// If we do not add any code, this request will get a `404 Not Found` response. We have two option 89 | /// too add a handler for this request: 90 | /// 91 | /// 1. use a empty path router table item: `.at("").uses(xxxx)`. 92 | /// 2. use a fallback handler: `.fallback().uses(xxxxx)`. 93 | /// 94 | /// When remain path is not empty, but we can't find a matched router item, the fallback handler 95 | /// will be executed(only if we have set one, of course). 96 | /// 97 | /// So if we choose the second option, the fallback is respond to all mismatched item, sometime 98 | /// this is what you want, and sometime not. Make sure choose the approach meets your need. 99 | /// 100 | /// ## API Design 101 | /// 102 | /// Because router can be nest, with many many levels, we need many code, many temp vars to build 103 | /// a multi level router. For reduce some ugly code, we designed a fluent api to construct this 104 | /// tree structure. 105 | /// 106 | /// As described above, a [`Router`] has three property: 107 | /// 108 | /// - Endpoint handler 109 | /// - Router table 110 | /// - Fallback handler 111 | /// 112 | /// And we have three methods foo them: 113 | /// 114 | /// - [`endpoint`] 115 | /// - [`at`] 116 | /// - [`fallback`] 117 | /// 118 | /// ### Editing Environment 119 | /// 120 | /// Let's start at the simplest method [`fallback`]. 121 | /// 122 | /// When we call [`fallback`] on a router, we do not set the middleware for this property, instead, 123 | /// we enter the fallback editing environment of this router. 124 | /// 125 | /// In a edit environment, we can use several method to finish this editing and exit environment. 126 | /// 127 | /// - any method of a [`MethodRouter`] like [`get`], [`post`], [`delete`], etc.. 128 | /// - `uses` method of that non-public environment type. 129 | /// 130 | /// A finish method consumes the environment, set the property of editing target and returns it. So 131 | /// we can enter other properties' editing environment to make more changes to it. 132 | /// 133 | /// The [`endpoint`] editing environment is almost the same, except it sets the endpoint handler. 134 | /// 135 | /// But [`at`] method has a little difference. It does not enter router table editing environment of 136 | /// `self`, but enter the [`endpoint`] editing environment of that corresponding router table item's 137 | /// middleware, a [`Router`] by default. 138 | /// 139 | /// If we finish this editing, it returns a type representing that Router with `endpoint` set by the 140 | /// finish method. But we do not have to finish it. We can use `is` method to use a 141 | /// custom middleware in that router table item directly. 142 | /// 143 | /// And a Router table item's endpoint editing environment also provided `fallback` and `at` method 144 | /// to enter the default sub Router's editing environment quickly without endpoint be set. 145 | /// 146 | /// if we finish set this sub router, a call of `done` method can actually add this item to parent's 147 | /// router table and returns parent router. 148 | /// 149 | /// example: 150 | /// 151 | /// ``` 152 | /// # use amiya::{Context, middleware::Router, Result, m}; 153 | /// 154 | /// async fn xxx(ctx: Context<'_, ()>) -> Result { Ok(()) } // some middleware func 155 | /// 156 | /// #[rustfmt::skip] 157 | /// let router = Router::new() 158 | /// // | this enter router table item "root"'s default router's endpoint 159 | /// // v editing environment 160 | /// .at("root") 161 | /// // | set "root" router's endpoint only support GET method, use middleware xxx 162 | /// // v this will return a type representing sub router 163 | /// .get(m!(xxx)) 164 | /// // | end 165 | /// .fallback() // <-- enter sub router's fallback editing endpoint 166 | /// // | set sub router's endpoint to use middleware xxx directly 167 | /// // v This method returns the type representing sub router again 168 | /// .uses(m!(xxx)) 169 | /// // | enter sub sub router's endpoint editing environment 170 | /// // v 171 | /// .at("sub") 172 | /// .is(m!(xxx)) // `is` make "sub" path directly uses xxx 173 | /// .done() // `done` finish "root" router editing 174 | /// .at("another") // we can continue add more item to the Router 175 | /// .is(m!(xxx)); // but for short we use a is here and finish router build. 176 | /// ``` 177 | /// 178 | /// Every [`at`] has a matched `done` or `is`, remember this, then you can use this API to build a 179 | /// router tree without any temp variable. 180 | /// 181 | /// Because that `rustfmt` align code using `.`, so all chain method call will have same indent by 182 | /// default. No indent means no multi level view, no level means we need to be very careful when add 183 | /// new path to old router. So I recommend use [`#[rustfmt::skip]`][rustfmt::skip] to prevent 184 | /// `rustfmt` to format the router creating code section and indent router code by hand. 185 | /// 186 | /// ## Examples 187 | /// 188 | /// see [`examples/router.rs`], [`examples/arg.rs`] and [`examples/subapp.rs`]. 189 | /// 190 | /// [`Router`]: #main 191 | /// [`Request`]: ../struct.Request.html 192 | /// [`path`]: ../struct.Context.html#method.path 193 | /// [`Context`]: ../struct.Context.html 194 | /// [`Context::arg`]: ../struct.Context.html#method.arg 195 | /// [`Response`]: ../struct.Response.html 196 | /// [`endpoint`]: #method.endpoint 197 | /// [`at`]: #method.at 198 | /// [`fallback`]: #method.fallback 199 | /// [`MethodRouter`]: struct.MethodRouter.html 200 | /// [`get`]: struct.MethodRouter.html#method.get 201 | /// [`post`]: struct.MethodRouter.html#method.post 202 | /// [`delete`]: struct.MethodRouter.html#method.delete 203 | /// [rustfmt::skip]: https://github.com/rust-lang/rustfmt#tips 204 | /// [`examples/arg.rs`]: https://github.com/7sDream/amiya/blob/master/examples/arg.rs 205 | /// [`examples/router.rs`]: https://github.com/7sDream/amiya/blob/master/examples/router.rs 206 | /// [`examples/subapp.rs`]: https://github.com/7sDream/amiya/blob/master/examples/subapp.rs 207 | #[allow(missing_debug_implementations)] 208 | pub struct Router { 209 | endpoint: Option>>, 210 | fallback: Option>>, 211 | any: Option<(Cow<'static, str>, Box>)>, 212 | table: HashMap, Box>>, 213 | } 214 | 215 | impl Default for Router { 216 | fn default() -> Self { 217 | Self { endpoint: None, fallback: None, any: None, table: HashMap::new() } 218 | } 219 | } 220 | 221 | impl RouterLike for Router { 222 | fn set_endpoint + 'static>(&mut self, middleware: M) { 223 | self.endpoint.replace(Box::new(middleware)); 224 | } 225 | 226 | fn set_fallback + 'static>(&mut self, middleware: M) { 227 | self.fallback.replace(Box::new(middleware)); 228 | } 229 | 230 | fn insert_to_router_table>, M: Middleware + 'static>( 231 | &mut self, path: P, middleware: M, 232 | ) { 233 | let path = path.into(); 234 | if path.starts_with('{') && path.ends_with('}') { 235 | match path { 236 | Cow::Owned(path) => { 237 | let key = &path[1..path.len() - 1]; 238 | self.any.replace((Cow::from(key.to_string()), Box::new(middleware))); 239 | } 240 | Cow::Borrowed(path) => { 241 | let key = &path[1..path.len() - 1]; 242 | self.any.replace((Cow::from(key), Box::new(middleware))); 243 | } 244 | } 245 | } else { 246 | self.table.insert(path, Box::new(middleware)); 247 | } 248 | } 249 | } 250 | 251 | impl Router { 252 | /// Create new Router middleware. 253 | #[must_use] 254 | pub fn new() -> Self { 255 | Self::default() 256 | } 257 | 258 | impl_router_like_pub_fn! { Ex } 259 | } 260 | 261 | #[async_trait] 262 | impl Middleware for Router 263 | where 264 | Ex: Send + Sync + 'static, 265 | { 266 | async fn handle(&self, mut ctx: Context<'_, Ex>) -> Result<()> { 267 | if ctx.remain_path.is_empty() { 268 | if let Some(ref endpoint) = self.endpoint { 269 | return endpoint.handle(ctx).await; 270 | } 271 | } else { 272 | let path = &ctx.remain_path[1..]; 273 | for (target_path, sub_router) in &self.table { 274 | if path.starts_with(target_path.as_ref()) { 275 | if path.len() == target_path.len() { 276 | ctx.remain_path = ""; 277 | } else if path[target_path.len()..].starts_with('/') { 278 | ctx.remain_path = &path[target_path.len()..]; 279 | } else { 280 | continue; 281 | } 282 | return sub_router.handle(ctx).await; 283 | } 284 | } 285 | 286 | if let Some((ref k, ref any)) = self.any { 287 | if !path.is_empty() && !path.starts_with('/') { 288 | let next_slash = path.find('/'); 289 | #[allow(clippy::option_if_let_else)] // use same ctx as mutable 290 | let pos = if let Some(pos) = next_slash { 291 | ctx.remain_path = &path[pos..]; 292 | pos 293 | } else { 294 | ctx.remain_path = ""; 295 | path.len() 296 | }; 297 | let value = &path[0..pos]; 298 | ctx.router_matches.insert(k.clone(), value.to_string()); 299 | return any.handle(ctx).await; 300 | } 301 | } 302 | 303 | if let Some(ref fallback) = self.fallback { 304 | return fallback.handle(ctx).await; 305 | } 306 | } 307 | 308 | ctx.resp.set_status(StatusCode::NotFound); 309 | Ok(()) 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/middleware/router/set_which.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{middleware::router::RouterLike, Middleware}, 3 | std::borrow::Cow, 4 | }; 5 | 6 | pub trait SetWhich { 7 | fn set_to_target(self, router: R, middleware: M) -> R 8 | where 9 | R: RouterLike, 10 | M: Middleware + 'static; 11 | } 12 | 13 | #[derive(Debug)] 14 | pub struct SetEndpoint {} 15 | 16 | impl SetWhich for SetEndpoint { 17 | fn set_to_target(self, mut router: R, middleware: M) -> R 18 | where 19 | R: RouterLike, 20 | M: Middleware + 'static, 21 | { 22 | router.set_endpoint(middleware); 23 | router 24 | } 25 | } 26 | 27 | #[derive(Debug)] 28 | pub struct SetFallback {} 29 | 30 | impl SetWhich for SetFallback { 31 | fn set_to_target(self, mut router: R, middleware: M) -> R 32 | where 33 | R: RouterLike, 34 | M: Middleware + 'static, 35 | { 36 | router.set_fallback(middleware); 37 | router 38 | } 39 | } 40 | 41 | #[derive(Debug)] 42 | pub struct SetTableItem { 43 | pub path: Cow<'static, str>, 44 | } 45 | 46 | impl SetWhich for SetTableItem { 47 | fn set_to_target(self, mut router: R, middleware: M) -> R 48 | where 49 | R: RouterLike, 50 | M: Middleware + 'static, 51 | { 52 | router.insert_to_router_table(self.path, middleware); 53 | router 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/middleware/router/setter.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | impl_all_http_method, impl_method, impl_router_like_pub_fn, 4 | middleware::router::{ 5 | set_which::{SetEndpoint, SetFallback, SetTableItem, SetWhich}, 6 | MethodRouter, Router, RouterLike, 7 | }, 8 | Method, Middleware, 9 | }, 10 | std::borrow::Cow, 11 | }; 12 | 13 | /// Router editing environment. 14 | /// 15 | /// You don’t need to understand this type, it's here just for list it's function. 16 | /// See [`Router`] document for api design and usage. 17 | /// 18 | /// [`Router`]: struct.Router.html 19 | #[allow(missing_debug_implementations)] 20 | pub struct RouterSetter { 21 | router: R, 22 | sub_router: Router, 23 | method_router: MethodRouter, 24 | setter: Sw, 25 | } 26 | 27 | impl RouterSetter 28 | where 29 | R: RouterLike, 30 | { 31 | #[doc(hidden)] 32 | pub fn new_endpoint_setter(router: R) -> Self { 33 | Self { 34 | router, 35 | method_router: MethodRouter::default(), 36 | sub_router: Router::default(), 37 | setter: SetEndpoint {}, 38 | } 39 | } 40 | } 41 | 42 | impl RouterSetter 43 | where 44 | R: RouterLike, 45 | { 46 | #[doc(hidden)] 47 | pub fn new_fallback_setter(router: R) -> Self { 48 | Self { 49 | router, 50 | method_router: MethodRouter::default(), 51 | sub_router: Router::default(), 52 | setter: SetFallback {}, 53 | } 54 | } 55 | } 56 | 57 | #[allow(clippy::use_self)] 58 | impl RouterSetter 59 | where 60 | R: RouterLike, 61 | { 62 | #[doc(hidden)] 63 | pub fn new_router_table_setter>>(router: R, path: P) -> Self { 64 | Self { 65 | router, 66 | method_router: MethodRouter::default(), 67 | sub_router: Router::default(), 68 | setter: SetTableItem { path: path.into() }, 69 | } 70 | } 71 | 72 | impl_router_like_pub_fn! { Ex } 73 | 74 | /// Finish this router table editing. 75 | pub fn done(self) -> R 76 | where 77 | Ex: Send + Sync + 'static, 78 | { 79 | self.setter.set_to_target(self.router, self.sub_router) 80 | } 81 | } 82 | 83 | impl RouterLike for RouterSetter 84 | where 85 | R: RouterLike, 86 | { 87 | fn set_endpoint + 'static>(&mut self, middleware: M) { 88 | self.sub_router.set_endpoint(middleware); 89 | } 90 | 91 | fn set_fallback + 'static>(&mut self, middleware: M) { 92 | self.sub_router.set_fallback(middleware); 93 | } 94 | 95 | fn insert_to_router_table>, M: Middleware + 'static>( 96 | &mut self, path: P, middleware: M, 97 | ) { 98 | self.sub_router.insert_to_router_table(path, middleware); 99 | } 100 | } 101 | 102 | #[allow(clippy::use_self)] 103 | impl RouterSetter, SetEndpoint, Ex> 104 | where 105 | R: RouterLike, 106 | Ex: Send + Sync + 'static, 107 | { 108 | /// Change to fallback editing environment. 109 | pub fn fallback(self) -> RouterSetter, SetFallback, Ex> { 110 | self.router.fallback() 111 | } 112 | 113 | /// Change to inner router's router table table editing environment. 114 | #[allow(clippy::type_complexity)] // it's api design, not use this type directly 115 | pub fn at>>( 116 | self, path: P, 117 | ) -> RouterSetter< 118 | RouterSetter, SetTableItem, Ex>, 119 | SetEndpoint, 120 | Ex, 121 | > { 122 | self.router.at(path) 123 | } 124 | 125 | /// Finish setting uses `middleware`. 126 | pub fn is + 'static>(self, middleware: M) -> R { 127 | self.router.setter.set_to_target(self.router.router, middleware) 128 | } 129 | } 130 | 131 | impl RouterSetter 132 | where 133 | R: RouterLike, 134 | Sw: SetWhich, 135 | Ex: Send + Sync + 'static, 136 | { 137 | /// Finish editing use a method router which accept `method` and uses `middleware`. 138 | pub fn method + 'static>(self, method: Method, middleware: M) -> R { 139 | self.setter.set_to_target(self.router, self.method_router.method(method, middleware)) 140 | } 141 | 142 | impl_all_http_method! { R } 143 | 144 | /// Finish editing use a middleware. 145 | pub fn uses + 'static>(self, middleware: M) -> R { 146 | self.setter.set_to_target(self.router, middleware) 147 | } 148 | } 149 | --------------------------------------------------------------------------------