├── static └── index.html ├── .cargo └── config.toml ├── .gitignore ├── rustfmt.toml ├── examples ├── panic.rs ├── hello_world.rs ├── static.rs ├── timeout.rs ├── custom_handler.rs ├── state.rs ├── catch_all.rs ├── guard.rs ├── cookies.rs ├── params.rs ├── middleware.rs ├── websocket.rs ├── extractors.rs └── queue_todo.rs ├── .github ├── dependabot.yml └── workflows │ ├── cargo-deny-pr.yml │ └── ci.yml ├── src ├── response.rs ├── extract │ ├── body.rs │ ├── method.rs │ ├── params.rs │ ├── vec.rs │ ├── header_map.rs │ ├── route.rs │ ├── string.rs │ ├── request.rs │ ├── splat.rs │ ├── state.rs │ ├── json.rs │ ├── query.rs │ ├── host.rs │ └── rejection.rs ├── defaults.rs ├── template.rs ├── error.rs ├── guard.rs ├── extract.rs ├── request.rs ├── typed_header.rs ├── json.rs ├── lib.rs ├── app.rs ├── state.rs ├── cookies.rs ├── reader.rs ├── macros.rs ├── handler.rs ├── response │ └── into_response_parts.rs ├── session.rs ├── params.rs └── core.rs ├── submillisecond_macros ├── Cargo.toml └── src │ ├── router │ ├── item_catch_all.rs │ ├── item_with_middleware.rs │ ├── method.rs │ ├── item_route.rs │ └── trie.rs │ ├── router.rs │ ├── static_router.rs │ ├── lib.rs │ └── named_param.rs ├── LICENSE ├── Cargo.toml ├── tests ├── websocket.rs ├── parsing_http_1_1.rs └── router.rs ├── README.md ├── benches └── router.rs └── deny.toml /static/index.html: -------------------------------------------------------------------------------- 1 |

Hi

2 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasi" 3 | 4 | [target.wasm32-wasi] 5 | runner = "lunatic" 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | persistence 8 | 9 | /.vscode 10 | Cargo.lock 11 | .DS_Store -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Stable 2 | newline_style = "Unix" 3 | use_field_init_shorthand = true 4 | 5 | # Unstable 6 | unstable_features = true 7 | version = "One" 8 | group_imports = "StdExternalCrate" 9 | imports_granularity = "Module" 10 | wrap_comments = true 11 | -------------------------------------------------------------------------------- /examples/panic.rs: -------------------------------------------------------------------------------- 1 | use submillisecond::{router, Application}; 2 | 3 | fn index() { 4 | panic!("Error"); 5 | } 6 | 7 | fn main() -> std::io::Result<()> { 8 | Application::new(router! { 9 | GET "/" => index 10 | }) 11 | .serve("0.0.0.0:3000") 12 | } 13 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | use submillisecond::{router, Application}; 2 | 3 | fn index() -> &'static str { 4 | "Hello :)" 5 | } 6 | 7 | fn main() -> std::io::Result<()> { 8 | Application::new(router! { 9 | GET "/" => index 10 | }) 11 | .serve("0.0.0.0:3000") 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "02:00" # UTC 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | -------------------------------------------------------------------------------- /src/response.rs: -------------------------------------------------------------------------------- 1 | //! Types and traits for http responses. 2 | 3 | pub use into_response::*; 4 | pub use into_response_parts::*; 5 | 6 | mod into_response; 7 | mod into_response_parts; 8 | 9 | /// Type alias for [`http::Response`] whose body defaults to [`Vec`]. 10 | pub type Response> = http::Response; 11 | -------------------------------------------------------------------------------- /examples/static.rs: -------------------------------------------------------------------------------- 1 | use http::StatusCode; 2 | use submillisecond::{static_router, Application}; 3 | 4 | fn handle_404() -> (StatusCode, &'static str) { 5 | (StatusCode::NOT_FOUND, "Resource not found") 6 | } 7 | 8 | fn main() -> std::io::Result<()> { 9 | Application::new(|| static_router!("./static", handle_404)).serve("0.0.0.0:3000") 10 | } 11 | -------------------------------------------------------------------------------- /src/extract/body.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use super::FromOwnedRequest; 4 | use crate::{Body, RequestContext}; 5 | 6 | impl FromOwnedRequest for Body<'static> { 7 | type Rejection = Infallible; 8 | 9 | fn from_owned_request(req: RequestContext) -> Result { 10 | Ok(*req.body()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/extract/method.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use http::Method; 4 | 5 | use super::FromRequest; 6 | use crate::RequestContext; 7 | 8 | impl FromRequest for Method { 9 | type Rejection = Infallible; 10 | 11 | fn from_request(req: &mut RequestContext) -> Result { 12 | Ok(req.method().clone()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/defaults.rs: -------------------------------------------------------------------------------- 1 | //! Default responses for errors. 2 | 3 | use crate::Response; 4 | 5 | /// Return an error 404 not found response. 6 | pub fn err_404() -> Response { 7 | Response::builder() 8 | .status(404) 9 | .header("Content-Type", "text/html; charset=UTF-8") 10 | .body(b"

404: Not found

".to_vec()) 11 | .unwrap() 12 | } 13 | -------------------------------------------------------------------------------- /src/extract/params.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use super::FromOwnedRequest; 4 | use crate::params::Params; 5 | use crate::RequestContext; 6 | 7 | impl FromOwnedRequest for Params { 8 | type Rejection = Infallible; 9 | 10 | fn from_owned_request(req: RequestContext) -> Result { 11 | Ok(req.params) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/extract/vec.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use super::FromOwnedRequest; 4 | use crate::RequestContext; 5 | 6 | impl FromOwnedRequest for Vec { 7 | type Rejection = Infallible; 8 | 9 | fn from_owned_request(req: RequestContext) -> Result { 10 | Ok(Vec::from(req.request.into_body().as_slice())) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/extract/header_map.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use http::HeaderMap; 4 | 5 | use super::FromRequest; 6 | use crate::RequestContext; 7 | 8 | impl FromRequest for HeaderMap { 9 | type Rejection = Infallible; 10 | 11 | fn from_request(req: &mut RequestContext) -> Result { 12 | Ok(req.headers().clone()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/timeout.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use submillisecond::{router, Application}; 4 | 5 | // Each request has a default 5 minute timeout. 6 | // Waiting for 10 minutes should fail. 7 | fn index() { 8 | lunatic::sleep(Duration::from_secs(60 * 10)); 9 | } 10 | 11 | fn main() -> std::io::Result<()> { 12 | Application::new(router! { 13 | GET "/" => index 14 | }) 15 | .serve("0.0.0.0:3000") 16 | } 17 | -------------------------------------------------------------------------------- /src/extract/route.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use super::FromRequest; 4 | use crate::RequestContext; 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq, PartialOrd)] 7 | pub struct Route(pub String); 8 | 9 | impl FromRequest for Route { 10 | type Rejection = Infallible; 11 | 12 | fn from_request(req: &mut RequestContext) -> Result { 13 | Ok(Route(req.uri().path().to_string())) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/extract/string.rs: -------------------------------------------------------------------------------- 1 | use super::rejection::{InvalidUtf8, StringRejection}; 2 | use super::FromOwnedRequest; 3 | use crate::RequestContext; 4 | 5 | impl FromOwnedRequest for String { 6 | type Rejection = StringRejection; 7 | 8 | fn from_owned_request(req: RequestContext) -> Result { 9 | let body = 10 | std::str::from_utf8(req.request.body().as_slice()).map_err(InvalidUtf8::from_err)?; 11 | Ok(String::from(body)) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/custom_handler.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use submillisecond::response::IntoResponse; 3 | use submillisecond::{Application, Handler}; 4 | 5 | #[derive(Clone, Serialize, Deserialize)] 6 | struct Name(String); 7 | 8 | impl Handler for Name { 9 | fn handle(&self, _req: submillisecond::RequestContext) -> submillisecond::response::Response { 10 | format!("Hello {}!", self.0).into_response() 11 | } 12 | } 13 | 14 | fn main() -> std::io::Result<()> { 15 | Application::new(|| Name("World".into())).serve("0.0.0.0:3000") 16 | } 17 | -------------------------------------------------------------------------------- /src/extract/request.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use super::FromOwnedRequest; 4 | use crate::core::Body; 5 | use crate::RequestContext; 6 | 7 | impl FromOwnedRequest for RequestContext { 8 | type Rejection = Infallible; 9 | 10 | fn from_owned_request(req: RequestContext) -> Result { 11 | Ok(req) 12 | } 13 | } 14 | 15 | impl FromOwnedRequest for http::Request> { 16 | type Rejection = Infallible; 17 | 18 | fn from_owned_request(req: RequestContext) -> Result { 19 | Ok(req.request) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/state.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use submillisecond::state::State; 3 | use submillisecond::{router, Application}; 4 | 5 | #[derive(Clone, Copy, Debug, Serialize, Deserialize)] 6 | struct Count(i32); 7 | 8 | fn index(mut count: State) -> String { 9 | // Increment count 10 | count.set(Count(count.0 + 1)); 11 | 12 | // Return current count 13 | format!("Count is {}", count.0) 14 | } 15 | 16 | fn main() -> std::io::Result<()> { 17 | State::init(Count(0)); 18 | 19 | Application::new(router! { 20 | GET "/" => index 21 | }) 22 | .serve("0.0.0.0:3000") 23 | } 24 | -------------------------------------------------------------------------------- /examples/catch_all.rs: -------------------------------------------------------------------------------- 1 | use submillisecond::{router, Application}; 2 | 3 | fn index() -> &'static str { 4 | "Hello :)" 5 | } 6 | 7 | fn bar() -> &'static str { 8 | "Foo Bar" 9 | } 10 | 11 | fn not_found_foo() -> &'static str { 12 | "Foo route not found" 13 | } 14 | 15 | fn not_found_all() -> &'static str { 16 | "Route not found" 17 | } 18 | 19 | fn main() -> std::io::Result<()> { 20 | Application::new(router! { 21 | GET "/" => index 22 | "/foo" => { 23 | GET "/bar" => bar 24 | _ => not_found_foo 25 | } 26 | _ => not_found_all 27 | }) 28 | .serve("0.0.0.0:3000") 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/cargo-deny-pr.yml: -------------------------------------------------------------------------------- 1 | name: cargo deny 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**/Cargo.toml' 7 | pull_request: 8 | paths: 9 | - '**/Cargo.toml' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | cargo-deny: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | checks: 21 | - advisories 22 | - bans licenses sources 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: EmbarkStudios/cargo-deny-action@v1 27 | with: 28 | command: check ${{ matrix.checks }} 29 | 30 | -------------------------------------------------------------------------------- /src/extract/splat.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use super::FromRequest; 4 | 5 | /// Extract the remainder of the url from a wildcard route. 6 | /// 7 | /// # Example 8 | /// 9 | /// ``` 10 | /// fn foo_handler(Splat(splat): Splat) { 11 | /// // GET "/foo-bar" prints "bar" 12 | /// println!("{splat}"); 13 | /// } 14 | /// 15 | /// router! { 16 | /// GET "/foo-*" => foo_handler 17 | /// } 18 | /// ``` 19 | pub struct Splat(pub String); 20 | 21 | impl FromRequest for Splat { 22 | type Rejection = Infallible; 23 | 24 | fn from_request(req: &mut crate::RequestContext) -> Result { 25 | Ok(Splat(req.reader.uri[req.reader.cursor..].to_string())) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /submillisecond_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "submillisecond_macros" 4 | description = "Macros used by the submillisecond web framework." 5 | version = "0.3.0" 6 | license = "Apache-2.0/MIT" 7 | repository = "https://github.com/lunatic-solutions/submillisecond/tree/main/submillisecond-macros" 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | better-bae = "0.1" 14 | lazy_static = "1.4.0" 15 | mime_guess = "2.0" 16 | proc-macro2 = "1.0" 17 | quote = "1.0" 18 | regex = "1.5.6" 19 | rust-format = { version = "0.3", features = ["token_stream"] } 20 | syn = { version = "1.0", features = ["derive", "extra-traits", "full"] } 21 | 22 | [package.metadata.docs.rs] 23 | targets = ["wasm32-wasi"] 24 | -------------------------------------------------------------------------------- /src/template.rs: -------------------------------------------------------------------------------- 1 | use crate::response::{IntoResponse, Response}; 2 | 3 | #[derive(Debug, Clone, Copy, Default)] 4 | pub struct Template(pub T); 5 | 6 | impl IntoResponse for Template 7 | where 8 | T: askama::Template, 9 | { 10 | fn into_response(self) -> Response { 11 | self.0.render().into_response() 12 | } 13 | } 14 | 15 | impl IntoResponse for &dyn askama::DynTemplate { 16 | fn into_response(self) -> Response { 17 | self.dyn_render().into_response() 18 | } 19 | } 20 | 21 | impl IntoResponse for askama::Error { 22 | fn into_response(self) -> Response { 23 | let mut res = ().into_response(); 24 | *res.status_mut() = http::StatusCode::INTERNAL_SERVER_ERROR; 25 | res 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/extract/state.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::rejection::{NotInitialized, StateRejection}; 4 | use super::FromRequest; 5 | use crate::state::State; 6 | use crate::{Error, RequestContext}; 7 | 8 | impl FromRequest for State 9 | where 10 | T: Clone + Serialize + for<'de> Deserialize<'de>, 11 | { 12 | type Rejection = StateRejection; 13 | 14 | fn from_request(_req: &mut RequestContext) -> Result { 15 | State::::load().ok_or_else(|| { 16 | StateRejection::NotInitialized(NotInitialized::from_err(Error::new(format!( 17 | "state should be initialized with State::<{}>::init(state)", 18 | std::any::type_name::() 19 | )))) 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::fmt; 3 | 4 | /// Alias for a type-erased error type. 5 | pub type BoxError = Box; 6 | 7 | /// Errors that can happen when using submillisecond. 8 | #[derive(Debug)] 9 | pub struct Error { 10 | inner: BoxError, 11 | } 12 | 13 | impl Error { 14 | /// Create a new `Error` from a boxable error. 15 | pub fn new(error: impl Into>) -> Self { 16 | Self { 17 | inner: error.into(), 18 | } 19 | } 20 | } 21 | 22 | impl fmt::Display for Error { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | self.inner.fmt(f) 25 | } 26 | } 27 | 28 | impl StdError for Error { 29 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 30 | Some(&*self.inner) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/guard.rs: -------------------------------------------------------------------------------- 1 | use crate::RequestContext; 2 | 3 | /// Types which implement [`Guard`] can be used to protect routes. 4 | /// 5 | /// This can be useful for admin-only routes for example. 6 | /// 7 | /// Guards which return false will cause a 404 error if no other routes are 8 | /// matched. 9 | /// 10 | /// # Example 11 | /// 12 | /// ``` 13 | /// use submillisecond::{router, Application, Guard, RequestContext}; 14 | /// 15 | /// struct AdminGuard; 16 | /// 17 | /// impl Guard for AdminGuard { 18 | /// fn check(&self, req: &RequestContext) -> bool { 19 | /// is_admin(req) 20 | /// } 21 | /// } 22 | /// 23 | /// router! { 24 | /// "/admin" if AdminGuard => { 25 | /// GET "/dashboard" => dashboard 26 | /// } 27 | /// } 28 | /// ``` 29 | pub trait Guard { 30 | /// Checks a given request, returning a bool if the guard is valid. 31 | fn check(&self, req: &RequestContext) -> bool; 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI Testing 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: "Check out repository" 12 | uses: actions/checkout@v3 13 | # Rust builds can take some time, cache them. 14 | - uses: Swatinem/rust-cache@v2 15 | - name: "Install lunatic" 16 | run: cargo install --git https://github.com/lunatic-solutions/lunatic lunatic-runtime 17 | - name: Install rust 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | toolchain: stable 21 | target: wasm32-wasi 22 | override: true 23 | components: rustfmt, clippy 24 | - name: "Run tests" 25 | run: cargo test --workspace 26 | - name: "Run clippy" 27 | run: cargo clippy -- -D warnings 28 | - name: "Check formatting" 29 | run: cargo fmt -- --check 30 | -------------------------------------------------------------------------------- /examples/guard.rs: -------------------------------------------------------------------------------- 1 | use submillisecond::{router, Application, Guard, RequestContext}; 2 | 3 | struct ContentLengthGuard(u64); 4 | 5 | impl Guard for ContentLengthGuard { 6 | fn check(&self, req: &RequestContext) -> bool { 7 | let content_length_header = req 8 | .headers() 9 | .get("content-length") 10 | .and_then(|content_length| content_length.to_str().ok()) 11 | .and_then(|content_length| content_length.parse::().ok()); 12 | match content_length_header { 13 | Some(content_length) if content_length == req.body().len() as u64 => { 14 | self.0 == content_length 15 | } 16 | _ => false, 17 | } 18 | } 19 | } 20 | 21 | fn foo_handler() -> &'static str { 22 | "foo bar" 23 | } 24 | 25 | fn main() -> std::io::Result<()> { 26 | Application::new(router! { 27 | POST "/foo" if ContentLengthGuard(5) || ContentLengthGuard(10) => foo_handler 28 | }) 29 | .serve("0.0.0.0:3000") 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Lunatic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/cookies.rs: -------------------------------------------------------------------------------- 1 | use cookie::Cookie; 2 | use submillisecond::cookies::{cookies_layer, Cookies, Key}; 3 | use submillisecond::session::{init_session, Session}; 4 | use submillisecond::{router, Application}; 5 | 6 | fn index(mut cookies: Cookies) -> String { 7 | let count: i32 = cookies 8 | .get("count") 9 | .and_then(|cookie| cookie.value().parse().ok()) 10 | .unwrap_or(0); 11 | 12 | cookies.add(Cookie::new("count", format!("{}", count + 1))); 13 | 14 | count.to_string() 15 | } 16 | 17 | fn session(mut session: Session) -> String { 18 | if *session < 10 { 19 | *session += 1; 20 | } 21 | session.to_string() 22 | } 23 | 24 | fn session_bool(mut session: Session) -> String { 25 | *session = !*session; 26 | session.to_string() 27 | } 28 | 29 | fn main() -> std::io::Result<()> { 30 | init_session(Key::from(&[2; 64])); 31 | 32 | Application::new(router! { 33 | with cookies_layer; 34 | 35 | GET "/" => index 36 | GET "/session" => session 37 | GET "/session-bool" => session_bool 38 | }) 39 | .serve("0.0.0.0:3000") 40 | } 41 | -------------------------------------------------------------------------------- /submillisecond_macros/src/router/item_catch_all.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use syn::parse::{Parse, ParseStream}; 3 | use syn::Token; 4 | 5 | use super::item_route::ItemHandler; 6 | use crate::hquote; 7 | 8 | /// _ => handler 9 | #[derive(Clone, Debug)] 10 | pub struct ItemCatchAll { 11 | pub underscore_token: Token![_], 12 | pub fat_arrow_token: Token![=>], 13 | pub handler: Box, 14 | } 15 | 16 | impl ItemCatchAll { 17 | pub fn expand_catch_all_handler(handler: Option<&ItemHandler>) -> TokenStream { 18 | match handler { 19 | Some(handler) => match handler { 20 | ItemHandler::Expr(handler) => { 21 | hquote! { 22 | ::submillisecond::Handler::handle(&#handler, req) 23 | } 24 | } 25 | ItemHandler::SubRouter(subrouter) => { 26 | let handler = subrouter.expand(); 27 | 28 | hquote! { 29 | ::submillisecond::Handler::handle(&#handler, req) 30 | } 31 | } 32 | }, 33 | None => { 34 | hquote! { ::submillisecond::defaults::err_404() } 35 | } 36 | } 37 | } 38 | } 39 | 40 | impl Parse for ItemCatchAll { 41 | fn parse(input: ParseStream) -> syn::Result { 42 | Ok(ItemCatchAll { 43 | underscore_token: input.parse()?, 44 | fat_arrow_token: input.parse()?, 45 | handler: input.parse()?, 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /submillisecond_macros/src/router/item_with_middleware.rs: -------------------------------------------------------------------------------- 1 | use syn::parse::{Parse, ParseStream}; 2 | use syn::punctuated::Punctuated; 3 | use syn::{bracketed, custom_keyword, token, Expr, Token}; 4 | 5 | custom_keyword!(with); 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct ItemWithMiddleware { 9 | pub with_token: with, 10 | pub items: Punctuated, 11 | } 12 | 13 | impl Parse for ItemWithMiddleware { 14 | fn parse(input: ParseStream) -> syn::Result { 15 | let with_token = input.parse()?; 16 | let items = if input.peek(token::Bracket) { 17 | let content; 18 | bracketed!(content in input); 19 | Punctuated::parse_separated_nonempty(&content)? 20 | } else { 21 | let mut items = Punctuated::new(); 22 | items.push(input.parse()?); 23 | items 24 | }; 25 | 26 | Ok(ItemWithMiddleware { with_token, items }) 27 | } 28 | } 29 | 30 | #[cfg(test)] 31 | mod tests { 32 | use quote::ToTokens; 33 | use syn::parse_quote; 34 | 35 | use super::ItemWithMiddleware; 36 | 37 | #[test] 38 | fn item_with_items() { 39 | let item_use: ItemWithMiddleware = parse_quote! { 40 | with [global, logger(warn)] 41 | }; 42 | let items = item_use.items; 43 | assert_eq!( 44 | items 45 | .iter() 46 | .map(|list| list.to_token_stream().to_string().replace(' ', "")) 47 | .collect::>(), 48 | ["global", "logger(warn)"] 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/params.rs: -------------------------------------------------------------------------------- 1 | use submillisecond::extract::Path; 2 | use submillisecond::params::Params; 3 | use submillisecond::response::Response; 4 | use submillisecond::{router, Application, Guard, RequestContext}; 5 | 6 | fn logging_middleware(req: RequestContext) -> Response { 7 | let request_id = req 8 | .headers() 9 | .get("x-request-id") 10 | .and_then(|req_id| req_id.to_str().ok()) 11 | .map(|req_id| req_id.to_string()) 12 | .unwrap_or_else(|| "unknown".to_string()); 13 | println!("[ENTER] request {request_id}"); 14 | let res = req.next_handler(); 15 | println!("[EXIT] request {request_id}"); 16 | res 17 | } 18 | 19 | fn foo_handler(params: Params) -> &'static str { 20 | println!("{params:#?}"); 21 | "foo" 22 | } 23 | 24 | fn bar_handler(Path((a, b, c)): Path<(String, String, String)>) -> &'static str { 25 | println!("GOT PATH {a:?} {b:?} {c:?}"); 26 | "bar" 27 | } 28 | 29 | struct FakeGuard; 30 | 31 | impl Guard for FakeGuard { 32 | fn check(&self, _: &submillisecond::RequestContext) -> bool { 33 | true 34 | } 35 | } 36 | 37 | struct BarGuard; 38 | impl Guard for BarGuard { 39 | fn check(&self, _: &submillisecond::RequestContext) -> bool { 40 | true 41 | } 42 | } 43 | 44 | fn main() -> std::io::Result<()> { 45 | Application::new(router! { 46 | "/:a" if FakeGuard => { 47 | "/:b" => { 48 | GET "/:c" if BarGuard with logging_middleware => bar_handler 49 | } 50 | } 51 | GET "/hello/:x/:y/:z" if BarGuard => foo_handler 52 | }) 53 | .serve("0.0.0.0:3000") 54 | } 55 | -------------------------------------------------------------------------------- /src/extract/json.rs: -------------------------------------------------------------------------------- 1 | use serde::de::DeserializeOwned; 2 | 3 | use super::rejection::{JsonDataError, JsonRejection, JsonSyntaxError, MissingJsonContentType}; 4 | use super::FromRequest; 5 | use crate::json::{json_content_type, Json}; 6 | use crate::RequestContext; 7 | 8 | impl FromRequest for Json 9 | where 10 | T: DeserializeOwned, 11 | { 12 | type Rejection = JsonRejection; 13 | 14 | fn from_request(req: &mut RequestContext) -> Result { 15 | if !json_content_type(req) { 16 | return Err(MissingJsonContentType.into()); 17 | } 18 | 19 | let value = match serde_json::from_slice(req.body().as_slice()) { 20 | Ok(value) => value, 21 | Err(err) => { 22 | let rejection = match err.classify() { 23 | serde_json::error::Category::Data => JsonDataError::from_err(err).into(), 24 | serde_json::error::Category::Syntax | serde_json::error::Category::Eof => { 25 | JsonSyntaxError::from_err(err).into() 26 | } 27 | serde_json::error::Category::Io => { 28 | if cfg!(debug_assertions) { 29 | // we don't use `serde_json::from_reader` and instead always buffer 30 | // bodies first, so we shouldn't encounter any IO errors 31 | unreachable!() 32 | } else { 33 | JsonSyntaxError::from_err(err).into() 34 | } 35 | } 36 | }; 37 | return Err(rejection); 38 | } 39 | }; 40 | 41 | Ok(Json(value)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/extract/query.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use serde::de::DeserializeOwned; 4 | 5 | use super::rejection::{FailedToDeserializeQueryString, QueryRejection}; 6 | use super::FromRequest; 7 | use crate::RequestContext; 8 | 9 | /// Extractor that deserializes query strings into some type. 10 | /// 11 | /// `T` is expected to implement [`serde::Deserialize`]. 12 | /// 13 | /// # Example 14 | /// 15 | /// ```rust,no_run 16 | /// use submillisecond::{router, extract::Query}; 17 | /// use serde::Deserialize; 18 | /// 19 | /// #[derive(Deserialize)] 20 | /// struct Pagination { 21 | /// page: usize, 22 | /// per_page: usize, 23 | /// } 24 | /// 25 | /// // This will parse query strings like `?page=2&per_page=30` into `Pagination` 26 | /// // structs. 27 | /// fn list_things(pagination: Query) { 28 | /// let pagination: Pagination = pagination.0; 29 | /// 30 | /// // ... 31 | /// } 32 | /// 33 | /// router! { 34 | /// GET "/list_things" => list_things 35 | /// } 36 | /// ``` 37 | /// 38 | /// If the query string cannot be parsed it will reject the request with a `422 39 | /// Unprocessable Entity` response. 40 | #[derive(Debug, Clone, Copy, Default)] 41 | pub struct Query(pub T); 42 | 43 | impl FromRequest for Query 44 | where 45 | T: DeserializeOwned, 46 | { 47 | type Rejection = QueryRejection; 48 | 49 | fn from_request(req: &mut RequestContext) -> Result { 50 | let query = req.uri().query().unwrap_or_default(); 51 | let value = serde_urlencoded::from_str(query) 52 | .map_err(FailedToDeserializeQueryString::__private_new::)?; 53 | Ok(Query(value)) 54 | } 55 | } 56 | 57 | impl Deref for Query { 58 | type Target = T; 59 | 60 | fn deref(&self) -> &Self::Target { 61 | &self.0 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /examples/middleware.rs: -------------------------------------------------------------------------------- 1 | use submillisecond::params::Params; 2 | use submillisecond::response::Response; 3 | use submillisecond::{router, Application, Guard, Handler, RequestContext}; 4 | 5 | fn global_middleware(req: RequestContext) -> Response { 6 | println!("[GLOBAL] ENTRY"); 7 | let res = req.next_handler(); 8 | println!("[GLOBAL] EXIT"); 9 | res 10 | } 11 | 12 | struct LoggingMiddleware { 13 | level: u8, 14 | } 15 | 16 | impl LoggingMiddleware { 17 | const fn new(level: u8) -> Self { 18 | LoggingMiddleware { level } 19 | } 20 | } 21 | 22 | impl Handler for LoggingMiddleware { 23 | fn handle(&self, req: RequestContext) -> Response { 24 | if self.level == 0 { 25 | return req.next_handler(); 26 | } 27 | 28 | println!("{} {}", req.method(), req.uri().path()); 29 | let res = req.next_handler(); 30 | println!("[EXIT]"); 31 | res 32 | } 33 | } 34 | 35 | fn foo_bar_handler() -> &'static str { 36 | "foo bar" 37 | } 38 | 39 | fn foo_handler(params: Params) -> &'static str { 40 | println!("{params:#?}"); 41 | "foo" 42 | } 43 | 44 | fn bar_handler() -> &'static str { 45 | "bar" 46 | } 47 | 48 | struct BarGuard; 49 | impl Guard for BarGuard { 50 | fn check(&self, _: &RequestContext) -> bool { 51 | true 52 | } 53 | } 54 | 55 | struct FooGuard; 56 | impl Guard for FooGuard { 57 | fn check(&self, _: &RequestContext) -> bool { 58 | true 59 | } 60 | } 61 | 62 | fn main() -> std::io::Result<()> { 63 | const LOGGER: LoggingMiddleware = LoggingMiddleware::new(1); 64 | 65 | Application::new(router! { 66 | with global_middleware; 67 | 68 | "/foo" if FooGuard => { 69 | with LOGGER; 70 | 71 | GET "/bar" if BarGuard => foo_bar_handler 72 | } 73 | GET "/bar" if BarGuard with LOGGER => bar_handler 74 | POST "/foo" with LOGGER => foo_handler 75 | }) 76 | .serve("0.0.0.0:3000") 77 | } 78 | -------------------------------------------------------------------------------- /examples/websocket.rs: -------------------------------------------------------------------------------- 1 | use lunatic::ap::{Config, ProcessRef}; 2 | use lunatic::{abstract_process, AbstractProcess, Mailbox, Process}; 3 | use serde::{Deserialize, Serialize}; 4 | use submillisecond::websocket::{ 5 | Message, SplitSink, SplitStream, WebSocket, WebSocketConnection, WebSocketUpgrade, 6 | }; 7 | use submillisecond::{router, Application}; 8 | 9 | #[derive(Serialize, Deserialize)] 10 | struct WebSocketHandler { 11 | writer: SplitSink, 12 | } 13 | 14 | #[abstract_process] 15 | impl WebSocketHandler { 16 | #[init] 17 | fn init(this: Config, ws_conn: WebSocketConnection) -> Result { 18 | let (writer, reader) = ws_conn.split(); 19 | 20 | fn read_handler( 21 | (mut reader, this): (SplitStream, ProcessRef), 22 | _: Mailbox<()>, 23 | ) { 24 | loop { 25 | match reader.read_message() { 26 | Ok(Message::Text(msg)) => { 27 | print!("{msg}"); 28 | this.send_message("Pong".to_owned()); 29 | } 30 | Ok(Message::Close(_)) => break, 31 | Ok(_) => { /* Ignore other messages */ } 32 | Err(err) => eprintln!("Read Message Error: {err:?}"), 33 | } 34 | } 35 | } 36 | 37 | Process::spawn_link((reader, this.self_ref()), read_handler); 38 | 39 | Ok(WebSocketHandler { writer }) 40 | } 41 | 42 | #[handle_message] 43 | fn send_message(&mut self, message: String) { 44 | self.writer 45 | .write_message(Message::text(message)) 46 | .unwrap_or_default(); 47 | } 48 | } 49 | 50 | fn main() -> std::io::Result<()> { 51 | fn websocket(ws: WebSocket) -> WebSocketUpgrade { 52 | ws.on_upgrade((), |conn, _| { 53 | WebSocketHandler::link().start(conn).unwrap(); 54 | }) 55 | } 56 | 57 | Application::new(router! { 58 | GET "/" => websocket 59 | }) 60 | .serve("0.0.0.0:3000") 61 | } 62 | -------------------------------------------------------------------------------- /src/extract.rs: -------------------------------------------------------------------------------- 1 | //! Types and traits for extracting data from requests. 2 | //! 3 | //! Many of the types and implementations were taken from [Axum](https://crates.io/crates/axum). 4 | 5 | pub use host::Host; 6 | pub use path::Path; 7 | #[cfg(feature = "query")] 8 | pub use query::Query; 9 | pub use splat::Splat; 10 | 11 | pub mod path; 12 | pub mod rejection; 13 | 14 | mod body; 15 | mod header_map; 16 | mod host; 17 | #[cfg(feature = "json")] 18 | mod json; 19 | mod method; 20 | mod params; 21 | #[cfg(feature = "query")] 22 | mod query; 23 | mod request; 24 | mod route; 25 | mod splat; 26 | mod state; 27 | mod string; 28 | mod vec; 29 | 30 | use crate::response::IntoResponse; 31 | use crate::RequestContext; 32 | 33 | /// Types that can be created from a request. Also known as 'extractors'. 34 | pub trait FromRequest: Sized { 35 | /// If the extractor fails it'll use this "rejection" type. A rejection is 36 | /// a kind of error that can be converted into a response. 37 | type Rejection: IntoResponse; 38 | 39 | /// Perform the extraction. 40 | fn from_request(req: &mut RequestContext) -> Result; 41 | } 42 | 43 | /// Types that can be created from an owned instance of the request. This can be 44 | /// used to avoid unnecessary clones. 45 | pub trait FromOwnedRequest: Sized { 46 | /// If the extractor fails it'll use this "rejection" type. A rejection is 47 | /// a kind of error that can be converted into a response. 48 | type Rejection: IntoResponse; 49 | 50 | /// Extract from an owned instance of the request. 51 | /// The first extractor in handlers will use this method, and can help avoid 52 | /// cloning in many cases. 53 | fn from_owned_request(req: RequestContext) -> Result; 54 | } 55 | 56 | impl FromOwnedRequest for T 57 | where 58 | T: FromRequest, 59 | { 60 | type Rejection = ::Rejection; 61 | 62 | fn from_owned_request(mut req: RequestContext) -> Result { 63 | T::from_request(&mut req) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "submillisecond" 3 | version = "0.4.1" 4 | edition = "2021" 5 | description = "A lunatic web framework for Rust." 6 | keywords = ["http", "web", "framework"] 7 | categories = ["network-programming", "web-programming"] 8 | license = "Apache-2.0/MIT" 9 | readme = "Readme.md" 10 | repository = "https://github.com/lunatic-solutions/submillisecond" 11 | 12 | [workspace] 13 | members = ["submillisecond_macros"] 14 | 15 | [features] 16 | default = ["logging"] 17 | cookies = ["dep:cookie", "serde_json"] 18 | json = ["serde_json"] 19 | logging = ["ansi_term", "lunatic-log"] 20 | query = ["serde_urlencoded"] 21 | template = ["askama"] 22 | websocket = ["base64ct", "sha1", "tungstenite"] 23 | 24 | [dependencies] 25 | headers = "0.3" 26 | http = "0.2.7" 27 | httparse = "1.7.1" 28 | lunatic = "0.13" 29 | mime = "0.3.16" 30 | paste = "1.0" 31 | percent-encoding = "2.1" 32 | serde = { version = "1.0.132", features = ["derive"] } 33 | serde_bytes = "0.11" 34 | submillisecond_macros = { version = "0.3", path = "submillisecond_macros" } 35 | 36 | # optional dependencies 37 | ansi_term = { version = "0.12", optional = true } 38 | askama = { version = "0.11", optional = true } 39 | base64ct = { version = "1.5", features = ["alloc"], optional = true } 40 | cookie = { version = "0.17", features = [ 41 | "percent-encode", 42 | "signed", 43 | "private", 44 | ], optional = true } 45 | lunatic-log = { version = "0.4", optional = true } 46 | serde_json = { version = "1.0", optional = true } 47 | serde_urlencoded = { version = "0.7", optional = true } 48 | sha1 = { version = "0.10", optional = true } 49 | tungstenite = { version = "0.19", optional = true } 50 | 51 | 52 | [dev-dependencies] 53 | base64 = "0.21.0" 54 | criterion = { git = "https://github.com/bheisler/criterion.rs", branch = "version-0.4", default-features = false } 55 | submillisecond = { path = ".", features = [ 56 | "cookies", 57 | "json", 58 | "logging", 59 | "query", 60 | "websocket", 61 | ] } # for examples 62 | ron = "0.8" 63 | uuid = { version = "1.0.0", features = ["v4", "serde"] } 64 | 65 | [package.metadata.docs.rs] 66 | all-features = true 67 | rustdoc-args = ["--cfg", "docsrs"] 68 | targets = ["wasm32-wasi"] 69 | 70 | [[bench]] 71 | harness = false 72 | name = "router" 73 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | use std::{convert, ops}; 2 | 3 | use lunatic::net::TcpStream; 4 | 5 | use crate::core::Body; 6 | use crate::params::Params; 7 | use crate::reader::UriReader; 8 | use crate::Response; 9 | 10 | /// Wrapper for [`http::Request`] containing params and cursor. 11 | pub struct RequestContext { 12 | /// The [`http::Request`] instance. 13 | pub request: http::Request>, 14 | /// Params collected from the router. 15 | pub params: Params, 16 | /// The uri reader. 17 | pub reader: UriReader, 18 | /// The next handler. 19 | /// 20 | /// This is useful for middleware. See [`RequestContext::next_handler`]. 21 | pub(crate) next: Option Response>, 22 | /// The TCP stream. 23 | #[cfg_attr(not(feature = "websocket"), allow(dead_code))] 24 | pub(crate) stream: TcpStream, 25 | } 26 | 27 | impl RequestContext { 28 | /// Creates a new instance of request context. 29 | pub fn new(request: http::Request>, stream: TcpStream) -> Self { 30 | let path = request.uri().path().to_string(); 31 | RequestContext { 32 | request, 33 | params: Params::default(), 34 | reader: UriReader::new(path), 35 | next: None, 36 | stream, 37 | } 38 | } 39 | 40 | /// Call the next handler, returning the response. 41 | /// 42 | /// # Panics 43 | /// 44 | /// This function might panic if no next handler exists. 45 | pub fn next_handler(mut self) -> Response { 46 | if let Some(next) = self.next.take() { 47 | next(self) 48 | } else { 49 | panic!("no next handler") 50 | } 51 | } 52 | 53 | /// Set the next handler. 54 | /// 55 | /// This is used internally by the [`router!`](crate::router) macro. 56 | pub fn set_next_handler(&mut self, next: fn(RequestContext) -> Response) { 57 | self.next = Some(next); 58 | } 59 | } 60 | 61 | impl<'a> convert::AsRef>> for RequestContext { 62 | fn as_ref(&self) -> &http::Request> { 63 | &self.request 64 | } 65 | } 66 | 67 | impl ops::Deref for RequestContext { 68 | type Target = http::Request>; 69 | 70 | fn deref(&self) -> &Self::Target { 71 | &self.request 72 | } 73 | } 74 | 75 | impl ops::DerefMut for RequestContext { 76 | fn deref_mut(&mut self) -> &mut Self::Target { 77 | &mut self.request 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /submillisecond_macros/src/router.rs: -------------------------------------------------------------------------------- 1 | pub use item_catch_all::*; 2 | pub use item_route::*; 3 | pub use item_with_middleware::*; 4 | pub use method::*; 5 | pub use router_trie::*; 6 | pub use trie::*; 7 | 8 | mod item_catch_all; 9 | mod item_route; 10 | mod item_with_middleware; 11 | mod method; 12 | mod router_trie; 13 | mod trie; 14 | 15 | use proc_macro2::TokenStream; 16 | use syn::parse::{Parse, ParseStream}; 17 | use syn::{LitStr, Token}; 18 | 19 | use crate::hquote; 20 | 21 | #[derive(Clone, Debug)] 22 | pub struct Router { 23 | middleware: Option, 24 | routes: Vec, 25 | catch_all: Option, 26 | inits: Vec, 27 | } 28 | 29 | impl Router { 30 | pub fn expand(&self) -> TokenStream { 31 | let trie = RouterTrie::new(self); 32 | let inner = trie.expand(); 33 | 34 | let inits = self.inits.iter().map(|handler| { 35 | hquote! { 36 | ::submillisecond::Handler::init(&#handler) 37 | } 38 | }); 39 | 40 | hquote! {(|| { 41 | #( #inits; )* 42 | 43 | (|mut req: ::submillisecond::RequestContext| -> ::submillisecond::response::Response { 44 | #inner 45 | }) as fn(_) -> _ 46 | }) as ::submillisecond::Router} 47 | } 48 | 49 | fn handlers(&mut self) -> Vec { 50 | self.routes 51 | .iter_mut() 52 | .flat_map(|route| match &mut route.handler { 53 | ItemHandler::Expr(expr) => vec![*expr.clone()], 54 | ItemHandler::SubRouter(router) => { 55 | router.inits = vec![]; 56 | router.handlers() 57 | } 58 | }) 59 | .collect() 60 | } 61 | } 62 | 63 | impl Parse for Router { 64 | fn parse(input: ParseStream) -> syn::Result { 65 | let middleware = if input.peek(with) { 66 | let middleware = input.parse()?; 67 | let _: Token![;] = input.parse()?; 68 | Some(middleware) 69 | } else { 70 | None 71 | }; 72 | 73 | let mut routes: Vec = Vec::new(); 74 | while Method::peek(input) 75 | || input.peek(LitStr) 76 | || (!routes.is_empty() && input.peek(Token![,])) 77 | { 78 | routes.push(input.parse()?); 79 | } 80 | 81 | let catch_all = input.peek(Token![_]).then(|| input.parse()).transpose()?; 82 | 83 | let mut router = Router { 84 | middleware, 85 | routes, 86 | catch_all, 87 | inits: vec![], 88 | }; 89 | 90 | router.inits = router.handlers(); 91 | 92 | Ok(router) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/typed_header.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::ops::Deref; 3 | 4 | use headers::HeaderMapExt; 5 | 6 | use crate::extract::rejection::{TypedHeaderRejection, TypedHeaderRejectionReason}; 7 | use crate::extract::FromRequest; 8 | use crate::response::{IntoResponse, IntoResponseParts, ResponseParts}; 9 | use crate::{RequestContext, Response}; 10 | 11 | /// Extractor and response that works with typed header values from [`headers`]. 12 | /// 13 | /// # As extractor 14 | /// 15 | /// In general, it's recommended to extract only the needed headers via 16 | /// `TypedHeader` rather than removing all headers with the `HeaderMap` 17 | /// extractor. 18 | /// 19 | /// ```rust,no_run 20 | /// use submillisecond::{router, TypedHeader, headers::UserAgent}; 21 | /// 22 | /// fn users_teams_show( 23 | /// TypedHeader(user_agent): TypedHeader, 24 | /// ) { 25 | /// // ... 26 | /// } 27 | /// 28 | /// router! { 29 | /// GET "/users/:user_id/team/:team_id" => users_teams_show 30 | /// } 31 | /// ``` 32 | /// 33 | /// # As response 34 | /// 35 | /// ```rust 36 | /// use submillisecond::{TypedHeader, headers::ContentType}; 37 | /// 38 | /// fn handler() -> (TypedHeader, &'static str) { 39 | /// ( 40 | /// TypedHeader(ContentType::text_utf8()), 41 | /// "Hello, World!", 42 | /// ) 43 | /// } 44 | /// ``` 45 | #[derive(Debug, Clone, Copy)] 46 | pub struct TypedHeader(pub T); 47 | 48 | impl FromRequest for TypedHeader 49 | where 50 | T: headers::Header, 51 | { 52 | type Rejection = TypedHeaderRejection; 53 | 54 | fn from_request(req: &mut RequestContext) -> Result { 55 | match req.headers().typed_try_get::() { 56 | Ok(Some(value)) => Ok(Self(value)), 57 | Ok(None) => Err(TypedHeaderRejection { 58 | name: T::name(), 59 | reason: TypedHeaderRejectionReason::Missing, 60 | }), 61 | Err(err) => Err(TypedHeaderRejection { 62 | name: T::name(), 63 | reason: TypedHeaderRejectionReason::Error(err), 64 | }), 65 | } 66 | } 67 | } 68 | 69 | impl Deref for TypedHeader { 70 | type Target = T; 71 | 72 | fn deref(&self) -> &Self::Target { 73 | &self.0 74 | } 75 | } 76 | 77 | impl IntoResponseParts for TypedHeader 78 | where 79 | T: headers::Header, 80 | { 81 | type Error = Infallible; 82 | 83 | fn into_response_parts(self, mut res: ResponseParts) -> Result { 84 | res.headers_mut().typed_insert(self.0); 85 | Ok(res) 86 | } 87 | } 88 | 89 | impl IntoResponse for TypedHeader 90 | where 91 | T: headers::Header, 92 | { 93 | fn into_response(self) -> Response { 94 | let mut res = ().into_response(); 95 | res.headers_mut().typed_insert(self.0); 96 | res 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /submillisecond_macros/src/router/method.rs: -------------------------------------------------------------------------------- 1 | use quote::ToTokens; 2 | use syn::parse::{Parse, ParseStream}; 3 | 4 | use crate::hquote; 5 | 6 | syn::custom_keyword!(GET); 7 | syn::custom_keyword!(POST); 8 | syn::custom_keyword!(PUT); 9 | syn::custom_keyword!(DELETE); 10 | syn::custom_keyword!(HEAD); 11 | syn::custom_keyword!(OPTIONS); 12 | syn::custom_keyword!(PATCH); 13 | 14 | #[derive(Clone, Copy, Debug)] 15 | pub enum Method { 16 | Get(GET), 17 | Post(POST), 18 | Put(PUT), 19 | Delete(DELETE), 20 | Head(HEAD), 21 | Options(OPTIONS), 22 | Patch(PATCH), 23 | } 24 | 25 | impl std::fmt::Display for Method { 26 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 27 | match self { 28 | Method::Get(_) => write!(f, "GET"), 29 | Method::Post(_) => write!(f, "POST"), 30 | Method::Put(_) => write!(f, "PUT"), 31 | Method::Delete(_) => write!(f, "DELETE"), 32 | Method::Head(_) => write!(f, "HEAD"), 33 | Method::Options(_) => write!(f, "OPTIONS"), 34 | Method::Patch(_) => write!(f, "PATCH"), 35 | } 36 | } 37 | } 38 | 39 | impl Method { 40 | pub fn peek(input: ParseStream) -> bool { 41 | input.peek(GET) 42 | || input.peek(POST) 43 | || input.peek(PUT) 44 | || input.peek(DELETE) 45 | || input.peek(HEAD) 46 | || input.peek(OPTIONS) 47 | || input.peek(PATCH) 48 | } 49 | } 50 | 51 | impl Parse for Method { 52 | fn parse(input: ParseStream) -> syn::Result { 53 | if input.peek(GET) { 54 | return Ok(Method::Get(input.parse()?)); 55 | } 56 | if input.peek(POST) { 57 | return Ok(Method::Post(input.parse()?)); 58 | } 59 | if input.peek(PUT) { 60 | return Ok(Method::Put(input.parse()?)); 61 | } 62 | if input.peek(DELETE) { 63 | return Ok(Method::Delete(input.parse()?)); 64 | } 65 | if input.peek(HEAD) { 66 | return Ok(Method::Head(input.parse()?)); 67 | } 68 | if input.peek(OPTIONS) { 69 | return Ok(Method::Options(input.parse()?)); 70 | } 71 | if input.peek(PATCH) { 72 | return Ok(Method::Patch(input.parse()?)); 73 | } 74 | 75 | Err(input.error( 76 | "expected http method `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTIONS`, or `PATCH`", 77 | )) 78 | } 79 | } 80 | 81 | impl ToTokens for Method { 82 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 83 | match self { 84 | Method::Get(get) => tokens.extend(hquote! { #get }), 85 | Method::Post(post) => tokens.extend(hquote! { #post }), 86 | Method::Put(put) => tokens.extend(hquote! { #put }), 87 | Method::Delete(delete) => tokens.extend(hquote! { #delete }), 88 | Method::Head(head) => tokens.extend(hquote! { #head }), 89 | Method::Options(options) => tokens.extend(hquote! { #options }), 90 | Method::Patch(patch) => tokens.extend(hquote! { #patch }), 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/json.rs: -------------------------------------------------------------------------------- 1 | use http::{header, HeaderValue, StatusCode}; 2 | use serde::Serialize; 3 | 4 | use crate::response::{IntoResponse, Response}; 5 | use crate::RequestContext; 6 | 7 | /// Json can be used as an extractor, or response type. 8 | /// 9 | /// When used as an extractor, the request body will be deserialized into inner 10 | /// type `T` with [`serde::Deserialize`]. 11 | /// 12 | /// For returning `Json`, the inner type `T` will be serialized into the 13 | /// response body with [`serde::Serialize`], and the `Content-Type` header will 14 | /// be set to `application/json`. 15 | /// 16 | /// # Extractor example 17 | /// 18 | /// ``` 19 | /// use serde::Deserialize; 20 | /// 21 | /// #[derive(Deserialize)] 22 | /// struct LoginPayload { 23 | /// email: String, 24 | /// password: String, 25 | /// } 26 | /// 27 | /// fn login(Json(login): Json) -> String { 28 | /// format!("Email: {}\nPassword: {}", login.email, login.password) 29 | /// } 30 | /// ``` 31 | /// 32 | /// # Response example 33 | /// 34 | /// ``` 35 | /// use serde::Serialize; 36 | /// use submillisecond::extract::Path; 37 | /// 38 | /// #[derive(Serialize)] 39 | /// struct User { 40 | /// email: String, 41 | /// password: String, 42 | /// } 43 | /// 44 | /// fn get_user(Path(id): Path) -> Json { 45 | /// let user = find_user(id); 46 | /// Json(user) 47 | /// } 48 | /// ``` 49 | #[derive(Debug, Clone, Copy, Default)] 50 | pub struct Json(pub T); 51 | 52 | impl IntoResponse for Json 53 | where 54 | T: Serialize, 55 | { 56 | fn into_response(self) -> Response { 57 | match serde_json::to_vec(&self.0) { 58 | Ok(bytes) => ( 59 | [( 60 | header::CONTENT_TYPE, 61 | HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()), 62 | )], 63 | bytes, 64 | ) 65 | .into_response(), 66 | Err(err) => ( 67 | StatusCode::INTERNAL_SERVER_ERROR, 68 | [( 69 | header::CONTENT_TYPE, 70 | HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()), 71 | )], 72 | err.to_string(), 73 | ) 74 | .into_response(), 75 | } 76 | } 77 | } 78 | 79 | pub(crate) fn json_content_type(req: &RequestContext) -> bool { 80 | let content_type = if let Some(content_type) = req.headers().get(header::CONTENT_TYPE) { 81 | content_type 82 | } else { 83 | return false; 84 | }; 85 | 86 | let content_type = if let Ok(content_type) = content_type.to_str() { 87 | content_type 88 | } else { 89 | return false; 90 | }; 91 | 92 | let mime = if let Ok(mime) = content_type.parse::() { 93 | mime 94 | } else { 95 | return false; 96 | }; 97 | 98 | let is_json_content_type = mime.type_() == "application" 99 | && (mime.subtype() == "json" || mime.suffix().map_or(false, |name| name == "json")); 100 | 101 | is_json_content_type 102 | } 103 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Submillisecond is a [lunatic](https://lunatic.solutions/) web framework. 2 | //! 3 | //! # Usage 4 | //! 5 | //! First, add `submillisecond` as a dependency in `Cargo.toml`: 6 | //! 7 | //! ```toml 8 | //! [dependencies] 9 | //! submillisecond = "0.1" 10 | //! ``` 11 | //! 12 | //! Then, add a `.cargo/config.toml` to configure the target and runner: 13 | //! 14 | //! ```toml 15 | //! [build] 16 | //! target = "wasm32-wasi" 17 | //! 18 | //! [target.wasm32-wasi] 19 | //! runner = "lunatic" 20 | //! ``` 21 | //! 22 | //! Finally, define a [handler](crate::Handler) and 23 | //! [router](crate::router), and run your application: 24 | //! 25 | //! ``` 26 | //! use submillisecond::{router, Application}; 27 | //! 28 | //! fn index() -> &'static str { 29 | //! "Hello from Submillisecond!" 30 | //! } 31 | //! 32 | //! fn main() -> std::io::Result<()> { 33 | //! Application::new(router! { 34 | //! GET "/" => index 35 | //! }) 36 | //! .serve("0.0.0.0:3000") 37 | //! } 38 | //! ``` 39 | //! 40 | //! The submillisecond repository has some more [examples](https://github.com/lunatic-solutions/submillisecond/tree/main/examples) to help you get started. 41 | //! 42 | //! # High-level features 43 | //! 44 | //! Submillisecond has some notable features including: 45 | //! 46 | //! - [Router macro](crate::router) for performant router generated at 47 | //! compile-time. 48 | //! - [Handlers](crate::Handler): functions taking any number of extractors and 49 | //! returning any type that implements 50 | //! [IntoResponse](crate::response::IntoResponse). 51 | //! - [Extractors](crate::extract::FromRequest): types that parse the request to 52 | //! provide useful data. 53 | //! - Middleware: any handler which calls 54 | //! [`req.next_handler()`](crate::RequestContext::next_handler). 55 | //! - [Guards](crate::Guard): types that protect routes per request. 56 | 57 | #![warn(missing_docs)] 58 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 59 | 60 | pub use submillisecond_macros::*; 61 | pub use {headers, http}; 62 | 63 | pub use crate::app::Application; 64 | pub use crate::core::Body; 65 | pub use crate::error::*; 66 | pub use crate::guard::*; 67 | pub use crate::handler::*; 68 | #[cfg(feature = "json")] 69 | pub use crate::json::*; 70 | pub use crate::request::*; 71 | use crate::response::Response; 72 | pub use crate::typed_header::*; 73 | 74 | #[macro_use] 75 | pub(crate) mod macros; 76 | 77 | #[cfg(feature = "cookies")] 78 | pub mod cookies; 79 | pub mod defaults; 80 | pub mod extract; 81 | pub mod params; 82 | pub mod reader; 83 | pub mod response; 84 | #[cfg(feature = "cookies")] 85 | pub mod session; 86 | pub mod state; 87 | #[cfg(feature = "template")] 88 | pub mod template; 89 | #[cfg(feature = "websocket")] 90 | pub mod websocket; 91 | 92 | mod app; 93 | mod core; 94 | mod error; 95 | mod guard; 96 | mod handler; 97 | #[cfg(feature = "json")] 98 | mod json; 99 | mod request; 100 | mod supervisor; 101 | mod typed_header; 102 | 103 | /// Signature of router function generated by the [`router!`] macro. 104 | pub type Router = fn() -> fn(RequestContext) -> Response; 105 | -------------------------------------------------------------------------------- /examples/extractors.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use http::HeaderMap; 4 | use serde::Deserialize; 5 | use submillisecond::extract::{Host, Path, Query, Splat}; 6 | use submillisecond::params::Params; 7 | use submillisecond::{router, Application, Json, NamedParam, TypedHeader}; 8 | 9 | fn index() -> &'static str { 10 | "Hello :)" 11 | } 12 | 13 | fn path(Path(id): Path) -> String { 14 | format!("Welcome, {id}") 15 | } 16 | 17 | fn splat(Splat(splat): Splat) -> String { 18 | splat 19 | } 20 | 21 | fn query(Query(query): Query>) -> String { 22 | query 23 | .into_iter() 24 | .map(|(key, value)| format!("{key}: {value}")) 25 | .collect::>() 26 | .join(", ") 27 | } 28 | 29 | fn header_map(headers: HeaderMap) -> String { 30 | headers 31 | .into_iter() 32 | .map(|(key, value)| { 33 | format!( 34 | "{}: {}", 35 | key.map(|key| key.to_string()) 36 | .unwrap_or_else(|| "unknown".to_string()), 37 | value.to_str().unwrap() 38 | ) 39 | }) 40 | .collect::>() 41 | .join("\n") 42 | } 43 | 44 | fn host(Host(host): Host) -> String { 45 | host 46 | } 47 | 48 | fn typed_header(TypedHeader(host): TypedHeader) -> String { 49 | host.to_string() 50 | } 51 | 52 | fn params(params: Params) -> String { 53 | let name = params.get("name").unwrap_or("user"); 54 | let age = params.get("age").unwrap_or("age"); 55 | format!("Welcome, {name}. You are {age} years old.") 56 | } 57 | 58 | #[derive(NamedParam)] 59 | #[param(name = "age")] 60 | struct AgeParam(i32); 61 | 62 | fn named_param(AgeParam(age): AgeParam) -> String { 63 | format!("You are {age} years old") 64 | } 65 | 66 | #[derive(NamedParam)] 67 | struct NamedParamStruct { 68 | name: String, 69 | age: i32, 70 | } 71 | 72 | fn named_param2(NamedParamStruct { name, age }: NamedParamStruct) -> String { 73 | format!("Hi {name}, you are {age} years old") 74 | } 75 | 76 | fn string(body: String) -> String { 77 | body 78 | } 79 | 80 | fn vec(body: Vec) -> Vec { 81 | body 82 | } 83 | 84 | #[derive(Deserialize, Debug)] 85 | struct Login { 86 | email: String, 87 | password: String, 88 | } 89 | 90 | fn json(Json(login): Json) -> String { 91 | format!("Email: {}\nPassword: {}", login.email, login.password) 92 | } 93 | 94 | fn main() -> std::io::Result<()> { 95 | Application::new(router! { 96 | GET "/" => index 97 | GET "/queries" => query 98 | GET "/header_map" => header_map 99 | GET "/host" => host 100 | GET "/typed_header" => typed_header 101 | GET "/params/:name/:age" => params 102 | GET "/named_param/:age" => named_param 103 | GET "/named_param2/:name/:age" => named_param2 104 | GET "/path/:id" => path 105 | GET "/splat-*" => splat 106 | POST "/string" => string 107 | POST "/vec" => vec 108 | POST "/json" => json 109 | }) 110 | .serve("0.0.0.0:3000") 111 | } 112 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::marker::PhantomData; 3 | 4 | pub use http; 5 | use lunatic::net::{TcpListener, ToSocketAddrs}; 6 | use lunatic::Process; 7 | 8 | use crate::supervisor::request_supervisor; 9 | use crate::ProcessSafeHandler; 10 | 11 | /// An application containing a router for listening and handling incoming 12 | /// requests. 13 | /// 14 | /// # Example 15 | /// 16 | /// ``` 17 | /// use submillisecond::{router, Application}; 18 | /// 19 | /// fn index() -> &'static str { "Welcome" } 20 | /// 21 | /// Application::new(router! { 22 | /// GET "/" => index 23 | /// }) 24 | /// .serve("0.0.0.0:3000") 25 | /// ``` 26 | #[derive(Clone, Copy)] 27 | pub struct Application { 28 | handler: T, 29 | phantom: PhantomData<(Kind, Arg, Ret)>, 30 | } 31 | 32 | impl Application 33 | where 34 | T: ProcessSafeHandler, 35 | { 36 | /// Creates a new application with a given router. 37 | pub fn new(handler: fn() -> T) -> Self { 38 | Application { 39 | handler: handler(), 40 | phantom: PhantomData, 41 | } 42 | } 43 | 44 | /// Listen on `addr` to receive incoming requests, and handling them with 45 | /// the router. 46 | pub fn serve(self, addr: A) -> io::Result<()> 47 | where 48 | A: ToSocketAddrs + Clone, 49 | { 50 | let safe_handler = self.handler.safe_handler(); 51 | let listener = TcpListener::bind(addr.clone())?; 52 | match listener.local_addr() { 53 | Ok(a) => log_server_start(a), 54 | Err(_) => log_server_start(addr), 55 | } 56 | while let Ok((stream, _)) = listener.accept() { 57 | Process::spawn_link((stream, safe_handler.clone()), request_supervisor); 58 | } 59 | 60 | Ok(()) 61 | } 62 | } 63 | 64 | #[cfg(feature = "logging")] 65 | fn log_server_start(addr: A) { 66 | use lunatic_log::subscriber::fmt::FmtSubscriber; 67 | use lunatic_log::{LevelFilter, __lookup_logging_process, info}; 68 | 69 | // If no logging process is running, start the default logger. 70 | if __lookup_logging_process().is_none() { 71 | lunatic_log::init( 72 | FmtSubscriber::new(LevelFilter::Trace) 73 | .with_color(true) 74 | .with_level(true) 75 | .with_target(true), 76 | ); 77 | } 78 | // Make address bold. 79 | let addrs = addr 80 | .to_socket_addrs() 81 | .unwrap() // is ok since the code is executed after bind 82 | .map(|addr| { 83 | let ip = addr.ip(); 84 | let ip_string = if ip.is_unspecified() { 85 | "localhost".to_string() 86 | } else { 87 | ip.to_string() 88 | }; 89 | ansi_term::Style::new() 90 | .bold() 91 | .paint(format!("http://{}:{}", ip_string, addr.port())) 92 | .to_string() 93 | }) 94 | .collect::>() 95 | .join(", "); 96 | info!("Server started on {addrs}"); 97 | } 98 | 99 | #[cfg(not(feature = "logging"))] 100 | fn log_server_start(_addr: A) {} 101 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | //! Application state stored in a long running process. 2 | //! 3 | //! # Example 4 | //! 5 | //! ``` 6 | //! State::init(0); 7 | //! 8 | //! #[derive(Clone, Copy, Debug, Serialize, Deserialize)] 9 | //! struct Count(i32); 10 | //! 11 | //! fn index(mut count: State) -> String { 12 | //! // Increment count 13 | //! count.set(Count(count.0 + 1)); 14 | //! 15 | //! // Return current count 16 | //! format!("Count is {}", count.0) 17 | //! } 18 | //! ``` 19 | 20 | use std::{fmt, ops}; 21 | 22 | use lunatic::ap::{AbstractProcess, Config, ProcessRef}; 23 | use lunatic::{abstract_process, ProcessName}; 24 | use serde::{Deserialize, Serialize}; 25 | 26 | /// State stored in a process. 27 | /// 28 | /// State should be initialized before use with [`State::init`], and can be 29 | /// updated with [`State::set`]. 30 | /// 31 | /// State implements [`FromRequest`](crate::extract::FromRequest), allowing it 32 | /// to be used as an extractor in handlers. If the state is not initialized, an 33 | /// internal server error will be returned to the response. 34 | #[derive(Clone, Serialize, Deserialize)] 35 | #[serde(bound = "")] 36 | pub struct State 37 | where 38 | T: Clone + Serialize + for<'d> Deserialize<'d> + 'static, 39 | { 40 | process: ProcessRef>, 41 | state: T, 42 | } 43 | 44 | struct StateProcess { 45 | value: T, 46 | } 47 | 48 | impl State 49 | where 50 | T: Clone + Serialize + for<'de> Deserialize<'de> + 'static, 51 | { 52 | /// Initializes state by spawning a process. 53 | pub fn init(state: T) -> Self { 54 | let name = StateProcessName::new::(); 55 | let process = StateProcess::start_as(&name, state.clone()).unwrap(); 56 | State { process, state } 57 | } 58 | 59 | /// Updates the value of state. 60 | pub fn set(&mut self, value: T) { 61 | self.state = value.clone(); 62 | self.process.set(value); 63 | } 64 | 65 | /// Loads the current state. 66 | /// 67 | /// If the state has not initialized, `None` is returned. 68 | pub fn load() -> Option { 69 | let name = StateProcessName::new::(); 70 | let process = ProcessRef::lookup(&name)?; 71 | let state = process.get(); 72 | Some(State { process, state }) 73 | } 74 | 75 | /// Consumes the wrapper, returning the wrapped inner state. 76 | pub fn into_inner(self) -> T { 77 | self.state 78 | } 79 | } 80 | 81 | impl ops::Deref for State 82 | where 83 | T: Clone + Serialize + for<'de> Deserialize<'de>, 84 | { 85 | type Target = T; 86 | 87 | fn deref(&self) -> &Self::Target { 88 | &self.state 89 | } 90 | } 91 | 92 | impl fmt::Debug for State 93 | where 94 | T: fmt::Debug + Clone + Serialize + for<'de> Deserialize<'de>, 95 | { 96 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 97 | ::fmt(&self.state, f) 98 | } 99 | } 100 | 101 | struct StateProcessName { 102 | name: String, 103 | } 104 | 105 | impl StateProcessName { 106 | fn new() -> Self { 107 | let name = format!("submillisecond-state-{}", std::any::type_name::()); 108 | StateProcessName { name } 109 | } 110 | } 111 | 112 | impl ProcessName for StateProcessName { 113 | fn process_name(&self) -> &str { 114 | &self.name 115 | } 116 | } 117 | 118 | #[abstract_process] 119 | impl StateProcess 120 | where 121 | T: Clone + Serialize + for<'de> Deserialize<'de> + 'static, 122 | { 123 | #[init] 124 | fn init(_: Config, value: T) -> Result { 125 | Ok(StateProcess { value }) 126 | } 127 | 128 | #[handle_request] 129 | fn get(&self) -> T { 130 | self.value.clone() 131 | } 132 | 133 | #[handle_message] 134 | fn set(&mut self, value: T) { 135 | self.value = value; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/cookies.rs: -------------------------------------------------------------------------------- 1 | //! Cookies layer and extractor. 2 | 3 | use std::cell::{RefCell, RefMut}; 4 | use std::convert::Infallible; 5 | use std::fmt::Write; 6 | use std::ops; 7 | 8 | use cookie::CookieJar; 9 | pub use cookie::{Cookie, Key}; 10 | use headers::HeaderValue; 11 | use http::header::{COOKIE, SET_COOKIE}; 12 | use lunatic::process_local; 13 | 14 | use crate::extract::FromRequest; 15 | use crate::response::Response; 16 | use crate::RequestContext; 17 | 18 | process_local! { 19 | /// Process local cookie jar. 20 | /// 21 | /// It is advised to use the [`cookies_layer`] and [`Cookies`] extractor to manage this. 22 | pub static COOKIES: RefCell = RefCell::new(CookieJar::new()); 23 | } 24 | 25 | /// Cookies layer which populates the cookie jar from the incoming request, 26 | /// and adds `Set-Cookie` header for modified cookies. 27 | pub fn cookies_layer(req: RequestContext) -> Response { 28 | // Load cookies from header into cookie jar 29 | if let Some(cookie_str) = req 30 | .headers() 31 | .get(COOKIE) 32 | .and_then(|cookie| cookie.to_str().ok()) 33 | { 34 | COOKIES.with_borrow_mut(|mut cookies| { 35 | for cookie in cookie_str.split(';') { 36 | if let Ok(cookie) = Cookie::parse_encoded(cookie.to_string()) { 37 | cookies.add_original(cookie); 38 | } 39 | } 40 | }) 41 | } 42 | 43 | let mut res = req.next_handler(); 44 | 45 | // Push cookies from jar into `Set-Cookie` header, merging if the header is 46 | // already set 47 | COOKIES.with_borrow(|cookies| { 48 | let mut delta = cookies.delta(); 49 | if let Some(first_cookie) = delta.next() { 50 | let mut header = first_cookie.encoded().to_string(); 51 | for cookie in delta { 52 | let _ = write!(header, "{};", cookie.encoded()); 53 | } 54 | 55 | if let Ok(header_value) = HeaderValue::from_str(&header) { 56 | let headers = res.headers_mut(); 57 | match headers.get(SET_COOKIE).and_then(|val| val.to_str().ok()) { 58 | // `Set-Cookie` header exists, merge 59 | Some(val) => { 60 | header.push(';'); 61 | header.push_str(val); 62 | match HeaderValue::from_str(&header) { 63 | Ok(header_value) => { 64 | headers.insert(SET_COOKIE, header_value); 65 | } 66 | Err(_) => { 67 | headers.insert(SET_COOKIE, header_value); 68 | } 69 | } 70 | } 71 | // Insert `Set-Cookie` header 72 | None => { 73 | headers.insert(SET_COOKIE, header_value); 74 | } 75 | } 76 | } 77 | } 78 | }); 79 | 80 | res 81 | } 82 | 83 | /// Cookie jar extractor allowing for reading and modifying cookies for a given 84 | /// request. 85 | /// 86 | /// The [`cookies_layer`] must be used for this to work. 87 | pub struct Cookies { 88 | jar: RefMut<'static, CookieJar>, 89 | } 90 | 91 | impl ops::Deref for Cookies { 92 | type Target = CookieJar; 93 | 94 | fn deref(&self) -> &Self::Target { 95 | &self.jar 96 | } 97 | } 98 | 99 | impl ops::DerefMut for Cookies { 100 | fn deref_mut(&mut self) -> &mut Self::Target { 101 | &mut self.jar 102 | } 103 | } 104 | 105 | impl FromRequest for Cookies { 106 | type Rejection = Infallible; 107 | 108 | fn from_request(_req: &mut RequestContext) -> Result { 109 | let jar = COOKIES.with_borrow_mut(|cookies| cookies); 110 | Ok(Cookies { jar }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/websocket.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use lunatic::ap::{Config, ProcessRef}; 4 | use lunatic::net::TcpStream; 5 | use lunatic::{abstract_process, sleep, AbstractProcess, Mailbox, Process}; 6 | use submillisecond::websocket::{ 7 | Message, SplitSink, SplitStream, WebSocket, WebSocketConnection, WebSocketUpgrade, 8 | }; 9 | use submillisecond::{router, Application}; 10 | use tungstenite::handshake::client::Response; 11 | use tungstenite::{client, ClientHandshake, HandshakeError}; 12 | 13 | #[lunatic::test] 14 | fn websocket_connection_test() { 15 | let port = 9000; 16 | Process::spawn_link(port, setup_server); 17 | // Give enough time to for server to start 18 | sleep(Duration::from_millis(1000)); 19 | 20 | let (mut socket, _response) = connect().expect("Can't connect"); 21 | 22 | socket.write_message(Message::Text("Ping".into())).unwrap(); 23 | 24 | let msg = socket.read_message().expect("Error reading message"); 25 | assert_eq!(msg.into_text().unwrap(), "Pong"); 26 | 27 | socket.close(None).unwrap(); 28 | } 29 | 30 | struct WebSocketHandler { 31 | writer: SplitSink, 32 | } 33 | 34 | #[abstract_process] 35 | impl WebSocketHandler { 36 | #[init] 37 | fn init(this: Config, ws_conn: WebSocketConnection) -> Result { 38 | let (writer, reader) = ws_conn.split(); 39 | 40 | fn read_handler( 41 | (mut reader, this): (SplitStream, ProcessRef), 42 | _: Mailbox<()>, 43 | ) { 44 | loop { 45 | match reader.read_message() { 46 | Ok(Message::Text(_)) => { 47 | this.send_message("Pong".to_owned()); 48 | } 49 | Ok(Message::Close(_)) => break, 50 | Ok(_) => { /* Ignore other messages */ } 51 | Err(err) => eprintln!("Read Message Error: {err:?}"), 52 | } 53 | } 54 | } 55 | 56 | Process::spawn_link((reader, this.self_ref()), read_handler); 57 | 58 | Ok(WebSocketHandler { writer }) 59 | } 60 | 61 | #[handle_message] 62 | fn send_message(&mut self, message: String) { 63 | self.writer 64 | .write_message(Message::text(message)) 65 | .unwrap_or_default(); 66 | } 67 | } 68 | 69 | fn setup_server(port: u16, _: Mailbox<()>) { 70 | fn websocket(ws: WebSocket) -> WebSocketUpgrade { 71 | ws.on_upgrade((), |conn, _| { 72 | WebSocketHandler::link().start(conn).unwrap(); 73 | }) 74 | } 75 | 76 | Application::new(router! { 77 | GET "/" => websocket 78 | }) 79 | .serve(format!("0.0.0.0:{port}")) 80 | .unwrap(); 81 | } 82 | 83 | fn connect() -> Result< 84 | (tungstenite::protocol::WebSocket, Response), 85 | HandshakeError>, 86 | > { 87 | let tcp_stream = TcpStream::connect("127.0.0.1:9000").unwrap(); 88 | 89 | let mut headers = [ 90 | httparse::Header { 91 | name: "Connection", 92 | value: b"Upgrade", 93 | }, 94 | httparse::Header { 95 | name: "Upgrade", 96 | value: b"websocket", 97 | }, 98 | httparse::Header { 99 | name: "Host", 100 | value: b"localhost:9000", 101 | }, 102 | httparse::Header { 103 | name: "Origin", 104 | value: b"http://localhost:9000", 105 | }, 106 | httparse::Header { 107 | name: "Sec-WebSocket-Key", 108 | value: b"SGVsbG8sIHdvcmxkIQ==", 109 | }, 110 | httparse::Header { 111 | name: "Sec-WebSocket-Version", 112 | value: b"13", 113 | }, 114 | ]; 115 | let mut req = httparse::Request::new(&mut headers); 116 | req.method = Some("GET"); 117 | req.version = Some(1); 118 | req.path = Some("ws://localhost:9000/"); 119 | 120 | client(req, tcp_stream) 121 | } 122 | -------------------------------------------------------------------------------- /submillisecond_macros/src/static_router.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::{fs, io}; 3 | 4 | use mime_guess::{mime, Mime}; 5 | use proc_macro2::TokenStream; 6 | use syn::parse::{Parse, ParseStream}; 7 | use syn::{LitStr, Token}; 8 | 9 | use crate::hquote; 10 | use crate::router::{ItemCatchAll, ItemHandler}; 11 | 12 | #[derive(Debug)] 13 | pub struct StaticRouter { 14 | files: Vec, 15 | catch_all: Option, 16 | } 17 | 18 | impl StaticRouter { 19 | pub fn expand(&self) -> TokenStream { 20 | let catch_all_expanded = self.expand_catch_all(); 21 | let match_arms = self.expand_match_arms(); 22 | 23 | hquote! { 24 | (|mut req: ::submillisecond::RequestContext| -> ::submillisecond::response::Response { 25 | if *req.method() != ::submillisecond::http::Method::GET { 26 | return #catch_all_expanded; 27 | } 28 | 29 | match req.reader.read_to_end() { 30 | #match_arms 31 | _ => #catch_all_expanded, 32 | } 33 | }) as fn(_) -> _ 34 | } 35 | } 36 | 37 | fn expand_catch_all(&self) -> TokenStream { 38 | ItemCatchAll::expand_catch_all_handler(self.catch_all.as_ref()) 39 | } 40 | 41 | fn expand_match_arms(&self) -> TokenStream { 42 | let arms = self.files.iter().map(|StaticFile { mime, path, content }| { 43 | let path = format!("/{path}"); 44 | let mime = mime.to_string(); 45 | let bytes = hquote! { &[#( #content ),*] }; 46 | 47 | hquote! { 48 | #path => { 49 | let mut headers = ::submillisecond::http::header::HeaderMap::new(); 50 | headers.insert(::submillisecond::http::header::CONTENT_TYPE, #mime.parse().unwrap()); 51 | ::submillisecond::response::IntoResponse::into_response((headers, #bytes as &'static [u8])) 52 | } 53 | } 54 | }); 55 | 56 | hquote! { #( #arms, )* } 57 | } 58 | } 59 | 60 | impl Parse for StaticRouter { 61 | fn parse(input: ParseStream) -> syn::Result { 62 | let dir: LitStr = input.parse()?; 63 | let catch_all = if input.peek(Token![,]) { 64 | let _: Token![,] = input.parse()?; 65 | Some(input.parse()?) 66 | } else { 67 | None 68 | }; 69 | let files = walk_dir(dir.value()).map_err(|err| syn::Error::new(dir.span(), err))?; 70 | 71 | Ok(StaticRouter { files, catch_all }) 72 | } 73 | } 74 | 75 | #[derive(Debug)] 76 | struct StaticFile { 77 | mime: Mime, 78 | path: String, 79 | content: Vec, 80 | } 81 | 82 | fn walk_dir

(base_path: P) -> io::Result> 83 | where 84 | P: AsRef, 85 | { 86 | fn walk_nested(base_path: &Path, path: &Path) -> io::Result> { 87 | let dir = fs::read_dir(path)?; 88 | let mut static_files = Vec::new(); 89 | for entry in dir { 90 | let entry = entry?; 91 | let file_type = entry.file_type()?; 92 | if file_type.is_dir() { 93 | static_files.extend(walk_nested(base_path, &entry.path())?.into_iter()); 94 | } else { 95 | let entry_path = entry.path(); 96 | let entry_path = entry_path 97 | .strip_prefix(base_path) 98 | .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; 99 | let mime = mime_guess::from_path(entry_path) 100 | .first() 101 | .unwrap_or(mime::TEXT_PLAIN); 102 | 103 | let content = fs::read(entry.path())?; 104 | 105 | static_files.push(StaticFile { 106 | mime, 107 | path: entry_path 108 | .to_str() 109 | .ok_or_else(|| { 110 | io::Error::new( 111 | io::ErrorKind::Other, 112 | "unable to convert path to UTF-8 string", 113 | ) 114 | })? 115 | .to_string(), 116 | content, 117 | }); 118 | } 119 | } 120 | 121 | Ok(static_files) 122 | } 123 | 124 | walk_nested(base_path.as_ref(), base_path.as_ref()) 125 | } 126 | -------------------------------------------------------------------------------- /submillisecond_macros/src/router/item_route.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{ToTokens, TokenStreamExt}; 3 | use syn::parse::{Parse, ParseStream}; 4 | use syn::spanned::Spanned; 5 | use syn::{braced, token, Expr, LitStr, Path, Token}; 6 | 7 | use super::item_with_middleware::ItemWithMiddleware; 8 | use super::method::Method; 9 | use super::with; 10 | use crate::hquote; 11 | use crate::router::Router; 12 | 13 | /// `"/abc" => sub_router` 14 | /// `GET "/abc" => handler` 15 | /// `GET "/abc" if guard => handler` 16 | /// `GET "/abc" use middleware => handler` 17 | /// `GET "/abc" if guard use middleware => handler` 18 | #[derive(Clone, Debug)] 19 | pub struct ItemRoute { 20 | pub method: Option, 21 | pub path: LitStr, 22 | pub guard: Option, 23 | pub middleware: Option, 24 | pub fat_arrow_token: Token![=>], 25 | pub handler: ItemHandler, 26 | } 27 | 28 | impl Parse for ItemRoute { 29 | fn parse(input: ParseStream) -> syn::Result { 30 | let item_route = ItemRoute { 31 | method: if Method::peek(input) { 32 | Some(input.parse()?) 33 | } else { 34 | None 35 | }, 36 | path: input.parse()?, 37 | guard: if input.peek(Token![if]) { 38 | Some(input.parse()?) 39 | } else { 40 | None 41 | }, 42 | middleware: if input.peek(with) { 43 | Some(input.parse()?) 44 | } else { 45 | None 46 | }, 47 | fat_arrow_token: input.parse()?, 48 | handler: input.parse()?, 49 | }; 50 | 51 | if let Some(method) = item_route.method { 52 | if matches!(item_route.handler, ItemHandler::SubRouter(_)) { 53 | return Err(syn::Error::new( 54 | method.span(), 55 | "method prefix cannot be used with sub routers", 56 | )); 57 | } 58 | } 59 | 60 | let path = item_route.path.value(); 61 | if let Some(pos) = path.find('*') { 62 | if pos < path.len() - 1 { 63 | return Err(syn::Error::new( 64 | item_route.path.span(), 65 | "wildcards must be placed at the end of the path", 66 | )); 67 | } 68 | 69 | if item_route.method.is_none() { 70 | return Err(syn::Error::new( 71 | item_route.path.span(), 72 | "wildcards cannot be used with sub routers - try adding a HTTP method", 73 | )); 74 | } 75 | } 76 | 77 | Ok(item_route) 78 | } 79 | } 80 | 81 | #[derive(Clone, Debug)] 82 | pub struct ItemGuard { 83 | pub if_token: Token![if], 84 | pub guard: Box, 85 | } 86 | 87 | impl Parse for ItemGuard { 88 | fn parse(input: ParseStream) -> syn::Result { 89 | Ok(ItemGuard { 90 | if_token: input.parse()?, 91 | guard: Box::new(input.call(Expr::parse_without_eager_brace)?), 92 | }) 93 | } 94 | } 95 | 96 | impl ToTokens for ItemGuard { 97 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 98 | tokens.append_all(expand_guard_struct(&self.guard)); 99 | } 100 | } 101 | 102 | fn expand_guard_struct(guard: &syn::Expr) -> TokenStream { 103 | match guard { 104 | Expr::Binary(expr_binary) => { 105 | let left = expand_guard_struct(&expr_binary.left); 106 | let op = &expr_binary.op; 107 | let right = expand_guard_struct(&expr_binary.right); 108 | 109 | hquote! { #left #op #right } 110 | } 111 | Expr::Paren(expr_paren) => { 112 | let expr = expand_guard_struct(&expr_paren.expr); 113 | hquote! { (#expr) } 114 | } 115 | expr => hquote! { ::submillisecond::Guard::check(&#expr, &req) }, 116 | } 117 | } 118 | 119 | #[derive(Clone, Debug)] 120 | pub enum ItemHandler { 121 | Expr(Box), 122 | SubRouter(Router), 123 | } 124 | 125 | impl Parse for ItemHandler { 126 | fn parse(input: ParseStream) -> syn::Result { 127 | if input.peek(token::Brace) { 128 | let content; 129 | braced!(content in input); 130 | return Ok(ItemHandler::SubRouter(content.parse()?)); 131 | } 132 | 133 | let fork = input.fork(); 134 | let _: Path = fork.parse()?; 135 | Ok(ItemHandler::Expr(input.parse()?)) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/reader.rs: -------------------------------------------------------------------------------- 1 | //! Uri reader using a cursor. 2 | //! 3 | //! The [`router!`](crate::router) macro uses this internally for routing. 4 | 5 | /// A uri string and cursor reader. 6 | #[derive(Clone, Debug, Default)] 7 | pub struct UriReader { 8 | /// Request uri. 9 | pub uri: String, 10 | /// Request uri cursor. 11 | pub cursor: usize, 12 | } 13 | 14 | impl UriReader { 15 | /// Creates a new [`UriReader`] with the cursor set to `0`. 16 | pub fn new(uri: String) -> UriReader { 17 | UriReader { uri, cursor: 0 } 18 | } 19 | 20 | /// Returns the next `len` characters from the uri, without modifying the 21 | /// cursor position. 22 | pub fn peek(&self, len: usize) -> &str { 23 | let read_attempt = self.cursor + len; 24 | if self.uri.len() >= read_attempt { 25 | return &self.uri[self.cursor..read_attempt]; 26 | } 27 | "" 28 | } 29 | 30 | /// Returns a bool indicating whether the reader has reached the end, 31 | /// disregarding any trailing slash. 32 | pub fn is_dangling_slash(&self) -> bool { 33 | self.uri.len() == self.cursor || &self.uri[self.cursor..self.cursor + 1] == "/" 34 | } 35 | 36 | /// Returns a bool indicating whether the reader has reached the end, 37 | /// disregarding any trailing slash. 38 | pub fn is_dangling_terminal_slash(&self) -> bool { 39 | self.uri.len() == self.cursor 40 | || (&self.uri[self.cursor..self.cursor + 1] == "/" && self.uri.len() - self.cursor == 1) 41 | } 42 | 43 | /// Move the cursor forward based on `len`. 44 | pub fn read(&mut self, len: usize) { 45 | self.cursor += len; 46 | } 47 | 48 | /// Attempt to read `s` from the current cursor position, modifying the 49 | /// cursor if a match was found. 50 | /// 51 | /// `true` is returned if `s` matched the uri and the cursor was updated. 52 | pub fn read_matching(&mut self, s: &str) -> bool { 53 | let read_to = self.cursor + s.len(); 54 | if read_to > self.uri.len() { 55 | return false; 56 | } 57 | 58 | if &self.uri[self.cursor..read_to] == s { 59 | self.cursor = read_to; 60 | return true; 61 | } 62 | 63 | false 64 | } 65 | 66 | /// Reads a single `/` character, modifying the cursor and returning whether 67 | /// there was a match. 68 | pub fn ensure_next_slash(&mut self) -> bool { 69 | self.read_matching("/") 70 | } 71 | 72 | /// Reset the cursor to the start of the uri. 73 | pub fn reset(&mut self) { 74 | self.cursor = 0; 75 | } 76 | 77 | /// Check if the cursor has reached the end of the uri, optionally allowing 78 | /// for a trailing slash. 79 | pub fn is_empty(&self, allow_trailing_slash: bool) -> bool { 80 | if allow_trailing_slash { 81 | self.uri.len() <= self.cursor || &self.uri[self.cursor..] == "/" 82 | } else { 83 | self.uri.len() <= self.cursor 84 | } 85 | } 86 | 87 | /// Read a param until the next `/` or end of uri. 88 | pub fn read_param(&mut self) -> Option<&str> { 89 | let initial_cursor = self.cursor; 90 | while !self.is_empty(false) { 91 | if self.peek(1) != "/" { 92 | self.read(1); 93 | } else { 94 | break; 95 | } 96 | } 97 | // if nothing was found, return none 98 | if initial_cursor == self.cursor { 99 | return None; 100 | } 101 | // read the param 102 | Some(&self.uri[initial_cursor..self.cursor]) 103 | } 104 | 105 | /// Check if the uri ends with `suffix`. 106 | pub fn ends_with(&self, suffix: &str) -> bool { 107 | if self.cursor >= self.uri.len() { 108 | return false; 109 | } 110 | let end = &self.uri[self.cursor..]; 111 | end == suffix 112 | } 113 | 114 | /// Returns the remainder of the uri from the current cursor position. 115 | pub fn read_to_end(&self) -> &str { 116 | &self.uri[self.cursor..] 117 | } 118 | } 119 | 120 | #[cfg(test)] 121 | mod tests { 122 | use super::UriReader; 123 | 124 | #[test] 125 | fn peek_empty_string() { 126 | let reader = UriReader::new("".to_string()); 127 | assert_eq!(reader.peek(5), ""); 128 | } 129 | 130 | #[test] 131 | fn peek_path() { 132 | let mut reader = UriReader::new("/alive".to_string()); 133 | assert_eq!(reader.peek(3), "/al"); 134 | reader.read(3); 135 | assert_eq!(reader.peek(3), "ive"); 136 | reader.read(3); 137 | assert_eq!(reader.peek(3), ""); 138 | reader.read(3); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! define_rejection { 2 | ( 3 | #[status = $status:ident] 4 | #[body = $body:expr] 5 | $(#[$m:meta])* 6 | pub struct $name:ident; 7 | ) => { 8 | $(#[$m])* 9 | #[derive(Debug)] 10 | #[non_exhaustive] 11 | pub struct $name; 12 | 13 | impl $crate::response::IntoResponse for $name { 14 | fn into_response(self) -> $crate::Response { 15 | (http::StatusCode::$status, $body).into_response() 16 | } 17 | } 18 | 19 | impl std::fmt::Display for $name { 20 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 21 | write!(f, "{}", $body) 22 | } 23 | } 24 | 25 | impl std::error::Error for $name {} 26 | 27 | impl Default for $name { 28 | fn default() -> Self { 29 | Self 30 | } 31 | } 32 | }; 33 | 34 | ( 35 | #[status = $status:ident] 36 | #[body = $body:expr] 37 | $(#[$m:meta])* 38 | pub struct $name:ident (Error); 39 | ) => { 40 | $(#[$m])* 41 | #[derive(Debug)] 42 | pub struct $name(pub(crate) crate::Error); 43 | 44 | impl $name { 45 | pub(crate) fn from_err(err: E) -> Self 46 | where 47 | E: Into, 48 | { 49 | Self(crate::Error::new(err)) 50 | } 51 | } 52 | 53 | impl crate::response::IntoResponse for $name { 54 | fn into_response(self) -> $crate::Response { 55 | ( 56 | http::StatusCode::$status, 57 | format!(concat!($body, ": {}"), self.0), 58 | ).into_response() 59 | } 60 | } 61 | 62 | impl std::fmt::Display for $name { 63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 64 | write!(f, "{}", $body) 65 | } 66 | } 67 | 68 | impl std::error::Error for $name { 69 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 70 | Some(&self.0) 71 | } 72 | } 73 | }; 74 | } 75 | 76 | macro_rules! composite_rejection { 77 | ( 78 | $(#[$m:meta])* 79 | pub enum $name:ident { 80 | $($variant:ident),+ 81 | $(,)? 82 | } 83 | ) => { 84 | $(#[$m])* 85 | #[derive(Debug)] 86 | #[non_exhaustive] 87 | pub enum $name { 88 | $( 89 | #[allow(missing_docs)] 90 | $variant($variant) 91 | ),+ 92 | } 93 | 94 | impl $crate::response::IntoResponse for $name { 95 | fn into_response(self) -> $crate::Response { 96 | match self { 97 | $( 98 | Self::$variant(inner) => inner.into_response(), 99 | )+ 100 | } 101 | } 102 | } 103 | 104 | $( 105 | impl From<$variant> for $name { 106 | fn from(inner: $variant) -> Self { 107 | Self::$variant(inner) 108 | } 109 | } 110 | )+ 111 | 112 | impl std::fmt::Display for $name { 113 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 114 | match self { 115 | $( 116 | Self::$variant(inner) => write!(f, "{}", inner), 117 | )+ 118 | } 119 | } 120 | } 121 | 122 | impl std::error::Error for $name { 123 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 124 | match self { 125 | $( 126 | Self::$variant(inner) => Some(inner), 127 | )+ 128 | } 129 | } 130 | } 131 | }; 132 | } 133 | 134 | macro_rules! all_the_tuples { 135 | ($name:ident) => { 136 | $name!(T1); 137 | $name!(T1, T2); 138 | $name!(T1, T2, T3); 139 | $name!(T1, T2, T3, T4); 140 | $name!(T1, T2, T3, T4, T5); 141 | $name!(T1, T2, T3, T4, T5, T6); 142 | $name!(T1, T2, T3, T4, T5, T6, T7); 143 | $name!(T1, T2, T3, T4, T5, T6, T7, T8); 144 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9); 145 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); 146 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); 147 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); 148 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13); 149 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14); 150 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15); 151 | $name!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16); 152 | }; 153 | ($name:ident, numbered) => { 154 | $name!(1, T1); 155 | $name!(2, T1, T2); 156 | $name!(3, T1, T2, T3); 157 | $name!(4, T1, T2, T3, T4); 158 | $name!(5, T1, T2, T3, T4, T5); 159 | $name!(6, T1, T2, T3, T4, T5, T6); 160 | $name!(7, T1, T2, T3, T4, T5, T6, T7); 161 | $name!(8, T1, T2, T3, T4, T5, T6, T7, T8); 162 | $name!(9, T1, T2, T3, T4, T5, T6, T7, T8, T9); 163 | $name!(10, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); 164 | $name!(11, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); 165 | $name!(12, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); 166 | $name!(13, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13); 167 | $name!(14, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14); 168 | $name!(15, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15); 169 | $name!(16, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16); 170 | }; 171 | } 172 | -------------------------------------------------------------------------------- /src/handler.rs: -------------------------------------------------------------------------------- 1 | use lunatic::function::reference::Fn as FnPtr; 2 | use lunatic::function::FuncRef; 3 | use serde::de::DeserializeOwned; 4 | use serde::Serialize; 5 | 6 | use crate::extract::{FromOwnedRequest, FromRequest}; 7 | use crate::response::IntoResponse; 8 | use crate::{RequestContext, Response}; 9 | 10 | /// Implemented for process-safe [`Handlers`](Handler). 11 | /// 12 | /// Submillisecond handles every request in a separate lunatic process, and 13 | /// lunatic's processes are sandboxed. This means that no memory is shared 14 | /// between the request handler and the rest of the app. This introduces an 15 | /// additional limitation on what can be a [`Handler`]. 16 | /// 17 | /// Two kinds of types are safe to be used as handlers: 18 | /// - Static functions 19 | /// - Serializable and clonable objects 20 | /// 21 | /// ### Static functions 22 | /// 23 | /// This type is obvious. Non-capturing functions are generated during compile 24 | /// time and are shared between all processes, so they can be easily used as 25 | /// handlers. In fact, the [`router!`](crate::router) macro will in the end just 26 | /// generate a function that will be used as a handler and invoke other handlers 27 | /// depending on the request values. 28 | /// 29 | /// ### Serializable and clonable objects 30 | /// 31 | /// Everything else needs to be passed somehow to the memory of the handler 32 | /// process. This means that we need to clone the value for every incoming 33 | /// request, serialize it and send it to the process handling the request. 34 | pub trait ProcessSafeHandler { 35 | /// A handler is only safe if it can be cloned and safely sent between 36 | /// processes. 37 | type SafeHandler: Handler + Clone + Serialize + DeserializeOwned; 38 | 39 | /// Turn type into a safe handler. 40 | fn safe_handler(self) -> Self::SafeHandler; 41 | } 42 | 43 | /// Marker type for functions that satisfy [`ProcessSafeHandler`]. 44 | pub struct Function; 45 | /// Marker type for objects that satisfy [`ProcessSafeHandler`]. 46 | pub struct Object; 47 | 48 | impl ProcessSafeHandler for T 49 | where 50 | T: FnPtr + Copy, 51 | FuncRef: Handler, 52 | { 53 | type SafeHandler = FuncRef; 54 | 55 | fn safe_handler(self) -> Self::SafeHandler { 56 | FuncRef::new(self) 57 | } 58 | } 59 | 60 | impl ProcessSafeHandler for T 61 | where 62 | T: Clone + Handler + Serialize + DeserializeOwned, 63 | { 64 | type SafeHandler = T; 65 | 66 | fn safe_handler(self) -> Self::SafeHandler { 67 | self 68 | } 69 | } 70 | 71 | impl Handler for FuncRef 72 | where 73 | T: FnPtr + Copy + Handler, 74 | { 75 | fn handle(&self, req: RequestContext) -> Response { 76 | self.get().handle(req) 77 | } 78 | } 79 | 80 | /// A handler is implemented for any function which takes any number of 81 | /// [extractors](crate::extract), and returns any type that implements 82 | /// [`IntoResponse`]. 83 | /// 84 | /// To avoid unnecessary clones, the [`RequestContext`], [`http::Request`], 85 | /// [`String`], [`Vec`], [`Params`](crate::params::Params) extractors (and 86 | /// any other types which implement [`FromOwnedRequest`] directly) should be 87 | /// placed as the first argument, and cannot be used together in a single 88 | /// handler. 89 | /// 90 | /// A maximum of 16 extractor arguments may be added for a single handler. 91 | /// 92 | /// # Handler examples 93 | /// 94 | /// ``` 95 | /// fn index() -> &'static str { 96 | /// "Hello, submillisecond" 97 | /// } 98 | /// 99 | /// use submillisecond::extract::Path; 100 | /// use submillisecond::http::status::FOUND; 101 | /// 102 | /// fn headers(Path(id): Path) -> (StatusCode, String) { 103 | /// (FOUND, id) 104 | /// } 105 | /// ``` 106 | /// 107 | /// # Middleware example 108 | /// 109 | /// ``` 110 | /// use submillisecond::RequestContent; 111 | /// use submillisecond::response::Response; 112 | /// 113 | /// fn logging_layer(req: RequestContext) -> Response { 114 | /// println!("Incoming request start"); 115 | /// let res = req.next_handler(); 116 | /// println!("Incoming request end"); 117 | /// res 118 | /// } 119 | /// ``` 120 | pub trait Handler { 121 | /// Handles the request, returning a response. 122 | fn handle(&self, req: RequestContext) -> Response; 123 | 124 | /// Initializes handler, useful for spawning processes on startup. 125 | fn init(&self) {} 126 | } 127 | 128 | impl Handler<(), R> for F 129 | where 130 | F: Fn() -> R, 131 | R: IntoResponse, 132 | { 133 | fn handle(&self, _req: RequestContext) -> Response { 134 | self().into_response() 135 | } 136 | } 137 | 138 | macro_rules! impl_handler { 139 | ( $arg1: ident $(, $( $args: ident ),*)? ) => { 140 | #[allow(unused_parens)] 141 | impl Handler<($arg1$(, $( $args, )*)?), R> for F 142 | where 143 | F: Fn($arg1$(, $( $args, )*)?) -> R, 144 | $arg1: FromOwnedRequest, 145 | $( $( $args: FromRequest, )* )? 146 | R: IntoResponse, 147 | { 148 | 149 | #[allow(unused_mut, unused_variables)] 150 | fn handle(&self, mut req: RequestContext) -> Response { 151 | paste::paste! { 152 | $($( 153 | let [< $args:lower >] = match <$args as FromRequest>::from_request(&mut req) { 154 | Ok(e) => e, 155 | Err(err) => return err.into_response(), 156 | }; 157 | )*)? 158 | let e1 = match <$arg1 as FromOwnedRequest>::from_owned_request(req) { 159 | Ok(e) => e, 160 | Err(err) => return err.into_response(), 161 | }; 162 | self(e1 $(, $( [< $args:lower >] ),*)?).into_response() 163 | } 164 | } 165 | } 166 | }; 167 | } 168 | 169 | all_the_tuples!(impl_handler); 170 | -------------------------------------------------------------------------------- /src/response/into_response_parts.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{Infallible, TryInto}; 2 | use std::fmt; 3 | 4 | use http::header::{HeaderMap, HeaderName, HeaderValue}; 5 | use http::{Extensions, StatusCode}; 6 | 7 | use super::{IntoResponse, Response}; 8 | 9 | /// Trait for adding headers and extensions to a response. 10 | pub trait IntoResponseParts { 11 | /// The type returned in the event of an error. 12 | /// 13 | /// This can be used to fallibly convert types into headers or extensions. 14 | type Error: IntoResponse; 15 | 16 | /// Set parts of the response 17 | fn into_response_parts(self, res: ResponseParts) -> Result; 18 | } 19 | 20 | impl IntoResponseParts for Option 21 | where 22 | T: IntoResponseParts, 23 | { 24 | type Error = T::Error; 25 | 26 | fn into_response_parts(self, res: ResponseParts) -> Result { 27 | if let Some(inner) = self { 28 | inner.into_response_parts(res) 29 | } else { 30 | Ok(res) 31 | } 32 | } 33 | } 34 | 35 | /// Parts of a response. 36 | /// 37 | /// Used with [`IntoResponseParts`]. 38 | #[derive(Debug)] 39 | pub struct ResponseParts { 40 | pub(crate) res: Response, 41 | } 42 | 43 | impl ResponseParts { 44 | /// Gets a reference to the response headers. 45 | pub fn headers(&self) -> &HeaderMap { 46 | self.res.headers() 47 | } 48 | 49 | /// Gets a mutable reference to the response headers. 50 | pub fn headers_mut(&mut self) -> &mut HeaderMap { 51 | self.res.headers_mut() 52 | } 53 | 54 | /// Gets a reference to the response extensions. 55 | pub fn extensions(&self) -> &Extensions { 56 | self.res.extensions() 57 | } 58 | 59 | /// Gets a mutable reference to the response extensions. 60 | pub fn extensions_mut(&mut self) -> &mut Extensions { 61 | self.res.extensions_mut() 62 | } 63 | } 64 | 65 | impl IntoResponseParts for HeaderMap { 66 | type Error = Infallible; 67 | 68 | fn into_response_parts(self, mut res: ResponseParts) -> Result { 69 | res.headers_mut().extend(self); 70 | Ok(res) 71 | } 72 | } 73 | 74 | impl IntoResponseParts for [(K, V); N] 75 | where 76 | K: TryInto, 77 | K::Error: fmt::Display, 78 | V: TryInto, 79 | V::Error: fmt::Display, 80 | { 81 | type Error = TryIntoHeaderError; 82 | 83 | fn into_response_parts(self, mut res: ResponseParts) -> Result { 84 | for (key, value) in self { 85 | let key = key.try_into().map_err(TryIntoHeaderError::key)?; 86 | let value = value.try_into().map_err(TryIntoHeaderError::value)?; 87 | res.headers_mut().insert(key, value); 88 | } 89 | 90 | Ok(res) 91 | } 92 | } 93 | 94 | /// Error returned if converting a value to a header fails. 95 | #[derive(Debug)] 96 | pub struct TryIntoHeaderError { 97 | kind: TryIntoHeaderErrorKind, 98 | } 99 | 100 | impl TryIntoHeaderError { 101 | pub(super) fn key(err: K) -> Self { 102 | Self { 103 | kind: TryIntoHeaderErrorKind::Key(err), 104 | } 105 | } 106 | 107 | pub(super) fn value(err: V) -> Self { 108 | Self { 109 | kind: TryIntoHeaderErrorKind::Value(err), 110 | } 111 | } 112 | } 113 | 114 | #[derive(Debug)] 115 | enum TryIntoHeaderErrorKind { 116 | Key(K), 117 | Value(V), 118 | } 119 | 120 | impl IntoResponse for TryIntoHeaderError 121 | where 122 | K: fmt::Display, 123 | V: fmt::Display, 124 | { 125 | fn into_response(self) -> Response { 126 | match self.kind { 127 | TryIntoHeaderErrorKind::Key(inner) => { 128 | (StatusCode::INTERNAL_SERVER_ERROR, inner.to_string()).into_response() 129 | } 130 | TryIntoHeaderErrorKind::Value(inner) => { 131 | (StatusCode::INTERNAL_SERVER_ERROR, inner.to_string()).into_response() 132 | } 133 | } 134 | } 135 | } 136 | 137 | impl fmt::Display for TryIntoHeaderError { 138 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 139 | match self.kind { 140 | TryIntoHeaderErrorKind::Key(_) => write!(f, "failed to convert key to a header name"), 141 | TryIntoHeaderErrorKind::Value(_) => { 142 | write!(f, "failed to convert value to a header value") 143 | } 144 | } 145 | } 146 | } 147 | 148 | impl std::error::Error for TryIntoHeaderError 149 | where 150 | K: std::error::Error + 'static, 151 | V: std::error::Error + 'static, 152 | { 153 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 154 | match &self.kind { 155 | TryIntoHeaderErrorKind::Key(inner) => Some(inner), 156 | TryIntoHeaderErrorKind::Value(inner) => Some(inner), 157 | } 158 | } 159 | } 160 | 161 | macro_rules! impl_into_response_parts { 162 | ( $($ty:ident),* $(,)? ) => { 163 | #[allow(non_snake_case)] 164 | impl<$($ty,)*> IntoResponseParts for ($($ty,)*) 165 | where 166 | $( $ty: IntoResponseParts, )* 167 | { 168 | type Error = Response; 169 | 170 | fn into_response_parts(self, res: ResponseParts) -> Result { 171 | let ($($ty,)*) = self; 172 | 173 | $( 174 | let res = match $ty.into_response_parts(res) { 175 | Ok(res) => res, 176 | Err(err) => { 177 | return Err(err.into_response()); 178 | } 179 | }; 180 | )* 181 | 182 | Ok(res) 183 | } 184 | } 185 | } 186 | } 187 | 188 | all_the_tuples!(impl_into_response_parts); 189 | 190 | impl IntoResponseParts for Extensions { 191 | type Error = Infallible; 192 | 193 | fn into_response_parts(self, mut res: ResponseParts) -> Result { 194 | res.extensions_mut().extend(self); 195 | Ok(res) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/session.rs: -------------------------------------------------------------------------------- 1 | //! Session data stored in encrypted user cookie. 2 | //! 3 | //! Sessions can be shared across handlers as long as the same type in the 4 | //! generic is used. Using different session types across handlers is valid, 5 | //! and the data will be stored in separate cookies named 6 | //! `session-{type_id_hash}`. 7 | //! 8 | //! The [`init_session`] should be called before starting the web server to 9 | //! initialize a key. 10 | //! 11 | //! The [`cookies::cookies_layer`](super::cookies::cookies_layer) layer must 12 | //! also be used for session data to work. 13 | //! 14 | //! # Example 15 | //! 16 | //! ``` 17 | //! use submillisecond::cookies::{cookies_layer, Key}; 18 | //! use submillisecond::session::{init_session, Session}; 19 | //! use submillisecond::{router, Application}; 20 | //! 21 | //! fn counter(mut session: Session) -> String { 22 | //! if *session < 10 { 23 | //! *session += 1; 24 | //! } 25 | //! session.to_string() 26 | //! } 27 | //! 28 | //! fn main() -> std::io::Result<()> { 29 | //! session::init_session(Key::generate()); 30 | //! 31 | //! Application::new(router! { 32 | //! with cookies_layer; 33 | //! 34 | //! GET "/counter" => counter 35 | //! }) 36 | //! .serve("0.0.0.0:3000") 37 | //! } 38 | //! ``` 39 | 40 | use std::any::TypeId; 41 | use std::collections::hash_map::DefaultHasher; 42 | use std::hash::{Hash, Hasher}; 43 | use std::ops::{Deref, DerefMut}; 44 | 45 | use cookie::{Cookie, Key}; 46 | use lunatic::ap::{AbstractProcess, Config, ProcessRef, RequestHandler, State}; 47 | use lunatic::serializer::Bincode; 48 | use lunatic::ProcessName; 49 | use serde::de::DeserializeOwned; 50 | use serde::{Deserialize, Serialize}; 51 | 52 | use crate::cookies::COOKIES; 53 | use crate::extract::FromRequest; 54 | 55 | #[derive(ProcessName)] 56 | struct SessionProcessID; 57 | 58 | /// Initialize the session key. 59 | pub fn init_session(key: Key) { 60 | SessionProcess::start_as(&SessionProcessID, KeyWrapper(key)).unwrap(); 61 | } 62 | 63 | /// Session extractor, used to store data encrypted in a browser cookie. 64 | /// 65 | /// If the session does not exist from the request, a default session will be 66 | /// used with [`Default::default`]. 67 | pub struct Session 68 | where 69 | D: Default + Serialize + DeserializeOwned + 'static, 70 | { 71 | changed: bool, 72 | data: D, 73 | key: Key, 74 | } 75 | 76 | impl Deref for Session 77 | where 78 | D: Default + Serialize + DeserializeOwned, 79 | { 80 | type Target = D; 81 | 82 | fn deref(&self) -> &Self::Target { 83 | &self.data 84 | } 85 | } 86 | 87 | impl DerefMut for Session 88 | where 89 | D: Default + Serialize + DeserializeOwned, 90 | { 91 | fn deref_mut(&mut self) -> &mut Self::Target { 92 | self.changed = true; 93 | &mut self.data 94 | } 95 | } 96 | 97 | impl Drop for Session 98 | where 99 | D: Default + Serialize + DeserializeOwned + 'static, 100 | { 101 | fn drop(&mut self) { 102 | if self.changed { 103 | if let Ok(value) = serde_json::to_string(&self.data) { 104 | COOKIES.with_borrow_mut(|mut cookies| { 105 | let mut private_jar = cookies.private_mut(&self.key); 106 | let cookie_name = cookie_name::(); 107 | private_jar.add(Cookie::new(cookie_name, value)); 108 | }); 109 | } 110 | } 111 | } 112 | } 113 | 114 | impl FromRequest for Session 115 | where 116 | D: Default + Serialize + DeserializeOwned + 'static, 117 | { 118 | type Rejection = SessionProcessNotRunning; 119 | 120 | fn from_request(_req: &mut crate::RequestContext) -> Result { 121 | let session_process = ProcessRef::::lookup(&SessionProcessID) 122 | .ok_or(SessionProcessNotRunning)?; 123 | let KeyWrapper(key) = session_process.request(GetSessionNameKey); 124 | let cookie_name = cookie_name::(); 125 | let (changed, data) = COOKIES.with_borrow(|cookies| { 126 | let private_jar = cookies.private(&key); 127 | let session_cookie = private_jar.get(&cookie_name); 128 | let changed = session_cookie.is_none(); 129 | let data = session_cookie 130 | .and_then(|session_cookie| serde_json::from_str(session_cookie.value()).ok()) 131 | .unwrap_or_default(); 132 | (changed, data) 133 | }); 134 | Ok(Session { changed, data, key }) 135 | } 136 | } 137 | 138 | define_rejection! { 139 | #[status = INTERNAL_SERVER_ERROR] 140 | #[body = "Session key not configured. Did you forget to call `session::init_session`?"] 141 | /// Rejection type used when the session process has not been started via [`session::init_session`]. 142 | pub struct SessionProcessNotRunning; 143 | } 144 | 145 | struct SessionProcess(KeyWrapper); 146 | impl AbstractProcess for SessionProcess { 147 | type Arg = KeyWrapper; 148 | type State = Self; 149 | type Serializer = Bincode; 150 | type Handlers = (); 151 | type StartupError = (); 152 | 153 | fn init(_: Config, key: KeyWrapper) -> Result { 154 | Ok(Self(key)) 155 | } 156 | } 157 | 158 | #[derive(serde::Serialize, serde::Deserialize)] 159 | struct GetSessionNameKey; 160 | impl RequestHandler for SessionProcess { 161 | type Response = KeyWrapper; 162 | 163 | fn handle(state: State, _: GetSessionNameKey) -> Self::Response { 164 | state.0.clone() 165 | } 166 | } 167 | 168 | #[derive(Clone)] 169 | struct KeyWrapper(Key); 170 | 171 | impl Serialize for KeyWrapper { 172 | fn serialize(&self, serializer: S) -> Result 173 | where 174 | S: serde::Serializer, 175 | { 176 | serializer.serialize_bytes(self.0.master()) 177 | } 178 | } 179 | 180 | impl<'de> Deserialize<'de> for KeyWrapper { 181 | fn deserialize(deserializer: D) -> Result 182 | where 183 | D: serde::Deserializer<'de>, 184 | { 185 | let key = >::deserialize(deserializer)?; 186 | Ok(KeyWrapper(Key::from(&key))) 187 | } 188 | } 189 | 190 | fn cookie_name() -> String { 191 | let type_id = TypeId::of::(); 192 | let mut hasher = DefaultHasher::new(); 193 | type_id.hash(&mut hasher); 194 | let hash = hasher.finish(); 195 | format!("session-{hash:x}") 196 | } 197 | -------------------------------------------------------------------------------- /src/extract/host.rs: -------------------------------------------------------------------------------- 1 | use http::header::{HeaderMap, FORWARDED}; 2 | 3 | use super::rejection::{FailedToResolveHost, HostRejection}; 4 | use super::FromRequest; 5 | 6 | const X_FORWARDED_HOST_HEADER_KEY: &str = "X-Forwarded-Host"; 7 | 8 | /// Extractor that resolves the hostname of the request. 9 | /// 10 | /// Hostname is resolved through the following, in order: 11 | /// - `Forwarded` header 12 | /// - `X-Forwarded-Host` header 13 | /// - `Host` header 14 | /// - request target / URI 15 | /// 16 | /// Note that user agents can set `X-Forwarded-Host` and `Host` headers to 17 | /// arbitrary values so make sure to validate them to avoid security issues. 18 | #[derive(Debug, Clone)] 19 | pub struct Host(pub String); 20 | 21 | impl FromRequest for Host { 22 | type Rejection = HostRejection; 23 | 24 | fn from_request(req: &mut crate::RequestContext) -> Result { 25 | let headers = req.headers(); 26 | 27 | if let Some(host) = parse_forwarded(headers) { 28 | return Ok(Host(host.to_owned())); 29 | } 30 | 31 | if let Some(host) = headers 32 | .get(X_FORWARDED_HOST_HEADER_KEY) 33 | .and_then(|host| host.to_str().ok()) 34 | { 35 | return Ok(Host(host.to_owned())); 36 | } 37 | 38 | if let Some(host) = headers 39 | .get(http::header::HOST) 40 | .and_then(|host| host.to_str().ok()) 41 | { 42 | return Ok(Host(host.to_owned())); 43 | } 44 | 45 | if let Some(host) = req.uri().host() { 46 | return Ok(Host(host.to_owned())); 47 | } 48 | 49 | Err(HostRejection::FailedToResolveHost(FailedToResolveHost)) 50 | } 51 | } 52 | 53 | #[allow(warnings)] 54 | fn parse_forwarded(headers: &HeaderMap) -> Option<&str> { 55 | // if there are multiple `Forwarded` `HeaderMap::get` will return the first one 56 | let forwarded_values = headers.get(FORWARDED)?.to_str().ok()?; 57 | 58 | // get the first set of values 59 | let first_value = forwarded_values.split(',').nth(0)?; 60 | 61 | // find the value of the `host` field 62 | first_value.split(';').find_map(|pair| { 63 | let (key, value) = pair.split_once('=')?; 64 | key.trim() 65 | .eq_ignore_ascii_case("host") 66 | .then(|| value.trim().trim_matches('"')) 67 | }) 68 | } 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use http::header::HeaderName; 73 | use lunatic::net::TcpStream; 74 | 75 | use super::*; 76 | use crate::{Body, RequestContext}; 77 | 78 | #[lunatic::test] 79 | fn host_header() { 80 | let original_host = "some-domain:123"; 81 | 82 | let mut req = RequestContext::new( 83 | http::Request::builder() 84 | .method("GET") 85 | .header(http::header::HOST, original_host) 86 | .body(Body::from_slice(&[])) 87 | .unwrap(), 88 | TcpStream::connect("127.0.0.1:22").unwrap(), 89 | ); 90 | 91 | let Host(host) = Host::from_request(&mut req).unwrap(); 92 | 93 | assert_eq!(host, original_host); 94 | } 95 | 96 | #[lunatic::test] 97 | fn x_forwarded_host_header() { 98 | let original_host = "some-domain:456"; 99 | 100 | let mut req = RequestContext::new( 101 | http::Request::builder() 102 | .method("GET") 103 | .header(X_FORWARDED_HOST_HEADER_KEY, original_host) 104 | .body(Body::from_slice(&[])) 105 | .unwrap(), 106 | TcpStream::connect("127.0.0.1:22").unwrap(), 107 | ); 108 | 109 | let Host(host) = Host::from_request(&mut req).unwrap(); 110 | 111 | assert_eq!(host, original_host); 112 | } 113 | 114 | #[lunatic::test] 115 | fn x_forwarded_host_precedence_over_host_header() { 116 | let x_forwarded_host_header = "some-domain:456"; 117 | let host_header = "some-domain:123"; 118 | 119 | let mut req = RequestContext::new( 120 | http::Request::builder() 121 | .method("GET") 122 | .header(X_FORWARDED_HOST_HEADER_KEY, x_forwarded_host_header) 123 | .header(http::header::HOST, host_header) 124 | .body(Body::from_slice(&[])) 125 | .unwrap(), 126 | TcpStream::connect("127.0.0.1:22").unwrap(), 127 | ); 128 | 129 | let Host(host) = Host::from_request(&mut req).unwrap(); 130 | 131 | assert_eq!(host, x_forwarded_host_header); 132 | } 133 | 134 | #[lunatic::test] 135 | fn uri_host() { 136 | let mut req = RequestContext::new( 137 | http::Request::builder() 138 | .method("GET") 139 | .uri("127.0.0.1") 140 | .body(Body::from_slice(&[])) 141 | .unwrap(), 142 | TcpStream::connect("127.0.0.1:22").unwrap(), 143 | ); 144 | 145 | let Host(host) = Host::from_request(&mut req).unwrap(); 146 | 147 | assert!(host.contains("127.0.0.1")); 148 | } 149 | 150 | #[lunatic::test] 151 | fn forwarded_parsing() { 152 | // the basic case 153 | let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]); 154 | let value = parse_forwarded(&headers).unwrap(); 155 | assert_eq!(value, "192.0.2.60"); 156 | 157 | // is case insensitive 158 | let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]); 159 | let value = parse_forwarded(&headers).unwrap(); 160 | assert_eq!(value, "192.0.2.60"); 161 | 162 | // ipv6 163 | let headers = header_map(&[(FORWARDED, "host=\"[2001:db8:cafe::17]:4711\"")]); 164 | let value = parse_forwarded(&headers).unwrap(); 165 | assert_eq!(value, "[2001:db8:cafe::17]:4711"); 166 | 167 | // multiple values in one header 168 | let headers = header_map(&[(FORWARDED, "host=192.0.2.60, host=127.0.0.1")]); 169 | let value = parse_forwarded(&headers).unwrap(); 170 | assert_eq!(value, "192.0.2.60"); 171 | 172 | // multiple header values 173 | let headers = header_map(&[ 174 | (FORWARDED, "host=192.0.2.60"), 175 | (FORWARDED, "host=127.0.0.1"), 176 | ]); 177 | let value = parse_forwarded(&headers).unwrap(); 178 | assert_eq!(value, "192.0.2.60"); 179 | } 180 | 181 | fn header_map(values: &[(HeaderName, &str)]) -> HeaderMap { 182 | let mut headers = HeaderMap::new(); 183 | for (key, value) in values { 184 | headers.append(key, value.parse().unwrap()); 185 | } 186 | headers 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/params.rs: -------------------------------------------------------------------------------- 1 | //! Params are data from the request url. 2 | //! 3 | //! The [`router!`](crate::router) macro collects params into the 4 | //! [`crate::RequestContext`] 5 | 6 | use std::{iter, mem, slice}; 7 | 8 | /// A single URL parameter, consisting of a key and a value. 9 | #[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Default, Clone)] 10 | pub struct Param { 11 | /// Param key as defined in the url. 12 | pub key: &'static str, 13 | /// Param value. 14 | pub value: String, 15 | } 16 | 17 | /// A list of parameters returned by a route match. 18 | /// 19 | /// # Extractor example 20 | /// 21 | /// ``` 22 | /// use submillisecond::router; 23 | /// use submillisecond::params::Params; 24 | /// 25 | /// fn params(params: Params) -> String { 26 | /// let name = params.get("name").unwrap_or("user"); 27 | /// format!("Welcome, {name}") 28 | /// } 29 | /// 30 | /// router! { 31 | /// GET "/:name" => params 32 | /// } 33 | /// ``` 34 | #[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Clone)] 35 | pub struct Params { 36 | kind: ParamsKind, 37 | } 38 | 39 | // Most routes have 1-3 dynamic parameters, so we can avoid a heap allocation in 40 | // common cases. 41 | const SMALL: usize = 3; 42 | 43 | #[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Clone)] 44 | enum ParamsKind { 45 | None, 46 | Small([Param; SMALL], usize), 47 | Large(Vec), 48 | } 49 | 50 | impl Default for Params { 51 | fn default() -> Self { 52 | Self::new() 53 | } 54 | } 55 | 56 | impl Params { 57 | /// Creates an empty instance of [`Params`]. 58 | pub fn new() -> Self { 59 | let kind = ParamsKind::None; 60 | Self { kind } 61 | } 62 | 63 | /// Returns the number of parameters. 64 | pub fn len(&self) -> usize { 65 | match &self.kind { 66 | ParamsKind::None => 0, 67 | ParamsKind::Small(_, len) => *len, 68 | ParamsKind::Large(vec) => vec.len(), 69 | } 70 | } 71 | 72 | /// Returns the value of the first parameter registered under the given key. 73 | pub fn get(&self, key: &str) -> Option<&str> { 74 | match &self.kind { 75 | ParamsKind::None => None, 76 | ParamsKind::Small(arr, len) => arr 77 | .iter() 78 | .take(*len) 79 | .find(|param| param.key == key) 80 | .map(|value| value.value.as_str()), 81 | ParamsKind::Large(vec) => vec 82 | .iter() 83 | .find(|param| param.key == key) 84 | .map(|value| value.value.as_str()), 85 | } 86 | } 87 | 88 | /// Returns an iterator over the parameters in the list. 89 | pub fn iter(&self) -> ParamsIter<'_> { 90 | ParamsIter::new(self) 91 | } 92 | 93 | /// Returns `true` if there are no parameters in the list. 94 | pub fn is_empty(&self) -> bool { 95 | match &self.kind { 96 | ParamsKind::None => true, 97 | ParamsKind::Small(_, len) => *len == 0, 98 | ParamsKind::Large(vec) => vec.is_empty(), 99 | } 100 | } 101 | 102 | /// Inserts a key value parameter pair into the list. 103 | pub fn push(&mut self, key: &'static str, value: String) { 104 | #[cold] 105 | fn drain_to_vec(len: usize, elem: T, arr: &mut [T; SMALL]) -> Vec { 106 | let mut vec = Vec::with_capacity(len + 1); 107 | vec.extend(arr.iter_mut().map(mem::take)); 108 | vec.push(elem); 109 | vec 110 | } 111 | 112 | let param = Param { key, value }; 113 | match &mut self.kind { 114 | ParamsKind::None => { 115 | self.kind = ParamsKind::Small([param, Param::default(), Param::default()], 1); 116 | } 117 | ParamsKind::Small(arr, len) => { 118 | if *len == SMALL { 119 | self.kind = ParamsKind::Large(drain_to_vec(*len, param, arr)); 120 | return; 121 | } 122 | arr[*len] = param; 123 | *len += 1; 124 | } 125 | ParamsKind::Large(vec) => vec.push(param), 126 | } 127 | } 128 | } 129 | 130 | /// An iterator over the keys and values of a route's [parameters](Params). 131 | pub struct ParamsIter<'ps> { 132 | kind: ParamsIterKind<'ps>, 133 | } 134 | 135 | impl<'ps> ParamsIter<'ps> { 136 | fn new(params: &'ps Params) -> Self { 137 | let kind = match ¶ms.kind { 138 | ParamsKind::None => ParamsIterKind::None, 139 | ParamsKind::Small(arr, len) => ParamsIterKind::Small(arr.iter().take(*len)), 140 | ParamsKind::Large(vec) => ParamsIterKind::Large(vec.iter()), 141 | }; 142 | Self { kind } 143 | } 144 | } 145 | 146 | enum ParamsIterKind<'ps> { 147 | None, 148 | Small(iter::Take>), 149 | Large(slice::Iter<'ps, Param>), 150 | } 151 | 152 | impl<'ps> Iterator for ParamsIter<'ps> { 153 | type Item = (&'ps str, &'ps str); 154 | 155 | fn next(&mut self) -> Option { 156 | match self.kind { 157 | ParamsIterKind::None => None, 158 | ParamsIterKind::Small(ref mut iter) => iter.next().map(|p| (p.key, p.value.as_str())), 159 | ParamsIterKind::Large(ref mut iter) => iter.next().map(|p| (p.key, p.value.as_str())), 160 | } 161 | } 162 | } 163 | 164 | #[cfg(test)] 165 | mod tests { 166 | use super::*; 167 | 168 | #[test] 169 | fn no_alloc() { 170 | assert_eq!(Params::new().kind, ParamsKind::None); 171 | } 172 | 173 | #[test] 174 | fn heap_alloc() { 175 | let vec = vec![ 176 | ("hello", "hello"), 177 | ("world", "world"), 178 | ("foo", "foo"), 179 | ("bar", "bar"), 180 | ("baz", "baz"), 181 | ]; 182 | 183 | let mut params = Params::new(); 184 | for (key, value) in vec.clone() { 185 | params.push(key, value.to_string()); 186 | assert_eq!(params.get(key), Some(value)); 187 | } 188 | 189 | match params.kind { 190 | ParamsKind::Large(..) => {} 191 | _ => panic!(), 192 | } 193 | 194 | assert!(params.iter().eq(vec.clone())); 195 | } 196 | 197 | #[test] 198 | fn stack_alloc() { 199 | let vec = vec![("hello", "hello"), ("world", "world"), ("baz", "baz")]; 200 | 201 | let mut params = Params::new(); 202 | for (key, value) in vec.clone() { 203 | params.push(key, value.to_string()); 204 | assert_eq!(params.get(key), Some(value)); 205 | } 206 | 207 | match params.kind { 208 | ParamsKind::Small(..) => {} 209 | _ => panic!(), 210 | } 211 | 212 | assert!(params.iter().eq(vec.clone())); 213 | } 214 | 215 | #[test] 216 | fn ignore_array_default() { 217 | let params = Params::new(); 218 | assert!(params.get("").is_none()); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /tests/parsing_http_1_1.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::str::from_utf8; 3 | use std::time::Duration; 4 | 5 | use lunatic::net::TcpStream; 6 | use lunatic::{sleep, test, Mailbox, Process}; 7 | use submillisecond::{router, Application, Body}; 8 | 9 | fn hell_world_server(port: u16, _: Mailbox<()>) { 10 | fn hello_world_handler() -> &'static str { 11 | "Hello world!" 12 | } 13 | 14 | Application::new(router! { 15 | GET "/" => hello_world_handler 16 | }) 17 | .serve(format!("localhost:{port}")) 18 | .unwrap(); 19 | } 20 | 21 | #[test] 22 | fn empty_line_prefix_is_valid() { 23 | Process::spawn_link(8900, hell_world_server); 24 | // Give enough time to for server to start 25 | sleep(Duration::from_millis(10)); 26 | let mut stream = TcpStream::connect("localhost:8900").unwrap(); 27 | let request = "\r\n\nGET / HTTP/1.1\r\n\r\n".as_bytes(); 28 | stream.write_all(request).unwrap(); 29 | let mut response = [0u8; 256]; 30 | let n = stream.read(&mut response).unwrap(); 31 | let response_str = from_utf8(&response[..n]).unwrap(); 32 | assert_eq!( 33 | response_str, 34 | "HTTP/1.1 200 OK\r\n\ 35 | content-type: text/plain; charset=utf-8\r\n\ 36 | content-length: 12\r\n\ 37 | \r\n\ 38 | Hello world!" 39 | ); 40 | } 41 | 42 | #[test] 43 | #[ignore] 44 | fn pipeline_requests() { 45 | Process::spawn_link(8901, hell_world_server); 46 | // Give enough time to for server to start 47 | sleep(Duration::from_millis(10)); 48 | let mut stream = TcpStream::connect("localhost:8901").unwrap(); 49 | let request = "GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\n\r\n".as_bytes(); 50 | stream.write_all(request).unwrap(); 51 | let mut response = [0u8; 256]; 52 | // First response 53 | let n = stream.read(&mut response[..100]).unwrap(); 54 | let response_str = from_utf8(&response[..n]).unwrap(); 55 | assert_eq!( 56 | response_str, 57 | "HTTP/1.1 200 OK\r\n\ 58 | content-type: text/plain; charset=utf-8\r\n\ 59 | content-length: 12\r\n\ 60 | \r\n\ 61 | Hello world!" 62 | ); 63 | // Second response 64 | let n = stream.read(&mut response).unwrap(); 65 | let response_str = from_utf8(&response[..n]).unwrap(); 66 | assert_eq!( 67 | response_str, 68 | "HTTP/1.1 200 OK\r\n\ 69 | content-type: text/plain; charset=utf-8\r\n\ 70 | content-length: 12\r\n\ 71 | \r\n\ 72 | Hello world!" 73 | ); 74 | } 75 | 76 | #[test] 77 | fn pipeline_requests_in_2_parts() { 78 | Process::spawn_link(8902, hell_world_server); 79 | // Give enough time to for server to start 80 | sleep(Duration::from_millis(10)); 81 | let mut stream = TcpStream::connect("localhost:8902").unwrap(); 82 | let request = "\ 83 | GET / HTTP/1.1\r\n\ 84 | Content-length: 5\r\n\ 85 | \r\n\ 86 | Hello\ 87 | GET /" 88 | .as_bytes(); 89 | stream.write_all(request).unwrap(); 90 | let mut response = [0u8; 256]; 91 | // First response 92 | let n = stream.read(&mut response).unwrap(); 93 | let response_str = from_utf8(&response[..n]).unwrap(); 94 | assert_eq!( 95 | response_str, 96 | "HTTP/1.1 200 OK\r\n\ 97 | content-type: text/plain; charset=utf-8\r\n\ 98 | content-length: 12\r\n\ 99 | \r\n\ 100 | Hello world!" 101 | ); 102 | // Second response 103 | let request_rest = " HTTP/1.1\r\n\r\n".as_bytes(); 104 | stream.write_all(request_rest).unwrap(); 105 | 106 | let n = stream.read(&mut response).unwrap(); 107 | let response_str = from_utf8(&response[..n]).unwrap(); 108 | assert_eq!( 109 | response_str, 110 | "HTTP/1.1 200 OK\r\n\ 111 | content-type: text/plain; charset=utf-8\r\n\ 112 | content-length: 12\r\n\ 113 | \r\n\ 114 | Hello world!" 115 | ); 116 | } 117 | 118 | #[test] 119 | fn invalid_method() { 120 | Process::spawn_link(8903, hell_world_server); 121 | // Give enough time to for server to start 122 | sleep(Duration::from_millis(10)); 123 | let mut stream = TcpStream::connect("localhost:8903").unwrap(); 124 | let request = "INVALID / HTTP/1.1\r\n\r\n".as_bytes(); 125 | stream.write_all(request).unwrap(); 126 | let mut response = [0u8; 256]; 127 | let n = stream.read(&mut response).unwrap(); 128 | let response_str = from_utf8(&response[..n]).unwrap(); 129 | assert_eq!( 130 | response_str, 131 | "HTTP/1.1 404 Not Found\r\n\ 132 | content-type: text/html; charset=UTF-8\r\n\ 133 | content-length: 23\r\n\ 134 | \r\n\ 135 |

404: Not found

" 136 | ); 137 | } 138 | 139 | fn panic_server(port: u16, _: Mailbox<()>) { 140 | fn panic_handler() { 141 | panic!() 142 | } 143 | 144 | Application::new(router! { 145 | GET "/" => panic_handler 146 | }) 147 | .serve(format!("localhost:{port}")) 148 | .unwrap(); 149 | } 150 | 151 | #[test] 152 | fn handler_panics() { 153 | Process::spawn_link(8904, panic_server); 154 | // Give enough time to for server to start 155 | sleep(Duration::from_millis(10)); 156 | let mut stream = TcpStream::connect("localhost:8904").unwrap(); 157 | let request = "GET / HTTP/1.1\r\n\r\n".as_bytes(); 158 | stream.write_all(request).unwrap(); 159 | let mut response = [0u8; 256]; 160 | let n = stream.read(&mut response).unwrap(); 161 | let response_str = from_utf8(&response[..n]).unwrap(); 162 | assert_eq!( 163 | response_str, 164 | "HTTP/1.1 500 Internal Server Error\r\n\ 165 | content-type: text/plain; charset=utf-8\r\n\ 166 | content-length: 21\r\n\ 167 | \r\n\ 168 | Internal Server Error" 169 | ); 170 | } 171 | 172 | fn post_echo_server(port: u16, _: Mailbox<()>) { 173 | fn hello_world_handler(data: Body) -> Vec { 174 | data.as_slice().into() 175 | } 176 | 177 | Application::new(router! { 178 | POST "/" => hello_world_handler 179 | }) 180 | .serve(format!("localhost:{port}")) 181 | .unwrap(); 182 | } 183 | 184 | #[test] 185 | fn post_request_keep_alive() { 186 | Process::spawn_link(8905, post_echo_server); 187 | // Give enough time to for server to start 188 | sleep(Duration::from_millis(10)); 189 | let mut stream = TcpStream::connect("localhost:8905").unwrap(); 190 | let request = "\ 191 | POST / HTTP/1.1\r\n\ 192 | Content-length: 5\r\n\ 193 | \r\n\ 194 | Hello" 195 | .as_bytes(); 196 | stream.write_all(request).unwrap(); 197 | let mut response = [0u8; 256]; 198 | let n = stream.read(&mut response).unwrap(); 199 | let response_str = from_utf8(&response[..n]).unwrap(); 200 | assert_eq!( 201 | response_str, 202 | "HTTP/1.1 200 OK\r\n\ 203 | content-type: application/octet-stream\r\n\ 204 | content-length: 5\r\n\ 205 | \r\n\ 206 | Hello" 207 | ); 208 | } 209 | -------------------------------------------------------------------------------- /submillisecond_macros/src/router/trie.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::mem; 3 | 4 | #[derive(Debug)] 5 | pub struct Trie { 6 | node: TrieNode, 7 | } 8 | 9 | impl Default for Trie { 10 | fn default() -> Self { 11 | Trie { 12 | node: TrieNode::Empty, 13 | } 14 | } 15 | } 16 | 17 | impl Trie 18 | where 19 | V: Clone, 20 | { 21 | pub fn children(&self) -> Children { 22 | Children::new(TrieNode::Node { 23 | value: None, 24 | prefix: String::new(), 25 | children: vec![self.node.clone()], 26 | }) 27 | } 28 | 29 | pub fn insert(&mut self, key: String, new_value: V) { 30 | self.node.insert(key, new_value) 31 | } 32 | } 33 | 34 | #[derive(Debug, Clone)] 35 | pub enum TrieNode { 36 | Node { 37 | value: Option, 38 | prefix: String, 39 | children: Vec>, 40 | }, 41 | Empty, 42 | } 43 | 44 | impl TrieNode 45 | where 46 | V: Clone, 47 | { 48 | pub fn create_terminal(prefix: String, value: V) -> Self { 49 | TrieNode::Node { 50 | value: Some(value), 51 | prefix, 52 | children: vec![], 53 | } 54 | } 55 | 56 | pub fn insert(&mut self, key: String, new_value: V) { 57 | let key_len = key.len(); 58 | match self { 59 | TrieNode::Node { 60 | ref mut prefix, 61 | ref mut children, 62 | ref mut value, 63 | } => { 64 | let mut last_match = key_len; 65 | for (idx, b) in key.chars().enumerate() { 66 | // if node prefix ended, try to delegate to a child 67 | if idx >= prefix.len() { 68 | return Self::delegate_to_child( 69 | key[idx..].to_string(), 70 | new_value, 71 | children, 72 | ); 73 | } 74 | // if matches current node, delegate to a child 75 | if b == prefix.chars().nth(idx).unwrap() { 76 | continue; 77 | } else { 78 | // they are not the same, need to split at longest common prefix 79 | last_match = idx; 80 | break; 81 | } 82 | } 83 | // inserting the same key 84 | if last_match == prefix.len() { 85 | return; 86 | } 87 | // in this case, key_len will ALWAYS be shorter than prefix.len() 88 | // prefix has left-over data, need to split the prefix 89 | let (new_prefix, suffix) = prefix.split_at(last_match); 90 | // create new node that carries data from current node 91 | let new_child = TrieNode::Node { 92 | value: mem::take(value), 93 | prefix: suffix.to_string(), 94 | children: mem::take(children), 95 | }; 96 | // insert new node with new suffix if 97 | if last_match < key_len { 98 | // create new self 99 | *self = TrieNode::Node { 100 | value: None, 101 | prefix: new_prefix.to_string(), 102 | children: vec![ 103 | new_child, 104 | TrieNode::Node { 105 | value: Some(new_value), 106 | prefix: key.as_str()[last_match..].to_string(), 107 | children: vec![], 108 | }, 109 | ], 110 | }; 111 | } else { 112 | // create new self 113 | *self = TrieNode::Node { 114 | value: Some(new_value), 115 | prefix: new_prefix.to_string(), 116 | children: vec![new_child], 117 | }; 118 | } 119 | } 120 | TrieNode::Empty => { 121 | *self = TrieNode::create_terminal(key, new_value); 122 | } 123 | } 124 | } 125 | 126 | fn delegate_to_child(key: String, new_value: V, children: &mut Vec>) { 127 | let next = key.chars().next(); 128 | // if we find any existing match for the next one we pass it on 129 | let child = children.iter_mut().find(|c| { 130 | if let TrieNode::Node { prefix, .. } = c { 131 | // if first character matches we found the right child 132 | return prefix.chars().next() == next; 133 | } 134 | false 135 | }); 136 | if let Some(child) = child { 137 | child.insert(key, new_value); 138 | } else { 139 | // create a new terminal child 140 | children.push(TrieNode::create_terminal(key, new_value)); 141 | } 142 | } 143 | 144 | pub fn children(&self) -> Children { 145 | Children::new(self.clone()) 146 | } 147 | } 148 | 149 | #[derive(Clone, Debug)] 150 | pub struct Node { 151 | pub prefix: String, 152 | pub value: Option, 153 | pub trie_node: TrieNode, 154 | } 155 | 156 | impl Node 157 | where 158 | V: Clone, 159 | { 160 | pub fn children(&self) -> Children { 161 | self.trie_node.children() 162 | } 163 | 164 | pub fn is_leaf(&self) -> bool { 165 | if let TrieNode::Node { ref children, .. } = self.trie_node { 166 | return children.is_empty(); 167 | } 168 | true 169 | } 170 | } 171 | 172 | #[derive(Clone, Debug)] 173 | pub struct Children 174 | where 175 | V: Clone, 176 | { 177 | trie: TrieNode, 178 | idx_child: usize, 179 | } 180 | 181 | impl Children 182 | where 183 | V: Clone, 184 | { 185 | pub fn new(trie: TrieNode) -> Self { 186 | Children { trie, idx_child: 0 } 187 | } 188 | } 189 | 190 | impl Iterator for Children 191 | where 192 | V: Clone, 193 | { 194 | type Item = Node; 195 | 196 | fn next(&mut self) -> Option { 197 | match self.trie { 198 | TrieNode::Node { ref children, .. } if self.idx_child < children.len() => { 199 | let current_child = &children[self.idx_child]; 200 | self.idx_child += 1; 201 | if let TrieNode::Node { value, prefix, .. } = current_child { 202 | Some(Node { 203 | prefix: prefix.clone(), 204 | value: value.clone(), 205 | trie_node: current_child.clone(), 206 | }) 207 | } else { 208 | None 209 | } 210 | } 211 | _ => None, 212 | } 213 | } 214 | } 215 | 216 | #[cfg(test)] 217 | mod tests { 218 | use super::*; 219 | 220 | #[test] 221 | fn trie_basic() { 222 | let mut trie = Trie::default(); 223 | trie.insert("/".to_string(), "/"); 224 | trie.insert("/vec".to_string(), "/"); 225 | trie.insert("/json".to_string(), "/"); 226 | 227 | let prefixes: Vec<_> = trie.children().map(|c| c.prefix).collect(); 228 | assert_eq!(prefixes, vec!["/"]) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /submillisecond_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod named_param; 2 | mod router; 3 | mod static_router; 4 | 5 | use proc_macro::TokenStream; 6 | use router::Router; 7 | use static_router::StaticRouter; 8 | use syn::{parse_macro_input, DeriveInput}; 9 | 10 | /// The `NamedParam` derive macro can be used to implement `FromRequest` for a 11 | /// struct. 12 | /// 13 | /// If using with unnamed struct, then the `#[param(name = "...")]` attribute 14 | /// should be used. 15 | /// 16 | /// If using with a struct with named fields, then each field name should match 17 | /// the ones defined in the router. 18 | /// 19 | /// # Struct with fields example 20 | /// 21 | /// ```ignore 22 | /// #[derive(NamedParam)] 23 | /// struct Params { 24 | /// name: String, 25 | /// age: i32, 26 | /// } 27 | /// 28 | /// fn user_name_age(Params { name, age }: Params) -> String { 29 | /// format!("Hello {name}, you are {age} years old") 30 | /// } 31 | /// 32 | /// router! { 33 | /// GET "/user/:name/:age" => user_name_age 34 | /// } 35 | /// ``` 36 | /// 37 | /// # Unnamed struct example 38 | /// 39 | /// ```ignore 40 | /// #[derive(NamedParam)] 41 | /// #[param(name = "age")] 42 | /// struct AgeParam(i32); 43 | /// 44 | /// fn age_param(AgeParam(age): AgeParam) -> String { 45 | /// format!("You are {age} years old") 46 | /// } 47 | /// 48 | /// router! { 49 | /// GET "/user/:age" => age_param 50 | /// } 51 | /// ``` 52 | #[proc_macro_derive(NamedParam, attributes(param))] 53 | pub fn named_param(input: TokenStream) -> TokenStream { 54 | let input = parse_macro_input!(input as DeriveInput); 55 | match named_param::NamedParam::try_from(input) { 56 | Ok(named_param) => named_param.expand().into(), 57 | Err(err) => err.into_compile_error().into(), 58 | } 59 | } 60 | 61 | /// Macro for defining a router in [submillisecond](https://github.com/lunatic-solutions/submillisecond). 62 | /// 63 | /// The syntax in this macro is aimed to be as simple and intuitive as possible. 64 | /// 65 | /// # Handlers 66 | /// 67 | /// Handlers are routes with a HTTP method, path and handler. 68 | /// 69 | /// A basic example would be: 70 | /// 71 | /// ```ignore 72 | /// router! { 73 | /// GET "/home" => home_handler 74 | /// } 75 | /// ``` 76 | /// 77 | /// In this example, `home_handler` is a function we defined which implements 78 | /// `Handler`. 79 | /// 80 | /// Multiple routes can be defined with handlers: 81 | /// 82 | /// ```ignore 83 | /// router! { 84 | /// GET "/" => index_handler 85 | /// GET "/about" => about_handler 86 | /// POST "/avatar" => avatar_handler 87 | /// } 88 | /// ``` 89 | /// 90 | /// ## Methods 91 | /// 92 | /// The supported methods are: 93 | /// 94 | /// - GET 95 | /// - POST 96 | /// - PUT 97 | /// - DELETE 98 | /// - HEAD 99 | /// - OPTIONS 100 | /// - PATCH 101 | /// 102 | /// # Sub-routers 103 | /// 104 | /// Routers can be nested to create more complex routing. 105 | /// 106 | /// Sub-routers are a similar syntax as handlers, except that they do not have a 107 | /// method prefix, and have curly braces after the `=>`. 108 | /// 109 | /// ```ignore 110 | /// router! { 111 | /// "/admin" => { 112 | /// GET "/dashboard" => admin_dashboard 113 | /// POST "/auth" => admin_auth 114 | /// } 115 | /// } 116 | /// ``` 117 | /// 118 | /// The syntax in-between `{` and `}` is the same as the `router` macro itself. 119 | /// 120 | /// # Layers/middleware 121 | /// 122 | /// Handlers which call [`submillisecond::RequestContext::next_handler`](https://docs.rs/submillisecond/latest/submillisecond/struct.RequestContext.html#method.next_handler) are 123 | /// considered to be middleware. 124 | /// 125 | /// Middleware can be used in the router macro using the `with` keyword. 126 | /// 127 | /// ```ignore 128 | /// router! { 129 | /// with global_logger; 130 | /// } 131 | /// ``` 132 | /// 133 | /// Multiple middleware can be used with the array syntax. 134 | /// 135 | /// ```ignore 136 | /// router! { 137 | /// with [layer_one, layer_two]; 138 | /// } 139 | /// ``` 140 | /// 141 | /// In the examples above, the middleware is used on the whole router. 142 | /// Instead, we can also use middleware on a per-route basis. 143 | /// 144 | /// ```ignore 145 | /// router! { 146 | /// GET "/" with logger_layer => index_handler 147 | /// } 148 | /// ``` 149 | /// 150 | /// When using guards, middleware should be placed after the if statement. 151 | /// 152 | /// ```ignore 153 | /// router! { 154 | /// GET "/admin" if IsAdmin with logger_layer => admin_handler 155 | /// } 156 | /// ``` 157 | /// 158 | /// # Guards 159 | /// 160 | /// Guards can be used to protect routes. 161 | /// 162 | /// The syntax is similar to a regular `if` statement, and is placed after the 163 | /// path of a route. 164 | /// 165 | /// ```ignore 166 | /// router! { 167 | /// GET "/admin" if IsAdmin => admin_handler 168 | /// } 169 | /// ``` 170 | /// 171 | /// # Syntax 172 | /// 173 | /// ##### RouterDefinition 174 | /// 175 | /// > `{` 176 | /// > 177 | /// >     [_RouterMiddleware_]﹖ `;` 178 | /// > 179 | /// >     [_RouterItem_]* 180 | /// > 181 | /// >     [_RouterCatchAll_]﹖ 182 | /// > 183 | /// > `}` 184 | /// 185 | /// ##### RouterItem 186 | /// 187 | /// > [_RouterMethod_]﹖ [STRING_LITERAL] [_RouterIfStmt_]﹖ 188 | /// > [_RouterMiddleware_] `=>` [_RouterItemValue_] 189 | /// 190 | /// ##### RouterItemValue 191 | /// 192 | /// > [IDENTIFIER] | [_RouterDefinition_] 193 | /// 194 | /// ##### RouterMethod 195 | /// 196 | /// > `GET` | `POST` | `PUT` | `DELETE` | `HEAD` | `OPTIONS` | `PATCH` 197 | /// 198 | /// ##### RouterMiddleware 199 | /// 200 | /// > `with` [_RouterMiddlewareItem_] 201 | /// 202 | /// ##### RouterMiddlewareItem 203 | /// 204 | /// > [IDENTIFIER] | `[` [IDENTIFIER] `]` 205 | /// 206 | /// ##### RouterIfStmt 207 | /// 208 | /// > `if` [Expression] 209 | /// 210 | /// ##### RouterCatchAll 211 | /// 212 | /// > `_` `=>` [_RouterItemValue_] 213 | /// 214 | /// [_RouterDefinition_]: #routerdefinition 215 | /// [_RouterMiddleware_]: #routermiddleware 216 | /// [_RouterMiddlewareItem_]: #routermiddlewareitem 217 | /// [_RouterItem_]: #routeritem 218 | /// [_RouterItemValue_]: #routeritemvalue 219 | /// [_RouterMethod_]: #routermethod 220 | /// [_RouterIfStmt_]: #routerifstmt 221 | /// [_RouterCatchAll_]: #routercatchall 222 | /// 223 | /// [IDENTIFIER]: https://doc.rust-lang.org/reference/identifiers.html 224 | /// [STRING_LITERAL]: https://doc.rust-lang.org/reference/tokens.html#string-literals 225 | /// [Expression]: https://doc.rust-lang.org/reference/expressions.html 226 | #[proc_macro] 227 | pub fn router(input: TokenStream) -> TokenStream { 228 | let input = parse_macro_input!(input as Router); 229 | input.expand().into() 230 | } 231 | 232 | /// The static router can be used to serve static files within a folder. 233 | /// 234 | /// Two arguments can be passed to the router, with the first one being 235 | /// optional: 236 | /// - The path to your static directory (relative to your Cargo.toml). 237 | /// - A custom 404 handler (optional). 238 | /// 239 | /// # Basic example 240 | /// 241 | /// ```ignore 242 | /// static_router!("./static") 243 | /// ``` 244 | /// 245 | /// # Example with custom 404 handler 246 | /// 247 | /// ```ignore 248 | /// static_router!("./static", handle_404) 249 | /// ``` 250 | #[proc_macro] 251 | pub fn static_router(input: TokenStream) -> TokenStream { 252 | let input = parse_macro_input!(input as StaticRouter); 253 | input.expand().into() 254 | } 255 | 256 | macro_rules! hquote {( $($tt:tt)* ) => ( 257 | ::quote::quote_spanned! { ::proc_macro2::Span::mixed_site()=> 258 | $($tt)* 259 | } 260 | )} 261 | pub(crate) use hquote; 262 | -------------------------------------------------------------------------------- /src/extract/rejection.rs: -------------------------------------------------------------------------------- 1 | //! Rejection response types. 2 | 3 | use super::path::FailedToDeserializePathParams; 4 | use crate::response::IntoResponse; 5 | use crate::Response; 6 | #[cfg(feature = "query")] 7 | use crate::{BoxError, Error}; 8 | 9 | define_rejection! { 10 | #[status = INTERNAL_SERVER_ERROR] 11 | #[body = "No paths parameters found for matched route. Are you also extracting `Request<_>`?"] 12 | /// Rejection type used if axum's internal representation of path parameters 13 | /// is missing. This is commonly caused by extracting `Request<_>`. `Path` 14 | /// must be extracted first. 15 | pub struct MissingPathParams; 16 | } 17 | 18 | composite_rejection! { 19 | /// Rejection used for [`Path`](super::Path). 20 | /// 21 | /// Contains one variant for each way the [`Path`](super::Path) extractor 22 | /// can fail. 23 | pub enum PathRejection { 24 | FailedToDeserializePathParams, 25 | MissingPathParams, 26 | } 27 | } 28 | 29 | /// Rejection type for extractors that deserialize query strings if the input 30 | /// couldn't be deserialized into the target type. 31 | #[cfg(feature = "query")] 32 | #[derive(Debug)] 33 | pub struct FailedToDeserializeQueryString { 34 | error: Error, 35 | type_name: &'static str, 36 | } 37 | 38 | #[cfg(feature = "query")] 39 | impl FailedToDeserializeQueryString { 40 | #[doc(hidden)] 41 | pub fn __private_new(error: E) -> Self 42 | where 43 | E: Into, 44 | { 45 | FailedToDeserializeQueryString { 46 | error: Error::new(error), 47 | type_name: std::any::type_name::(), 48 | } 49 | } 50 | } 51 | 52 | #[cfg(feature = "query")] 53 | impl IntoResponse for FailedToDeserializeQueryString { 54 | fn into_response(self) -> Response { 55 | (http::StatusCode::UNPROCESSABLE_ENTITY, self.to_string()).into_response() 56 | } 57 | } 58 | 59 | #[cfg(feature = "query")] 60 | impl std::fmt::Display for FailedToDeserializeQueryString { 61 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 62 | write!( 63 | f, 64 | "Failed to deserialize query string. Expected something of type `{}`. Error: {}", 65 | self.type_name, self.error, 66 | ) 67 | } 68 | } 69 | 70 | #[cfg(feature = "query")] 71 | impl std::error::Error for FailedToDeserializeQueryString {} 72 | 73 | #[cfg(feature = "query")] 74 | composite_rejection! { 75 | /// Rejection used for [`Query`](super::Query). 76 | /// 77 | /// Contains one variant for each way the [`Query`](super::Query) extractor 78 | /// can fail. 79 | pub enum QueryRejection { 80 | FailedToDeserializeQueryString, 81 | } 82 | } 83 | 84 | define_rejection! { 85 | #[status = BAD_REQUEST] 86 | #[body = "Request body didn't contain valid UTF-8"] 87 | /// Rejection type used when buffering the request into a [`String`] if the 88 | /// body doesn't contain valid UTF-8. 89 | pub struct InvalidUtf8(Error); 90 | } 91 | 92 | composite_rejection! { 93 | /// Rejection used for [`String`]. 94 | /// 95 | /// Contains one variant for each way the [`String`] extractor can fail. 96 | pub enum StringRejection { 97 | InvalidUtf8, 98 | } 99 | } 100 | 101 | define_rejection! { 102 | #[status = BAD_REQUEST] 103 | #[body = "No host found in request"] 104 | /// Rejection type used if the [`Host`](super::Host) extractor is unable to 105 | /// resolve a host. 106 | pub struct FailedToResolveHost; 107 | } 108 | 109 | composite_rejection! { 110 | /// Rejection used for [`Host`](super::Host). 111 | /// 112 | /// Contains one variant for each way the [`Host`](super::Host) extractor 113 | /// can fail. 114 | pub enum HostRejection { 115 | FailedToResolveHost, 116 | } 117 | } 118 | 119 | #[cfg(feature = "json")] 120 | define_rejection! { 121 | #[status = UNPROCESSABLE_ENTITY] 122 | #[body = "Failed to deserialize the JSON body into the target type"] 123 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 124 | /// Rejection type for [`Json`](crate::json::Json). 125 | /// 126 | /// This rejection is used if the request body is syntactically valid JSON but couldn't be 127 | /// deserialized into the target type. 128 | pub struct JsonDataError(Error); 129 | } 130 | 131 | #[cfg(feature = "json")] 132 | define_rejection! { 133 | #[status = BAD_REQUEST] 134 | #[body = "Failed to parse the request body as JSON"] 135 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 136 | /// Rejection type for [`Json`](crate::json::Json). 137 | /// 138 | /// This rejection is used if the request body didn't contain syntactically valid JSON. 139 | pub struct JsonSyntaxError(Error); 140 | } 141 | 142 | #[cfg(feature = "json")] 143 | define_rejection! { 144 | #[status = UNSUPPORTED_MEDIA_TYPE] 145 | #[body = "Expected request with `Content-Type: application/json`"] 146 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 147 | /// Rejection type for [`Json`](crate::json::Json) used if the `Content-Type` 148 | /// header is missing. 149 | pub struct MissingJsonContentType; 150 | } 151 | 152 | #[cfg(feature = "json")] 153 | composite_rejection! { 154 | /// Rejection used for [`Json`](crate::json::Json). 155 | /// 156 | /// Contains one variant for each way the [`Json`](crate::json::Json) extractor 157 | /// can fail. 158 | #[cfg_attr(docsrs, doc(cfg(feature = "json")))] 159 | pub enum JsonRejection { 160 | JsonDataError, 161 | JsonSyntaxError, 162 | MissingJsonContentType, 163 | } 164 | } 165 | 166 | /// Rejection used for [`TypedHeader`](crate::TypedHeader). 167 | #[derive(Debug)] 168 | pub struct TypedHeaderRejection { 169 | pub(crate) name: &'static http::header::HeaderName, 170 | pub(crate) reason: TypedHeaderRejectionReason, 171 | } 172 | 173 | impl TypedHeaderRejection { 174 | /// Name of the header that caused the rejection 175 | pub fn name(&self) -> &http::header::HeaderName { 176 | self.name 177 | } 178 | 179 | /// Reason why the header extraction has failed 180 | pub fn reason(&self) -> &TypedHeaderRejectionReason { 181 | &self.reason 182 | } 183 | } 184 | 185 | /// Additional information regarding a [`TypedHeaderRejection`] 186 | #[derive(Debug)] 187 | #[non_exhaustive] 188 | pub enum TypedHeaderRejectionReason { 189 | /// The header was missing from the HTTP request 190 | Missing, 191 | /// An error occurred when parsing the header from the HTTP request 192 | Error(headers::Error), 193 | } 194 | 195 | impl IntoResponse for TypedHeaderRejection { 196 | fn into_response(self) -> Response { 197 | (http::StatusCode::BAD_REQUEST, self.to_string()).into_response() 198 | } 199 | } 200 | 201 | impl std::fmt::Display for TypedHeaderRejection { 202 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 203 | match &self.reason { 204 | TypedHeaderRejectionReason::Missing => { 205 | write!(f, "Header of type `{}` was missing", self.name) 206 | } 207 | TypedHeaderRejectionReason::Error(err) => { 208 | write!(f, "{} ({})", err, self.name) 209 | } 210 | } 211 | } 212 | } 213 | 214 | impl std::error::Error for TypedHeaderRejection { 215 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 216 | match &self.reason { 217 | TypedHeaderRejectionReason::Error(err) => Some(err), 218 | TypedHeaderRejectionReason::Missing => None, 219 | } 220 | } 221 | } 222 | 223 | define_rejection! { 224 | #[status = INTERNAL_SERVER_ERROR] 225 | #[body = "State not initialized"] 226 | /// Rejection type used when loading state without initializing. 227 | pub struct NotInitialized(Error); 228 | } 229 | 230 | composite_rejection! { 231 | /// Rejection used for [`State`]. 232 | /// 233 | /// Contains one variant for each way the [`State`] extractor can fail. 234 | pub enum StateRejection { 235 | NotInitialized, 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | 3 | use httparse::{self, Status, EMPTY_HEADER}; 4 | use lunatic::net::TcpStream; 5 | 6 | const MAX_REQUEST_SIZE: usize = 10 * 1024 * 1024; 7 | const REQUEST_BUFFER_SIZE: usize = 4096; 8 | const MAX_HEADERS: usize = 128; 9 | 10 | /// The request body. 11 | #[derive(Debug, Clone, Copy)] 12 | pub struct Body<'a>(&'a [u8]); 13 | 14 | impl<'a> Body<'a> { 15 | /// Create a request body from a slice. 16 | pub fn from_slice(slice: &'a [u8]) -> Self { 17 | Self(slice) 18 | } 19 | 20 | /// Returns the request body as a slice. 21 | pub fn as_slice(&self) -> &[u8] { 22 | self.0 23 | } 24 | 25 | /// Returns the body length in bytes. 26 | pub fn len(&self) -> usize { 27 | self.0.len() 28 | } 29 | 30 | /// Returns true if body is empty. 31 | pub fn is_empty(&self) -> bool { 32 | self.0.is_empty() 33 | } 34 | } 35 | 36 | /// The result of parsing a request from a buffer. 37 | type RequestResult<'a> = Result>, ParseRequestError>; 38 | /// Data belonging to the next request. 39 | type NextRequest = Vec; 40 | 41 | /// One or more HTTP request. 42 | /// 43 | /// One TCP read can yield multiple pipelined requests. We keep the data of the 44 | /// next request(s) around (without parsing it) and seed the next handler 45 | /// process with it. 46 | pub(crate) struct PipelinedRequests<'a> { 47 | request: RequestResult<'a>, 48 | next: NextRequest, 49 | } 50 | 51 | impl<'a> PipelinedRequests<'a> { 52 | /// Returns the result of parsing the first request + data belonging to 53 | /// other pipelined requests. 54 | pub(crate) fn pipeline(self) -> (RequestResult<'a>, NextRequest) { 55 | (self.request, self.next) 56 | } 57 | } 58 | 59 | impl<'a> PipelinedRequests<'a> { 60 | /// A complete request means **only one** complete request is the buffer and 61 | /// no pipelined requests. 62 | fn from_complete(request: http::Request>) -> Self { 63 | PipelinedRequests { 64 | request: Ok(request), 65 | next: Vec::new(), 66 | } 67 | } 68 | 69 | /// One complete request and data belonging to others is contained in the 70 | /// buffer. 71 | fn from_pipeline(request: http::Request>, next: Vec) -> Self { 72 | PipelinedRequests { 73 | request: Ok(request), 74 | next, 75 | } 76 | } 77 | 78 | /// If the first request can't be parsed correctly, it doesn't make sense to 79 | /// attempt parsing pipelined requests. 80 | fn from_err(err: ParseRequestError) -> Self { 81 | PipelinedRequests { 82 | request: Err(err), 83 | next: Vec::new(), 84 | } 85 | } 86 | } 87 | 88 | pub(crate) fn parse_requests<'a>( 89 | request_buffer: &'a mut Vec, 90 | stream: &mut TcpStream, 91 | ) -> PipelinedRequests<'a> { 92 | let mut buffer = [0_u8; REQUEST_BUFFER_SIZE]; 93 | let mut headers = [EMPTY_HEADER; MAX_HEADERS]; 94 | 95 | // Loop until at least one complete request is read. 96 | let (request_raw, offset) = loop { 97 | // In case of pipelined requests the `request_buffer` is going to come 98 | // prefilled with some data, and we should attempt to parse it into a request 99 | // before we decide to read more from `TcpStream`. 100 | let mut request_raw = httparse::Request::new(&mut headers); 101 | match request_raw.parse(request_buffer) { 102 | Ok(state) => match state { 103 | Status::Complete(offset) => { 104 | // Continue outside the loop. 105 | break (request_raw, offset); 106 | } 107 | Status::Partial => { 108 | // Read more data from TCP stream 109 | let n = stream.read(&mut buffer); 110 | if n.is_err() || *n.as_ref().unwrap() == 0 { 111 | if request_buffer.is_empty() { 112 | return PipelinedRequests::from_err( 113 | ParseRequestError::TcpStreamClosedWithoutData, 114 | ); 115 | } else { 116 | return PipelinedRequests::from_err(ParseRequestError::TcpStreamClosed); 117 | } 118 | } 119 | // Invalidate references in `headers` that could point to the previous 120 | // `request_buffer` before extending it. 121 | headers = [EMPTY_HEADER; MAX_HEADERS]; 122 | request_buffer.extend(&buffer[..(n.unwrap())]); 123 | // If request passed max size, abort 124 | if request_buffer.len() > MAX_REQUEST_SIZE { 125 | return PipelinedRequests::from_err(ParseRequestError::RequestTooLarge); 126 | } 127 | } 128 | }, 129 | Err(err) => { 130 | return PipelinedRequests::from_err(ParseRequestError::HttpParseError(err)); 131 | } 132 | } 133 | }; 134 | 135 | // At this point one full request header is available, but the body (if it 136 | // exists) might not be fully loaded yet. 137 | 138 | let method = match http::Method::try_from(request_raw.method.unwrap()) { 139 | Ok(method) => method, 140 | Err(_) => { 141 | return PipelinedRequests::from_err(ParseRequestError::UnknownMethod); 142 | } 143 | }; 144 | let request = http::Request::builder() 145 | .method(method) 146 | .uri(request_raw.path.unwrap()); 147 | let mut content_length = None; 148 | let request = request_raw.headers.iter().fold(request, |request, header| { 149 | if header.name.to_lowercase() == "content-length" { 150 | let value_string = std::str::from_utf8(header.value).unwrap(); 151 | let length = value_string.parse::().unwrap(); 152 | content_length = Some(length); 153 | } 154 | request.header(header.name, header.value) 155 | }); 156 | // If content-length exists, request has a body 157 | if let Some(content_length) = content_length { 158 | #[allow(clippy::comparison_chain)] 159 | if request_buffer[offset..].len() == content_length { 160 | // Complete content is captured from the request w/o trailing pipelined 161 | // requests. 162 | PipelinedRequests::from_complete( 163 | request 164 | .body(Body::from_slice(&request_buffer[offset..])) 165 | .unwrap(), 166 | ) 167 | } else if request_buffer[offset..].len() > content_length { 168 | // Complete content is captured from the request with trailing pipelined 169 | // requests. 170 | PipelinedRequests::from_pipeline( 171 | request 172 | .body(Body::from_slice(&request_buffer[offset..])) 173 | .unwrap(), 174 | Vec::from(&request_buffer[offset + content_length..]), 175 | ) 176 | } else { 177 | // Read the rest from TCP stream to form a full request 178 | let rest = content_length - request_buffer[offset..].len(); 179 | let mut buffer = vec![0u8; rest]; 180 | stream.read_exact(&mut buffer).unwrap(); 181 | request_buffer.extend(&buffer); 182 | PipelinedRequests::from_complete( 183 | request 184 | .body(Body::from_slice(&request_buffer[offset..])) 185 | .unwrap(), 186 | ) 187 | } 188 | } else { 189 | // If the offset points to the end of `requests_buffer` we have a full request, 190 | // w/o a trailing pipelined request. 191 | if request_buffer[offset..].is_empty() { 192 | PipelinedRequests::from_complete(request.body(Body::from_slice(&[])).unwrap()) 193 | } else { 194 | PipelinedRequests::from_pipeline( 195 | request.body(Body::from_slice(&[])).unwrap(), 196 | Vec::from(&request_buffer[offset..]), 197 | ) 198 | } 199 | } 200 | } 201 | 202 | #[derive(Debug)] 203 | pub(crate) enum ParseRequestError { 204 | TcpStreamClosed, 205 | TcpStreamClosedWithoutData, 206 | HttpParseError(httparse::Error), 207 | RequestTooLarge, 208 | UnknownMethod, 209 | } 210 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # submillisecond 2 | 3 | A [lunatic] web framework for the Rust language. 4 | 5 | Submillisecond is a **backend** web framework around the Rust language, 6 | [WebAssembly's][wasm] security and the [lunatic scheduler][lunatic_gh]. 7 | 8 | > This is an early stage project, probably has bugs and the API is still changing. It's also 9 | > important to point out that many Rust crates don't compile to WebAssembly yet and can't be used 10 | > with submillisecond. 11 | 12 | If you would like to ask for help or just follow the discussions around Lunatic & submillisecond, 13 | [join our discord server][discord]. 14 | 15 | # Features 16 | 17 | - Fast compilation times 18 | - async-free - All preemption and scheduling is done by [lunatic][lunatic_gh] 19 | - strong security - Each request is handled in a separate _lunatic_ process 20 | - Batteries included 21 | - Cookies 22 | - Json 23 | - Logging 24 | - Websockets 25 | - [Submillisecond LiveView] - Frontend web framework 26 | 27 | [submillisecond liveview]: https://github.com/lunatic-solutions/submillisecond-live-view 28 | 29 | # Code example 30 | 31 | ```rust 32 | use submillisecond::{router, Application}; 33 | 34 | fn index() -> &'static str { 35 | "Hello :)" 36 | } 37 | 38 | fn main() -> std::io::Result<()> { 39 | Application::new(router! { 40 | GET "/" => index 41 | }) 42 | .serve("0.0.0.0:3000") 43 | } 44 | ``` 45 | 46 | ## Getting started with lunatic 47 | 48 | To run the example you will first need to download the lunatic runtime by following the 49 | installation steps in [this repository][lunatic_gh]. The runtime is just a single executable and runs on 50 | Windows, macOS and Linux. If you have already Rust installed, you can get it with: 51 | 52 | ```bash 53 | cargo install lunatic-runtime 54 | ``` 55 | 56 | [Lunatic][lunatic_gh] applications need to be compiled to [WebAssembly][wasm] before they can be executed by 57 | the runtime. Rust has great support for WebAssembly and you can build a lunatic compatible 58 | application just by passing the `--target=wasm32-wasi` flag to cargo, e.g: 59 | 60 | ```bash 61 | # Add the WebAssembly target 62 | rustup target add wasm32-wasi 63 | # Build the app 64 | cargo build --release --target=wasm32-wasi 65 | ``` 66 | 67 | This will generate a .wasm file in the `target/wasm32-wasi/release/` folder inside your project. 68 | You can now run your application by passing the generated .wasm file to Lunatic, e.g: 69 | 70 | ``` 71 | lunatic target/wasm32-wasi/release/.wasm 72 | ``` 73 | 74 | #### Better developer experience 75 | 76 | To simplify developing, testing and running lunatic applications with cargo, you can add a 77 | `.cargo/config.toml` file to your project with the following content: 78 | 79 | ```toml 80 | [build] 81 | target = "wasm32-wasi" 82 | 83 | [target.wasm32-wasi] 84 | runner = "lunatic" 85 | ``` 86 | 87 | Now you can just use the commands you are already familiar with, such as `cargo run`, `cargo test` 88 | and cargo is going to automatically build your project as a WebAssembly module and run it inside 89 | `lunatic`. 90 | 91 | ## Getting started with submillisecond 92 | 93 | Add it as a dependency 94 | 95 | ```toml 96 | submillisecond = "0.3.0" 97 | ``` 98 | 99 | ## Handlers 100 | 101 | Handlers are functions which return a response which implements [`IntoResponse`][intoresponse]. 102 | 103 | They can have any number of arguments, where each argument is an [extractor]. 104 | 105 | ```rust 106 | fn index(body: Vec, cookies: Cookies) -> String { 107 | // ... 108 | } 109 | ``` 110 | 111 | ## Routers 112 | 113 | Submillisecond provides a [`router!`][router] macro for defining routes in your app. 114 | 115 | ```rust 116 | #[derive(NamedParam)] 117 | struct User { 118 | first_name: String, 119 | last_name: String, 120 | } 121 | 122 | fn hi(user: User) -> String { 123 | format!("Hi {} {}!", user.first_name, user.last_name) 124 | } 125 | 126 | fn main() -> std::io::Result<()> { 127 | Application::new(router! { 128 | GET "/hi/:first_name/:last_name" => hi 129 | POST "/update_data" => update_age 130 | }) 131 | .serve("0.0.0.0:3000") 132 | } 133 | ``` 134 | 135 | The router macro supports: 136 | 137 | - [Nested routes](#nested-routes) 138 | - [Url parameters](#url-parameters) 139 | - [Catch-all](#catch-all) 140 | - [Guards](#guards) 141 | - [Middleware](#middleware) 142 | 143 | ### Nested routes 144 | 145 | Routes can be nested. 146 | 147 | ```rust 148 | router! { 149 | "/foo" => { 150 | GET "/bar" => bar 151 | } 152 | } 153 | ``` 154 | 155 | ### Url parameters 156 | 157 | Uri parameters can be captured with the [Path] extractor. 158 | 159 | ```rust 160 | router! { 161 | GET "/users/:first/:last/:age" => greet 162 | } 163 | 164 | fn greet(Path((first, last, age)): Path<(String, String, u32)>) -> String { 165 | format!("Welcome {first} {last}. You are {age} years old.") 166 | } 167 | ``` 168 | 169 | You can use the [NamedParam] derive macro to define named parameters. 170 | 171 | ```rust 172 | router! { 173 | GET "/users/:first/:last/:age" => greet 174 | } 175 | 176 | #[derive(NamedParam)] 177 | struct GreetInfo { 178 | first: String, 179 | last: String, 180 | age: u32, 181 | } 182 | 183 | fn greet(GreetInfo { first, last, age }: GreetInfo) -> String { 184 | format!("Welcome {first} {last}. You are {age} years old.") 185 | } 186 | ``` 187 | 188 | Alternatively, you can access the params directly with the [Params] extractor. 189 | 190 | ### Catch-all 191 | 192 | The `_` syntax can be used to catch-all routes. 193 | 194 | ```rust 195 | router! { 196 | "/foo" => { 197 | GET "/bar" => bar 198 | _ => matches_foo_but_not_bar 199 | } 200 | _ => not_found 201 | } 202 | ``` 203 | 204 | ### Guards 205 | 206 | Routes can be protected by guards. 207 | 208 | ```rust 209 | struct ContentLengthLimit(u64); 210 | 211 | impl Guard for ContentLengthLimit { 212 | fn check(&self, req: &RequestContext) -> bool { 213 | // ... 214 | } 215 | } 216 | 217 | router! { 218 | "/short_requests" if ContentLengthGuard(128) => { 219 | POST "/super" if ContentLengthGuard(64) => super_short 220 | POST "/" => short 221 | } 222 | } 223 | ``` 224 | 225 | Guards can be chained with the `&&` and `||` syntax. 226 | 227 | ### Middleware 228 | 229 | Middleware is any handler which calls [`next_handler`][next_handler] on the request context. 230 | Like handlers, it can use extractors. 231 | 232 | ```rust 233 | fn logger(req: RequestContext) -> Response { 234 | println!("Before"); 235 | let result = req.next_handler(); 236 | println!("After"); 237 | result 238 | } 239 | 240 | fn main() -> std::io::Result<()> { 241 | Application::new(router! { 242 | with logger; 243 | 244 | GET "/" => hi 245 | }) 246 | .serve("0.0.0.0:3000") 247 | } 248 | ``` 249 | 250 | Middleware can be chained together, and placed within sub-routes. 251 | 252 | ```rust 253 | router! { 254 | with [mid1, mid2]; 255 | 256 | "/foo" => { 257 | with [foo_mid1, foo_mid2]; 258 | } 259 | } 260 | ``` 261 | 262 | They can also be specific to a single route. 263 | 264 | ```rust 265 | router! { 266 | GET "/" with mid1 => home 267 | } 268 | ``` 269 | 270 | ## Testing 271 | 272 | Lunatic provides a macro `#[lunatic::test]` to turn your tests into processes. Check out the 273 | [`tests`][tests] folder for examples. 274 | 275 | # License 276 | 277 | Licensed under either of 278 | 279 | - Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0) 280 | - MIT license (http://opensource.org/licenses/MIT) 281 | 282 | at your option. 283 | 284 | [lunatic]: https://lunatic.solutions 285 | [lunatic_gh]: https://github.com/lunatic-solutions/lunatic 286 | [wasm]: https://webassembly.org 287 | [discord]: https://discord.gg/b7zDqpXpB4 288 | [tests]: /tests 289 | [router]: https://docs.rs/submillisecond/latest/submillisecond/macro.router.html 290 | [intoresponse]: https://docs.rs/submillisecond/latest/submillisecond/response/trait.IntoResponse.html 291 | [params]: https://docs.rs/submillisecond/latest/submillisecond/params/struct.Params.html 292 | [path]: https://docs.rs/submillisecond/latest/submillisecond/extract/path/struct.Path.html 293 | [namedparam]: https://docs.rs/submillisecond/latest/submillisecond/derive.NamedParam.html 294 | [extractor]: https://docs.rs/submillisecond/latest/submillisecond/extract/trait.FromRequest.html 295 | [next_handler]: https://docs.rs/submillisecond/latest/submillisecond/struct.RequestContext.html#method.next_handler 296 | -------------------------------------------------------------------------------- /tests/router.rs: -------------------------------------------------------------------------------- 1 | use http::Method; 2 | use lunatic::net::TcpStream; 3 | use lunatic::test; 4 | use submillisecond::response::Response; 5 | use submillisecond::{http, router, Body, Handler, RequestContext}; 6 | 7 | macro_rules! build_request { 8 | ($method: ident, $uri: literal) => { 9 | build_request!($method, $uri, &[]) 10 | }; 11 | ($method: ident, $uri: literal, $body: expr) => { 12 | RequestContext::new( 13 | http::Request::builder() 14 | .method(Method::$method) 15 | .uri($uri) 16 | .body(Body::from_slice($body)) 17 | .unwrap(), 18 | TcpStream::connect("127.0.0.1:22").unwrap(), 19 | ) 20 | }; 21 | } 22 | 23 | macro_rules! handle_request { 24 | ($router: ident, $method: ident, $uri: literal) => {{ 25 | let req = build_request!($method, $uri); 26 | Handler::handle(&$router(), req) 27 | }}; 28 | ($router: ident, $method: ident, $uri: literal, $body: expr) => {{ 29 | let req = build_request!($method, $uri, $body); 30 | Handler::handle(&$router(), req) 31 | }}; 32 | } 33 | 34 | macro_rules! assert_200 { 35 | ($res: expr) => { 36 | assert!(res.status().is_success(), "response wasn't 200"); 37 | }; 38 | ($res: expr, $body: expr) => { 39 | assert!($res.status().is_success(), "response wasn't 200"); 40 | assert_eq!($body, $res.into_body().as_slice()); 41 | }; 42 | } 43 | 44 | macro_rules! assert_404 { 45 | ($res: expr) => { 46 | assert!( 47 | $res.status() == http::StatusCode::NOT_FOUND, 48 | "response wasn't 404, but was:\n{:?}", 49 | $res 50 | ) 51 | }; 52 | } 53 | 54 | fn simple_handler() -> &'static str { 55 | "OK" 56 | } 57 | 58 | #[test] 59 | fn simple_router() { 60 | let router = router! { 61 | GET "/" => simple_handler 62 | }; 63 | 64 | // 200 65 | let res = handle_request!(router, GET, "/"); 66 | assert_200!(res, b"OK"); 67 | 68 | // 404 69 | let res = handle_request!(router, POST, "/"); 70 | assert_404!(res); 71 | } 72 | 73 | fn echo_handler(body: Vec) -> Vec { 74 | body 75 | } 76 | 77 | #[test] 78 | fn echo_router() { 79 | let router = router! { 80 | POST "/echo" => echo_handler 81 | }; 82 | 83 | // 200 84 | let res = handle_request!(router, POST, "/echo", b"Hello, world!"); 85 | assert_200!(res, b"Hello, world!"); 86 | 87 | // 404 88 | let res = handle_request!(router, GET, "/echo", b"Hello, world!"); 89 | assert_404!(res); 90 | } 91 | 92 | #[test] 93 | fn nested_router() { 94 | let router = router! { 95 | "/a" => { 96 | "/b" => { 97 | GET "/c" => simple_handler 98 | } 99 | } 100 | }; 101 | 102 | // 200 103 | let res = handle_request!(router, GET, "/a/b/c"); 104 | assert_200!(res, b"OK"); 105 | 106 | // 404 107 | let res = handle_request!(router, GET, "/a/b/d"); 108 | assert_404!(res); 109 | 110 | let res = handle_request!(router, GET, "/a/b/c/d"); 111 | assert_404!(res); 112 | 113 | let res = handle_request!(router, GET, "/a/b"); 114 | assert_404!(res); 115 | 116 | let res = handle_request!(router, GET, "/a"); 117 | assert_404!(res); 118 | 119 | let res = handle_request!(router, POST, "/a/b/c"); 120 | assert_404!(res); 121 | } 122 | 123 | #[test] 124 | fn param_router() { 125 | let router = router! { 126 | "/:a" => { 127 | "/:b" => { 128 | "/:c" => { 129 | GET "/:one/:two/:three" => simple_handler 130 | } 131 | } 132 | } 133 | }; 134 | 135 | // 200 136 | let res = handle_request!(router, GET, "/a/b/c/one/two/three"); 137 | assert_200!(res, b"OK"); 138 | 139 | // 404 140 | let res = handle_request!(router, GET, "/a/b/c/one/two"); 141 | assert_404!(res); 142 | 143 | let res = handle_request!(router, GET, "/a/b/c/one/two/three/four"); 144 | assert_404!(res); 145 | } 146 | 147 | #[test] 148 | fn fallthrough_router() { 149 | let router = router! { 150 | GET "/a" => simple_handler 151 | GET "/b" => simple_handler 152 | GET "/c" => simple_handler 153 | GET "/:foo" => simple_handler 154 | POST "/:foo" => simple_handler 155 | }; 156 | 157 | // 200 158 | let res = handle_request!(router, GET, "/a"); 159 | assert_200!(res, b"OK"); 160 | 161 | let res = handle_request!(router, GET, "/b"); 162 | assert_200!(res, b"OK"); 163 | 164 | let res = handle_request!(router, GET, "/c"); 165 | assert_200!(res, b"OK"); 166 | 167 | let res = handle_request!(router, GET, "/hello"); 168 | assert_200!(res, b"OK"); 169 | 170 | let res = handle_request!(router, POST, "/hello"); 171 | assert_200!(res, b"OK"); 172 | 173 | let res = handle_request!(router, GET, "/hello/"); 174 | assert_200!(res, b"OK"); 175 | 176 | // 404 177 | let res = handle_request!(router, GET, "/a/b"); 178 | assert_404!(res); 179 | } 180 | 181 | #[test] 182 | fn all_methods_router() { 183 | let router = router! { 184 | GET "/get" => simple_handler 185 | POST "/post" => simple_handler 186 | PUT "/put" => simple_handler 187 | DELETE "/delete" => simple_handler 188 | HEAD "/head" => simple_handler 189 | OPTIONS "/options" => simple_handler 190 | PATCH "/patch" => simple_handler 191 | }; 192 | 193 | // 200 194 | let res = handle_request!(router, GET, "/get"); 195 | assert_200!(res, b"OK"); 196 | 197 | let res = handle_request!(router, POST, "/post"); 198 | assert_200!(res, b"OK"); 199 | 200 | let res = handle_request!(router, PUT, "/put"); 201 | assert_200!(res, b"OK"); 202 | 203 | let res = handle_request!(router, DELETE, "/delete"); 204 | assert_200!(res, b"OK"); 205 | 206 | let res = handle_request!(router, HEAD, "/head"); 207 | assert_200!(res, b"OK"); 208 | 209 | let res = handle_request!(router, OPTIONS, "/options"); 210 | assert_200!(res, b"OK"); 211 | 212 | let res = handle_request!(router, PATCH, "/patch"); 213 | assert_200!(res, b"OK"); 214 | 215 | // 404 216 | let res = handle_request!(router, GET, "/post"); 217 | assert_404!(res); 218 | 219 | let res = handle_request!(router, POST, "/put"); 220 | assert_404!(res); 221 | 222 | let res = handle_request!(router, PUT, "/delete"); 223 | assert_404!(res); 224 | 225 | let res = handle_request!(router, DELETE, "/head"); 226 | assert_404!(res); 227 | 228 | let res = handle_request!(router, HEAD, "/options"); 229 | assert_404!(res); 230 | 231 | let res = handle_request!(router, OPTIONS, "/patch"); 232 | assert_404!(res); 233 | 234 | let res = handle_request!(router, PATCH, "/get"); 235 | assert_404!(res); 236 | } 237 | 238 | #[test] 239 | fn wildcard_router() { 240 | let router = router! { 241 | GET "/" => simple_handler 242 | GET "/foo" => simple_handler 243 | GET "/any-*" => simple_handler 244 | }; 245 | 246 | // 200 247 | let res = handle_request!(router, GET, "/"); 248 | assert_200!(res, b"OK"); 249 | 250 | let res = handle_request!(router, GET, "/foo"); 251 | assert_200!(res, b"OK"); 252 | 253 | let res = handle_request!(router, GET, "/any-"); 254 | assert_200!(res, b"OK"); 255 | 256 | let res = handle_request!(router, GET, "/any-thing"); 257 | assert_200!(res, b"OK"); 258 | 259 | let res = handle_request!(router, GET, "/any-thing/at/all"); 260 | assert_200!(res, b"OK"); 261 | 262 | // 400 263 | let res = handle_request!(router, GET, "/abc"); 264 | assert_404!(res); 265 | 266 | let res = handle_request!(router, GET, "/abc/def"); 267 | assert_404!(res); 268 | 269 | let res = handle_request!(router, GET, "/fooo"); 270 | assert_404!(res); 271 | 272 | let res = handle_request!(router, GET, "/any"); 273 | assert_404!(res); 274 | } 275 | 276 | fn handle_aaa() -> Response { 277 | Response::builder().body(b"aaa".to_vec()).unwrap() 278 | } 279 | 280 | fn handle_aab() -> Response { 281 | Response::builder().body(b"aab".to_vec()).unwrap() 282 | } 283 | 284 | fn handle_bbb() -> Response { 285 | Response::builder().body(b"bbb".to_vec()).unwrap() 286 | } 287 | 288 | fn handle_bba() -> Response { 289 | Response::builder().body(b"bba".to_vec()).unwrap() 290 | } 291 | 292 | // this test will make sure that if a trie node 293 | // needs to be split up due to a matching prefix 294 | // that the previous children will still be handled 295 | #[test] 296 | fn router_split() { 297 | let router = router! { 298 | GET "/aaa" => handle_aaa 299 | GET "/aab" => handle_aab 300 | GET "/bbb" => handle_bbb 301 | GET "/bba" => handle_bba 302 | }; 303 | 304 | // 200 aaa 305 | let res = handle_request!(router, GET, "/aaa"); 306 | assert_200!(res, b"aaa"); 307 | 308 | // 200 aab 309 | let res = handle_request!(router, GET, "/aab"); 310 | assert_200!(res, b"aab"); 311 | 312 | // 200 bbb 313 | let res = handle_request!(router, GET, "/bbb"); 314 | assert_200!(res, b"bbb"); 315 | 316 | // 200 bba 317 | let res = handle_request!(router, GET, "/bba"); 318 | assert_200!(res, b"bba"); 319 | } 320 | -------------------------------------------------------------------------------- /submillisecond_macros/src/named_param.rs: -------------------------------------------------------------------------------- 1 | use better_bae::{FromAttributes, TryFromAttributes}; 2 | use proc_macro2::TokenStream; 3 | use quote::quote; 4 | use syn::spanned::Spanned; 5 | use syn::{Data, DeriveInput, Ident, Index, LitStr}; 6 | 7 | #[derive(Debug, Eq, PartialEq, FromAttributes)] 8 | #[bae("param")] 9 | pub struct Attributes { 10 | name: LitStr, 11 | } 12 | 13 | #[derive(Debug)] 14 | pub struct NamedParam { 15 | ident: Ident, 16 | fields: NamedParamFields, 17 | } 18 | 19 | impl NamedParam { 20 | pub fn expand(&self) -> TokenStream { 21 | let NamedParam { ident, fields } = self; 22 | 23 | let content = match fields { 24 | NamedParamFields::Named(named) => { 25 | let names = named.iter().map(|NamedParamField { name, .. }| name); 26 | let tuple_types = named.iter().map(|_| quote! { _ }); 27 | let fields = named 28 | .iter() 29 | .enumerate() 30 | .map(|(i, NamedParamField { ident, .. })| { 31 | let index = Index::from(i); 32 | quote! { 33 | #ident: params.#index 34 | } 35 | }); 36 | 37 | quote! { 38 | let params = &req.params; 39 | let fields = ::std::iter::Iterator::collect::<::std::result::Result<::std::vec::Vec<_>, _>>( 40 | ::std::iter::Iterator::map( 41 | ::std::iter::IntoIterator::into_iter([#( #names ),*]), 42 | |name| { 43 | let value = params 44 | .get(name) 45 | .ok_or_else(<::submillisecond::extract::rejection::MissingPathParams as ::std::default::Default>::default)?; 46 | 47 | let percent_decoded_str = ::submillisecond::extract::path::de::PercentDecodedStr::new(value) 48 | .ok_or_else(|| { 49 | ::submillisecond::extract::rejection::PathRejection::FailedToDeserializePathParams( 50 | ::submillisecond::extract::path::FailedToDeserializePathParams( 51 | ::submillisecond::extract::path::PathDeserializationError::new( 52 | ::submillisecond::extract::path::ErrorKind::InvalidUtf8InPathParam { 53 | key: ::std::string::ToString::to_string(name), 54 | }, 55 | ), 56 | ), 57 | ) 58 | })?; 59 | 60 | ::std::result::Result::<_, ::submillisecond::extract::rejection::PathRejection>::Ok( 61 | (::std::convert::From::from(name), percent_decoded_str) 62 | ) 63 | }, 64 | ), 65 | )?; 66 | 67 | ::serde::de::Deserialize::deserialize( 68 | ::submillisecond::extract::path::de::PathDeserializer::new(fields.as_slice()), 69 | ) 70 | .map_err(|err| { 71 | ::submillisecond::extract::rejection::PathRejection::FailedToDeserializePathParams( 72 | ::submillisecond::extract::path::FailedToDeserializePathParams(err), 73 | ) 74 | }) 75 | .map(|params: (#( #tuple_types ),*)| #ident { 76 | #( #fields ),* 77 | }) 78 | } 79 | } 80 | NamedParamFields::Unnamed(NamedParamField { name, .. }) => { 81 | quote! { 82 | let param_str = req.params 83 | .get(#name) 84 | .ok_or_else(<::submillisecond::extract::rejection::MissingPathParams as ::std::default::Default>::default)?; 85 | 86 | let param = ::submillisecond::extract::path::de::PercentDecodedStr::new(param_str) 87 | .ok_or_else(|| { 88 | ::submillisecond::extract::rejection::PathRejection::FailedToDeserializePathParams( 89 | ::submillisecond::extract::path::FailedToDeserializePathParams( 90 | ::submillisecond::extract::path::PathDeserializationError::new( 91 | ::submillisecond::extract::path::ErrorKind::InvalidUtf8InPathParam { 92 | key: ::std::string::ToString::to_string(#name), 93 | }, 94 | ), 95 | ), 96 | ) 97 | })?; 98 | 99 | ::serde::de::Deserialize::deserialize( 100 | ::submillisecond::extract::path::de::PathDeserializer::new(&[( 101 | ::std::convert::From::from(#name), 102 | param, 103 | )]), 104 | ) 105 | .map_err(|err| { 106 | ::submillisecond::extract::rejection::PathRejection::FailedToDeserializePathParams( 107 | ::submillisecond::extract::path::FailedToDeserializePathParams(err), 108 | ) 109 | }) 110 | .map(Self) 111 | } 112 | } 113 | }; 114 | 115 | quote! { 116 | impl ::submillisecond::extract::FromRequest for #ident { 117 | type Rejection = ::submillisecond::extract::rejection::PathRejection; 118 | 119 | fn from_request( 120 | req: &mut ::submillisecond::RequestContext, 121 | ) -> ::std::result::Result { 122 | #content 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | impl TryFrom for NamedParam { 130 | type Error = syn::Error; 131 | 132 | fn try_from(input: DeriveInput) -> syn::Result { 133 | let span = input.span(); 134 | let fields = match input.data { 135 | Data::Enum(_) => { 136 | return Err(syn::Error::new( 137 | span, 138 | "enum is not supported with NamedParam", 139 | )); 140 | } 141 | Data::Struct(data_struct) => match data_struct.fields { 142 | syn::Fields::Named(fields_named) => { 143 | let attrs = Attributes::try_from_attributes(&input.attrs)?; 144 | if let Some(attrs) = attrs { 145 | return Err(syn::Error::new( 146 | attrs.name.span(), 147 | "Param name can only be applied to unnamed structs with a single value. You might have meant to place it above a field instead?", 148 | )); 149 | } 150 | 151 | let fields = fields_named 152 | .named 153 | .into_iter() 154 | .map(|named| { 155 | let name = Attributes::try_from_attributes(&named.attrs)? 156 | .map(|attrs| attrs.name.value()) 157 | .unwrap_or_else(|| named.ident.clone().unwrap().to_string()); 158 | 159 | syn::Result::Ok(NamedParamField { 160 | name, 161 | ident: named.ident, 162 | }) 163 | }) 164 | .collect::>()?; 165 | 166 | NamedParamFields::Named(fields) 167 | } 168 | syn::Fields::Unnamed(fields_unnamed) => { 169 | let fields_unnamed_span = fields_unnamed.span(); 170 | let mut fields_iter = fields_unnamed.unnamed.into_iter(); 171 | fields_iter.next().ok_or_else(|| { 172 | syn::Error::new(fields_unnamed_span, "expected unnamed field") 173 | })?; 174 | if let Some(field) = fields_iter.next() { 175 | return Err(syn::Error::new( 176 | field.span(), 177 | "only one field can be used with NamedParam", 178 | )); 179 | } 180 | 181 | let name = Attributes::from_attributes(&input.attrs)?.name.value(); 182 | NamedParamFields::Unnamed(NamedParamField { name, ident: None }) 183 | } 184 | syn::Fields::Unit => { 185 | return Err(syn::Error::new( 186 | span, 187 | "unit struct is not supported with NamedParam", 188 | )); 189 | } 190 | }, 191 | Data::Union(_) => { 192 | return Err(syn::Error::new( 193 | span, 194 | "union is not supported with NamedParam", 195 | )); 196 | } 197 | }; 198 | 199 | Ok(NamedParam { 200 | ident: input.ident, 201 | fields, 202 | }) 203 | } 204 | } 205 | 206 | #[derive(Debug)] 207 | enum NamedParamFields { 208 | Named(Vec), 209 | Unnamed(NamedParamField), 210 | } 211 | 212 | #[derive(Debug)] 213 | struct NamedParamField { 214 | name: String, 215 | ident: Option, 216 | } 217 | -------------------------------------------------------------------------------- /benches/router.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | use http::Method; 3 | use lunatic::net::TcpStream; 4 | use submillisecond::{router, Body, Handler, RequestContext}; 5 | 6 | fn handler() {} 7 | 8 | fn router_benchmark_simple(c: &mut Criterion) { 9 | let router = router! { 10 | GET "/simple" => handler 11 | }; 12 | 13 | c.bench_function("simple router", |b| { 14 | let stream = TcpStream::connect("127.0.0.1:22").unwrap(); 15 | 16 | b.iter(|| { 17 | let req = RequestContext::new( 18 | http::Request::builder() 19 | .method(Method::GET) 20 | .uri("/simple") 21 | .body(Body::from_slice(&[])) 22 | .unwrap(), 23 | stream.clone(), 24 | ); 25 | 26 | let res = Handler::handle(&router(), req); 27 | assert!(res.status().is_success()); 28 | }); 29 | }); 30 | } 31 | 32 | fn router_benchmark_nested(c: &mut Criterion) { 33 | let router = router! { 34 | "/a" => { 35 | "/b" => { 36 | "/c" => { 37 | "/d" => { 38 | "/e" => { 39 | "/f" => { 40 | "/g" => { 41 | "/h" => { 42 | "/i" => { 43 | "/j" => { 44 | "/k" => { 45 | "/l" => { 46 | "/m" => { 47 | "/n" => { 48 | "/o" => { 49 | "/p" => { 50 | "/q" => { 51 | "/r" => { 52 | "/s" => { 53 | "/t" => { 54 | "/u" => { 55 | "/v" => { 56 | "/w" => { 57 | "/x" => { 58 | "/y" => { 59 | "/z" => { 60 | GET "/one/two/three/four/five/six/seven/eight/nine/ten" => handler 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | }; 88 | 89 | c.bench_function("nested router", |b| { 90 | let stream = TcpStream::connect("127.0.0.1:22").unwrap(); 91 | 92 | b.iter(|| { 93 | let req = RequestContext::new( 94 | http::Request::builder() 95 | .method(Method::GET) 96 | .uri("/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/one/two/three/four/five/six/seven/eight/nine/ten") 97 | .body(Body::from_slice(&[])) 98 | .unwrap(), 99 | stream.clone(), 100 | ); 101 | 102 | let res = Handler::handle(&router(), req); 103 | assert!(res.status().is_success()); 104 | }); 105 | }); 106 | } 107 | 108 | fn router_benchmark_params(c: &mut Criterion) { 109 | let router = router! { 110 | "/:a" => { 111 | "/:b" => { 112 | "/:c" => { 113 | "/:d" => { 114 | "/:e" => { 115 | "/:f" => { 116 | "/:g" => { 117 | "/:h" => { 118 | "/:i" => { 119 | "/:j" => { 120 | "/:k" => { 121 | "/:l" => { 122 | "/:m" => { 123 | "/:n" => { 124 | "/:o" => { 125 | "/:p" => { 126 | "/:q" => { 127 | "/:r" => { 128 | "/:s" => { 129 | "/:t" => { 130 | "/:u" => { 131 | "/:v" => { 132 | "/:w" => { 133 | "/:x" => { 134 | "/:y" => { 135 | "/:z" => { 136 | GET "/:one/:two/:three/:four/:five/:six/:seven/:eight/:nine/:ten" => handler 137 | } 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | }; 164 | 165 | c.bench_function("params router", |b| { 166 | let stream = TcpStream::connect("127.0.0.1:22").unwrap(); 167 | 168 | b.iter(|| { 169 | let req = RequestContext::new( 170 | http::Request::builder() 171 | .method(Method::GET) 172 | .uri("/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z/one/two/three/four/five/six/seven/eight/nine/ten") 173 | .body(Body::from_slice(&[])) 174 | .unwrap(), 175 | stream.clone(), 176 | ); 177 | 178 | let res = Handler::handle(&router(), req); 179 | assert!(res.status().is_success()); 180 | }); 181 | }); 182 | } 183 | 184 | criterion_group!( 185 | benches, 186 | router_benchmark_simple, 187 | router_benchmark_nested, 188 | router_benchmark_params 189 | ); 190 | criterion_main!(benches); 191 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | # If 1 or more target triples (and optionally, target_features) are specified, 13 | # only the specified targets will be checked when running `cargo deny check`. 14 | # This means, if a particular package is only ever used as a target specific 15 | # dependency, such as, for example, the `nix` crate only being used via the 16 | # `target_family = "unix"` configuration, that only having windows targets in 17 | # this list would mean the nix crate, as well as any of its exclusive 18 | # dependencies not shared by any other crates, would be ignored, as the target 19 | # list here is effectively saying which targets you are building for. 20 | targets = [ 21 | # The triple can be any string, but only the target triples built in to 22 | # rustc (as of 1.40) can be checked against actual config expressions 23 | #{ triple = "x86_64-unknown-linux-musl" }, 24 | # You can also specify which target_features you promise are enabled for a 25 | # particular target. target_features are currently not validated against 26 | # the actual valid features supported by the target architecture. 27 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 28 | ] 29 | 30 | # This section is considered when running `cargo deny check advisories` 31 | # More documentation for the advisories section can be found here: 32 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 33 | [advisories] 34 | # The path where the advisory database is cloned/fetched into 35 | db-path = "~/.cargo/advisory-db" 36 | # The url(s) of the advisory databases to use 37 | db-urls = ["https://github.com/rustsec/advisory-db"] 38 | # The lint level for security vulnerabilities 39 | vulnerability = "deny" 40 | # The lint level for unmaintained crates 41 | unmaintained = "warn" 42 | # The lint level for crates that have been yanked from their source registry 43 | yanked = "warn" 44 | # The lint level for crates with security notices. Note that as of 45 | # 2019-12-17 there are no security notice advisories in 46 | # https://github.com/rustsec/advisory-db 47 | notice = "warn" 48 | # A list of advisory IDs to ignore. Note that ignored advisories will still 49 | # output a note when they are encountered. 50 | ignore = [ 51 | # time is not threadsafe - chrono default-features = false will fix this 52 | "RUSTSEC-2020-0071", 53 | # atty advisory Windows - TBD 54 | "RUSTSEC-2021-0145" 55 | # This is tokio pipeserver related config - lunatic doesn't use this 56 | # Also there is SemVer compatible backport meaning people will get it 57 | # tokio 1.24.0 bump means we don't need to ignore this 58 | #"RUSTSEC-2023-0001" 59 | ] 60 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score 61 | # lower than the range specified will be ignored. Note that ignored advisories 62 | # will still output a note when they are encountered. 63 | # * None - CVSS Score 0.0 64 | # * Low - CVSS Score 0.1 - 3.9 65 | # * Medium - CVSS Score 4.0 - 6.9 66 | # * High - CVSS Score 7.0 - 8.9 67 | # * Critical - CVSS Score 9.0 - 10.0 68 | #severity-threshold = 69 | 70 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 71 | # If this is false, then it uses a built-in git library. 72 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 73 | # See Git Authentication for more information about setting up git authentication. 74 | #git-fetch-with-cli = true 75 | 76 | # This section is considered when running `cargo deny check licenses` 77 | # More documentation for the licenses section can be found here: 78 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 79 | [licenses] 80 | # The lint level for crates which do not have a detectable license 81 | unlicensed = "deny" 82 | # List of explicitly allowed licenses 83 | # See https://spdx.org/licenses/ for list of possible licenses 84 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 85 | allow = [ 86 | "MIT", 87 | "Apache-2.0", 88 | "Apache-2.0 WITH LLVM-exception", 89 | "Unlicense", 90 | "Unicode-DFS-2016", 91 | "MPL-2.0", 92 | "ISC", 93 | "BSD-2-Clause", 94 | "BSD-3-Clause", 95 | # OpenSSL is Apache License v2.0 (ASL v2) 96 | # https://www.openssl.org/blog/blog/2017/03/22/license/ 97 | # ring crate is ISC & MIT 98 | "OpenSSL", 99 | ] 100 | # List of explicitly disallowed licenses 101 | # See https://spdx.org/licenses/ for list of possible licenses 102 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 103 | deny = [ 104 | #"Nokia", 105 | ] 106 | # Lint level for licenses considered copyleft 107 | copyleft = "warn" 108 | # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses 109 | # * both - The license will be approved if it is both OSI-approved *AND* FSF 110 | # * either - The license will be approved if it is either OSI-approved *OR* FSF 111 | # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF 112 | # * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved 113 | # * neither - This predicate is ignored and the default lint level is used 114 | allow-osi-fsf-free = "neither" 115 | # Lint level used when no other predicates are matched 116 | # 1. License isn't in the allow or deny lists 117 | # 2. License isn't copyleft 118 | # 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" 119 | default = "deny" 120 | # The confidence threshold for detecting a license from license text. 121 | # The higher the value, the more closely the license text must be to the 122 | # canonical license text of a valid SPDX license file. 123 | # [possible values: any between 0.0 and 1.0]. 124 | confidence-threshold = 0.8 125 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 126 | # aren't accepted for every possible crate as with the normal allow list 127 | exceptions = [ 128 | ] 129 | 130 | # Some crates don't have (easily) machine readable licensing information, 131 | # adding a clarification entry for it allows you to manually specify the 132 | # licensing information 133 | [[licenses.clarify]] 134 | name = "ring" 135 | expression = "ISC AND MIT AND OpenSSL" 136 | license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] 137 | 138 | [licenses.private] 139 | # If true, ignores workspace crates that aren't published, or are only 140 | # published to private registries. 141 | # To see how to mark a crate as unpublished (to the official registry), 142 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 143 | ignore = false 144 | # One or more private registries that you might publish crates to, if a crate 145 | # is only published to private registries, and ignore is true, the crate will 146 | # not have its license(s) checked 147 | registries = [ 148 | #"https://sekretz.com/registry 149 | ] 150 | 151 | # This section is considered when running `cargo deny check bans`. 152 | # More documentation about the 'bans' section can be found here: 153 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 154 | [bans] 155 | # Lint level for when multiple versions of the same crate are detected 156 | multiple-versions = "warn" 157 | # Lint level for when a crate version requirement is `*` 158 | wildcards = "allow" 159 | # The graph highlighting used when creating dotgraphs for crates 160 | # with multiple versions 161 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 162 | # * simplest-path - The path to the version with the fewest edges is highlighted 163 | # * all - Both lowest-version and simplest-path are used 164 | highlight = "all" 165 | # List of crates that are allowed. Use with care! 166 | allow = [ 167 | #{ name = "ansi_term", version = "=0.11.0" }, 168 | ] 169 | # List of crates to deny 170 | deny = [ 171 | # Each entry the name of a crate and a version range. If version is 172 | # not specified, all versions will be matched. 173 | #{ name = "ansi_term", version = "=0.11.0" }, 174 | # 175 | # Wrapper crates can optionally be specified to allow the crate when it 176 | # is a direct dependency of the otherwise banned crate 177 | #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, 178 | ] 179 | # Certain crates/versions that will be skipped when doing duplicate detection. 180 | skip = [ 181 | #{ name = "ansi_term", version = "=0.11.0" }, 182 | ] 183 | # Similarly to `skip` allows you to skip certain crates during duplicate 184 | # detection. Unlike skip, it also includes the entire tree of transitive 185 | # dependencies starting at the specified crate, up to a certain depth, which is 186 | # by default infinite 187 | skip-tree = [ 188 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, 189 | ] 190 | 191 | # This section is considered when running `cargo deny check sources`. 192 | # More documentation about the 'sources' section can be found here: 193 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 194 | [sources] 195 | # Lint level for what to happen when a crate from a crate registry that is not 196 | # in the allow list is encountered 197 | unknown-registry = "warn" 198 | # Lint level for what to happen when a crate from a git repository that is not 199 | # in the allow list is encountered 200 | unknown-git = "warn" 201 | # List of URLs for allowed crate registries. Defaults to the crates.io index 202 | # if not specified. If it is specified but empty, no registries are allowed. 203 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 204 | # List of URLs for allowed Git repositories 205 | allow-git = [] 206 | 207 | [sources.allow-org] 208 | # 1 or more github.com organizations to allow git sources for 209 | github = [""] 210 | # 1 or more gitlab.com organizations to allow git sources for 211 | gitlab = [""] 212 | # 1 or more bitbucket.org organizations to allow git sources for 213 | bitbucket = [""] 214 | -------------------------------------------------------------------------------- /examples/queue_todo.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, VecDeque}; 2 | use std::fs::{DirBuilder, File}; 3 | use std::io::{self, Write}; 4 | use std::path::{Path, PathBuf}; 5 | use std::str::FromStr; 6 | 7 | use base64::engine::general_purpose; 8 | use base64::Engine as _; 9 | use lunatic::ap::{AbstractProcess, Config, ProcessRef}; 10 | use lunatic::supervisor::{Supervisor, SupervisorConfig}; 11 | use lunatic::{abstract_process, ProcessName}; 12 | use serde::{Deserialize, Serialize}; 13 | use submillisecond::params::Params; 14 | use submillisecond::response::Response; 15 | use submillisecond::{router, Application, Json, RequestContext, Router}; 16 | use uuid::Uuid; 17 | 18 | // ===================================== 19 | // Middleware for requests 20 | // ===================================== 21 | fn logging_middleware(req: RequestContext) -> Response { 22 | let request_id = req 23 | .headers() 24 | .get("x-request-id") 25 | .and_then(|req_id| req_id.to_str().ok()) 26 | .map(|req_id| req_id.to_string()) 27 | .unwrap_or_else(|| "DEFAULT_REQUEST_ID".to_string()); 28 | println!("[ENTER] request {request_id}"); 29 | let res = req.next_handler(); 30 | println!("[EXIT] request {request_id}"); 31 | res 32 | } 33 | 34 | // ===================================== 35 | // Persistence utils 36 | // ===================================== 37 | const NEWLINE: &[u8] = &[b'\n']; 38 | 39 | /// Every Line is a new "state change" entry 40 | /// Each line starts with one of the following keywords 41 | /// that indicate the type of entry 42 | const NEW_USER: u8 = 1; 43 | const PUSH_TODO: u8 = 2; 44 | const POLL_TODO: u8 = 3; 45 | 46 | #[derive(Debug)] 47 | pub struct FileLog { 48 | // cwd: str, 49 | // file_name: str, 50 | full_path: PathBuf, 51 | file: File, 52 | } 53 | 54 | #[derive(Serialize, Deserialize)] 55 | struct PushEntry { 56 | user_uuid: Uuid, 57 | todo: Todo, 58 | } 59 | 60 | impl FileLog { 61 | pub fn new(cwd: &str, file_name: &str) -> FileLog { 62 | DirBuilder::new().recursive(true).create(cwd).unwrap(); 63 | let full_path = Path::new(cwd).join(file_name); 64 | FileLog { 65 | // cwd, 66 | // file_name, 67 | full_path: full_path.clone(), 68 | file: match File::create(&full_path) { 69 | Err(why) => panic!("couldn't open {cwd:?}: {why}"), 70 | // write 0 as initial cursor 71 | Ok(file) => file, 72 | }, 73 | } 74 | } 75 | 76 | pub fn append_new_user(&mut self, user: &User) { 77 | self.append(NEW_USER, ron::to_string(user).unwrap().as_bytes()) 78 | } 79 | 80 | pub fn append_poll_todo(&mut self, user_uuid: Uuid, todo_uuid: Uuid) { 81 | self.append( 82 | POLL_TODO, 83 | ron::to_string(&(user_uuid, todo_uuid)).unwrap().as_bytes(), 84 | ) 85 | } 86 | 87 | pub fn append_push_todo(&mut self, user_uuid: Uuid, todo: Todo) { 88 | self.append( 89 | PUSH_TODO, 90 | ron::to_string(&PushEntry { user_uuid, todo }) 91 | .unwrap() 92 | .as_bytes(), 93 | ) 94 | } 95 | 96 | pub fn append(&mut self, header: u8, data: &[u8]) { 97 | // let x: MyStruct = ron::from_str("(boolean: true, float: 1.23)").unwrap(); 98 | let encoded = general_purpose::STANDARD.encode(data); 99 | let buf = [&[header], encoded.as_bytes(), NEWLINE].concat(); 100 | match self.file.write_all(&buf) { 101 | Err(why) => panic!( 102 | "[FileLog {:?}] couldn't write to file: {}", 103 | self.full_path, why 104 | ), 105 | Ok(_) => println!( 106 | "[FileLog {:?}] Successfully appended log to file", 107 | self.full_path 108 | ), 109 | }; 110 | } 111 | } 112 | 113 | // ===================================== 114 | // Persistence process definition 115 | // ===================================== 116 | pub struct PersistenceSup; 117 | impl Supervisor for PersistenceSup { 118 | type Arg = String; 119 | type Children = (PersistenceProcess,); 120 | 121 | fn init(config: &mut SupervisorConfig, name: Self::Arg) { 122 | // Always register the `PersistenceProcess` under the name passed to the 123 | // supervisor. 124 | config.children_args((((), Some(name)),)) 125 | } 126 | } 127 | 128 | #[derive(ProcessName)] 129 | pub struct PersistenceProcessID; 130 | 131 | pub struct PersistenceProcess { 132 | users: HashMap, 133 | users_nicknames: HashMap, 134 | wal: FileLog, 135 | } 136 | 137 | #[abstract_process(visibility=pub)] 138 | impl PersistenceProcess { 139 | #[init] 140 | fn init(_: Config, _: ()) -> Result { 141 | // Coordinator shouldn't die when a client dies. This makes the link 142 | // one-directional. 143 | unsafe { lunatic::host::api::process::die_when_link_dies(0) }; 144 | Ok(PersistenceProcess { 145 | users: HashMap::new(), 146 | users_nicknames: HashMap::new(), 147 | wal: FileLog::new("/persistence", "todos.wal"), 148 | }) 149 | } 150 | 151 | #[handle_request] 152 | fn add_todo(&mut self, user_id: Uuid, todo: Todo) -> bool { 153 | if let Some(user) = self.users.get_mut(&user_id) { 154 | self.wal.append_push_todo(user.uuid, todo.clone()); 155 | user.todos.push_back(todo); 156 | return true; 157 | } 158 | false 159 | } 160 | 161 | #[handle_request] 162 | fn create_user(&mut self, CreateUserDto { nickname, name }: CreateUserDto) -> Option { 163 | let user_uuid = Uuid::new_v4(); 164 | if self.users_nicknames.get(&nickname).is_some() { 165 | // user already exists 166 | return None; 167 | } 168 | let user = User { 169 | uuid: user_uuid, 170 | nickname: nickname.clone(), 171 | name, 172 | todos: VecDeque::new(), 173 | }; 174 | self.wal.append_new_user(&user); 175 | self.users_nicknames.insert(nickname, user_uuid); 176 | self.users.insert(user_uuid, user); 177 | Some(user_uuid) 178 | } 179 | 180 | #[handle_request] 181 | fn poll_todo(&mut self, user_id: Uuid) -> Option { 182 | if let Some(user) = self.users.get_mut(&user_id) { 183 | if let Some(front) = user.todos.front() { 184 | self.wal.append_poll_todo(user.uuid, front.uuid); 185 | } 186 | return user.todos.pop_front(); 187 | } 188 | None 189 | } 190 | 191 | #[handle_request] 192 | fn peek_todo(&mut self, user_id: Uuid) -> Option { 193 | if let Some(user) = self.users.get_mut(&user_id) { 194 | if let Some(f) = user.todos.front() { 195 | return Some(f.clone()); 196 | } 197 | } 198 | None 199 | } 200 | 201 | #[handle_request] 202 | fn list_todos(&mut self, user_id: Uuid) -> Vec { 203 | // self.todos_wal 204 | // .append_confirmation(message_uuid, pubrel.clone(), SystemTime::now()); 205 | if let Some(user) = self.users.get_mut(&user_id) { 206 | return user.todos.iter().cloned().collect(); 207 | } 208 | vec![] 209 | } 210 | } 211 | 212 | // ===================================== 213 | // DTOs 214 | // ===================================== 215 | #[derive(Serialize, Deserialize, Clone, Debug)] 216 | pub struct Todo { 217 | uuid: Uuid, 218 | title: String, 219 | description: String, 220 | } 221 | 222 | #[derive(Serialize, Deserialize, Clone, Debug)] 223 | pub struct User { 224 | uuid: Uuid, 225 | nickname: String, 226 | name: String, 227 | todos: VecDeque, 228 | } 229 | 230 | #[derive(Serialize, Deserialize, Debug)] 231 | pub struct CreateUserDto { 232 | nickname: String, 233 | name: String, 234 | } 235 | 236 | #[derive(Serialize, Deserialize, Debug)] 237 | struct CreateTodoDto { 238 | title: String, 239 | description: String, 240 | } 241 | 242 | #[derive(Serialize, Deserialize, Debug)] 243 | struct CreateUserResponseDto { 244 | uuid: Uuid, 245 | } 246 | 247 | // routes logic 248 | fn create_user(user: Json) -> Json { 249 | let persistence = ProcessRef::::lookup(&"persistence").unwrap(); 250 | if let Some(uuid) = persistence.create_user(user.0) { 251 | return Json(CreateUserResponseDto { uuid }); 252 | } 253 | panic!("Cannot create user"); 254 | } 255 | 256 | fn list_todos(params: Params) -> Json> { 257 | let persistence = ProcessRef::::lookup(&PersistenceProcessID).unwrap(); 258 | let user_id = params.get("user_id").unwrap(); 259 | let todos = persistence.list_todos(Uuid::from_str(user_id).unwrap()); 260 | Json(todos) 261 | } 262 | 263 | fn poll_todo(params: Params) -> Json { 264 | let persistence = ProcessRef::::lookup(&PersistenceProcessID).unwrap(); 265 | let user_id = params.get("user_id").unwrap(); 266 | if let Some(todo) = persistence.poll_todo(Uuid::from_str(user_id).unwrap()) { 267 | return Json(todo); 268 | } 269 | panic!("Cannot poll todo {params:#?}"); 270 | } 271 | 272 | fn push_todo(params: Params, body: Json) -> Json> { 273 | let persistence = ProcessRef::::lookup(&PersistenceProcessID).unwrap(); 274 | let user_id = params.get("user_id").unwrap(); 275 | println!("RECEIVED BODY {body:?} | {user_id}"); 276 | let todo = Todo { 277 | uuid: Uuid::new_v4(), 278 | title: body.0.title, 279 | description: body.0.description, 280 | }; 281 | if persistence.add_todo(Uuid::from_str(user_id).unwrap(), todo.clone()) { 282 | return Json(Some(todo)); 283 | } 284 | Json(None) 285 | } 286 | 287 | fn liveness_check() -> &'static str { 288 | println!("Running liveness check"); 289 | r#"{"status":"UP"}"# 290 | } 291 | 292 | // has the prefix /api/mgmt 293 | const MGMT_ROUTER: Router = router! { 294 | GET "/alive" => liveness_check 295 | GET "/health" => liveness_check 296 | GET "/metrics" => liveness_check 297 | }; 298 | 299 | const ROUTER: Router = router! { 300 | with logging_middleware; 301 | 302 | "/api/users" => { 303 | POST "/" => create_user 304 | "/:user_id" => { 305 | GET "/todos" => list_todos 306 | POST "/todos" => push_todo 307 | POST "/todos/poll" => poll_todo 308 | } 309 | } 310 | "/api/mgmt" => MGMT_ROUTER() 311 | GET "/something_different/:shoppingcart_id" => liveness_check 312 | }; 313 | 314 | fn main() -> io::Result<()> { 315 | PersistenceSup::link() 316 | .start("persistence".to_owned()) 317 | .unwrap(); 318 | 319 | Application::new(ROUTER).serve("0.0.0.0:3000") 320 | } 321 | --------------------------------------------------------------------------------