From f3ed7cbe53a22f497d4eefa9d55e3262ce0506c5 Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Mon, 28 Sep 2020 22:40:40 -0400 Subject: [PATCH 01/15] Added subdomain feature --- src/lib.rs | 2 ++ src/namespace.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++++++ src/router.rs | 33 ++++++++++++++++++--------- src/server.rs | 30 ++++++++++++++++--------- src/subdomain.rs | 51 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 20 deletions(-) create mode 100644 src/namespace.rs create mode 100644 src/subdomain.rs diff --git a/src/lib.rs b/src/lib.rs index 706d20373..25ab2c28c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -205,11 +205,13 @@ mod cookies; mod endpoint; mod fs; mod middleware; +mod namespace; mod redirect; mod request; mod response; mod response_builder; mod route; +mod subdomain; #[cfg(not(feature = "__internal__bench"))] mod router; diff --git a/src/namespace.rs b/src/namespace.rs new file mode 100644 index 000000000..cfeacbb66 --- /dev/null +++ b/src/namespace.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; + +use crate::{router::Selection, subdomain::Subdomain}; + +pub struct Namespace { + subdomain_map: SubdomainMapper, +} + +// enum SubdomainParameter { +// Param(String), +// String(String), +// } + +struct SubdomainMapper { + pub mapper: HashMap>, +} + +impl SubdomainMapper { + pub fn new() -> Self { + Self { + mapper: HashMap::new(), + } + } +} + +impl Namespace { + pub fn new() -> Self { + Self { + subdomain_map: SubdomainMapper::new(), + } + } + + pub fn add(&mut self, subdomain: String, router: Subdomain) -> &mut Subdomain { + self.subdomain_map + .mapper + .entry(subdomain) + .or_insert_with(|| router) + } + + /// ```rust + /// let app = tide::new(); + /// let sub = app.subdomain("api"); + /// sub.with(logging2); + /// sub.at("/").with(logging).get(Alec) + /// sub.at("/erik").with(logging).post(Erik) + /// ``` + pub fn route( + &self, + domain: &str, + path: &str, + method: http_types::Method, + ) -> Selection<'_, State> { + match self.subdomain_map.mapper.get(domain) { + Some(d) => d.route(path, method), + None => Selection::not_found_endpoint(), + } + } +} diff --git a/src/router.rs b/src/router.rs index b3673ee7d..f5fa3866a 100644 --- a/src/router.rs +++ b/src/router.rs @@ -21,6 +21,25 @@ pub struct Selection<'a, State> { pub(crate) params: Params, } +impl<'a, State> Selection<'a, State> +where + State: Clone + Send + Sync + 'static, +{ + pub fn not_found_endpoint() -> Selection<'a, State> { + Selection { + endpoint: &method_not_allowed, + params: Params::new(), + } + } + + pub fn method_not_allowed() -> Selection<'a, State> { + Selection { + endpoint: ¬_found_endpoint, + params: Params::new(), + } + } +} + impl Router { pub fn new() -> Self { Router { @@ -68,26 +87,20 @@ impl Router { { // If this `path` can be handled by a callback registered with a different HTTP method // should return 405 Method Not Allowed - Selection { - endpoint: &method_not_allowed, - params: Params::new(), - } + Selection::method_not_allowed() } else { - Selection { - endpoint: ¬_found_endpoint, - params: Params::new(), - } + Selection::not_found_endpoint() } } } -async fn not_found_endpoint( +pub async fn not_found_endpoint( _req: Request, ) -> crate::Result { Ok(Response::new(StatusCode::NotFound)) } -async fn method_not_allowed( +pub async fn method_not_allowed( _req: Request, ) -> crate::Result { Ok(Response::new(StatusCode::MethodNotAllowed)) diff --git a/src/server.rs b/src/server.rs index 4bc5b03f9..5adcdc1bb 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,11 +3,14 @@ use async_std::io; use async_std::sync::Arc; -use crate::cookies; -use crate::listener::{Listener, ToListener}; use crate::log; use crate::middleware::{Middleware, Next}; -use crate::router::{Router, Selection}; +use crate::router::Selection; +use crate::{cookies, namespace::Namespace}; +use crate::{ + listener::{Listener, ToListener}, + subdomain::Subdomain, +}; use crate::{Endpoint, Request, Route}; /// An HTTP server. /// @@ -26,7 +29,7 @@ use crate::{Endpoint, Request, Route}; /// add middleware to an app, use the [`Server::middleware`] method. #[allow(missing_debug_implementations)] pub struct Server { - router: Arc>, + router: Arc>, state: State, middleware: Arc>>>, } @@ -93,7 +96,7 @@ impl Server { /// ``` pub fn with_state(state: State) -> Self { let mut server = Self { - router: Arc::new(Router::new()), + router: Arc::new(Namespace::new()), middleware: Arc::new(vec![]), state, }; @@ -103,6 +106,12 @@ impl Server { server } + pub fn subdomain<'a>(&'a mut self, domain: &str) -> &'a mut Subdomain { + let namespace = Arc::get_mut(&mut self.router) + .expect("Registering namespaces is not possible after the server has started"); + Subdomain::new(namespace, domain) + } + /// Add a new route at the given `path`, relative to root. /// /// Routing means mapping an HTTP request to an endpoint. Here Tide applies @@ -150,9 +159,8 @@ impl Server { /// match or not, which means that the order of adding resources has no /// effect. pub fn at<'a>(&'a mut self, path: &str) -> Route<'a, State> { - let router = Arc::get_mut(&mut self.router) - .expect("Registering routes is not possible after the Server has started"); - Route::new(router, path.to_owned()) + let subdomain = self.subdomain(""); + subdomain.at(path) } /// Add middleware to an application. @@ -215,7 +223,8 @@ impl Server { } = self.clone(); let method = req.method().to_owned(); - let Selection { endpoint, params } = router.route(&req.url().path(), method); + let domain = req.host().unwrap_or(""); + let Selection { endpoint, params } = router.route(domain, &req.url().path(), method); let route_params = vec![params]; let req = Request::new(state, req, route_params); @@ -265,13 +274,14 @@ impl { + subdomain: String, + router: Router, + middleware: Vec>>, +} + +impl Subdomain { + pub(crate) fn new<'a>( + namespace: &'a mut Namespace, + subdomain: &str, + ) -> &'a mut Subdomain { + let router = Self { + subdomain: subdomain.to_owned(), + router: Router::new(), + middleware: Vec::new(), + }; + namespace.add(router.subdomain.clone(), router) + } + + pub(crate) fn route(&self, path: &str, method: http_types::Method) -> Selection<'_, State> { + self.router.route(path, method) + } + + pub fn with(&mut self, middleware: M) -> &mut Self + where + M: Middleware, + { + log::trace!( + "Adding middleware {} to subdomain {:?}", + middleware.name(), + self.subdomain + ); + self.middleware.push(Arc::new(middleware)); + self + } + + pub fn at<'b>(&'b mut self, path: &str) -> Route<'b, State> { + Route::new(&mut self.router, path.to_owned()) + } +} From 7abfd8b2a3683127e6c13cb61f32ecab6de7fba3 Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Mon, 28 Sep 2020 22:40:54 -0400 Subject: [PATCH 02/15] fixed surf tests --- tests/server.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/server.rs b/tests/server.rs index 8ffd591bb..835493059 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -25,7 +25,7 @@ fn hello_world() -> Result<(), http_types::Error> { let client = task::spawn(async move { task::sleep(Duration::from_millis(100)).await; let string = surf::get(format!("http://localhost:{}", port)) - .body_string("nori".to_string()) + .body("nori".to_string()) .recv_string() .await .unwrap(); @@ -52,7 +52,7 @@ fn echo_server() -> Result<(), http_types::Error> { let client = task::spawn(async move { task::sleep(Duration::from_millis(100)).await; let string = surf::get(format!("http://localhost:{}", port)) - .body_string("chashu".to_string()) + .body("chashu".to_string()) .recv_string() .await .unwrap(); @@ -86,9 +86,10 @@ fn json() -> Result<(), http_types::Error> { }); let client = task::spawn(async move { + let counter = Counter { count: 0 }; task::sleep(Duration::from_millis(100)).await; let counter: Counter = surf::get(format!("http://localhost:{}", &port)) - .body_json(&Counter { count: 0 })? + .body(serde_json::to_string(&counter)?) .recv_json() .await .unwrap(); From 286a4c765a590d7d14bfa028a3a6b6666d42dd85 Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Tue, 29 Sep 2020 10:15:33 -0400 Subject: [PATCH 03/15] Updated namespace route logic to pass unit tests --- src/namespace.rs | 12 +++++++++++- src/server.rs | 4 +--- tests/server.rs | 1 - 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/namespace.rs b/src/namespace.rs index cfeacbb66..89d690ca5 100644 --- a/src/namespace.rs +++ b/src/namespace.rs @@ -50,7 +50,17 @@ impl Namespace { path: &str, method: http_types::Method, ) -> Selection<'_, State> { - match self.subdomain_map.mapper.get(domain) { + let subdomains = domain.split('.').rev().skip(2).collect::>(); + let domain = if subdomains.len() == 0 { + "".to_owned() + } else { + subdomains + .iter() + .rev() + .fold(String::new(), |sub, part| sub + "." + part)[1..] + .to_owned() + }; + match self.subdomain_map.mapper.get(&domain) { Some(d) => d.route(path, method), None => Selection::not_found_endpoint(), } diff --git a/src/server.rs b/src/server.rs index 3bd05b70c..5adcdc1bb 100644 --- a/src/server.rs +++ b/src/server.rs @@ -227,9 +227,7 @@ impl Server { let Selection { endpoint, params } = router.route(domain, &req.url().path(), method); let route_params = vec![params]; let req = Request::new(state, req, route_params); - for m in middleware.iter() { - println!("middlewares {:?}", m.name()); - } + let next = Next { endpoint, next_middleware: &middleware, diff --git a/tests/server.rs b/tests/server.rs index f18a45c4e..28a4b6923 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -86,7 +86,6 @@ fn json() -> Result<(), http_types::Error> { }); let client = task::spawn(async move { - let counter = Counter { count: 0 }; task::sleep(Duration::from_millis(100)).await; let counter: Counter = surf::get(format!("http://localhost:{}", &port)) .body(Body::from_json(&Counter { count: 0 })?) From a53fda216c6be1318428f75fd8a6e5d535d4963f Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Tue, 29 Sep 2020 15:14:42 -0400 Subject: [PATCH 04/15] Fixed integration tests --- src/router.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/router.rs b/src/router.rs index f5fa3866a..8cf4905ea 100644 --- a/src/router.rs +++ b/src/router.rs @@ -27,14 +27,14 @@ where { pub fn not_found_endpoint() -> Selection<'a, State> { Selection { - endpoint: &method_not_allowed, + endpoint: ¬_found_endpoint, params: Params::new(), } } pub fn method_not_allowed() -> Selection<'a, State> { Selection { - endpoint: ¬_found_endpoint, + endpoint: &method_not_allowed, params: Params::new(), } } From 982e8ad96c567b48290a2f3440081d5f680814c1 Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Wed, 30 Sep 2020 20:59:01 -0400 Subject: [PATCH 05/15] Updated subdomain to allow it to share middleware information --- src/namespace.rs | 158 +++++++++++++++++++++++++++++++++++++++-------- src/server.rs | 24 ++++--- src/subdomain.rs | 6 +- 3 files changed, 150 insertions(+), 38 deletions(-) diff --git a/src/namespace.rs b/src/namespace.rs index 89d690ca5..806103e83 100644 --- a/src/namespace.rs +++ b/src/namespace.rs @@ -1,25 +1,98 @@ -use std::collections::HashMap; +use std::{collections::BTreeMap, collections::HashMap, sync::Arc}; -use crate::{router::Selection, subdomain::Subdomain}; +use crate::{router::Selection, subdomain::Subdomain, Middleware}; -pub struct Namespace { - subdomain_map: SubdomainMapper, +enum Param { + Param(String), + String(String), +} + +struct Holder { + data: T, + map: Vec, +} + +struct Match<'a, T> { + data: &'a T, + params: BTreeMap<&'a String, String>, } -// enum SubdomainParameter { -// Param(String), -// String(String), -// } +impl Holder { + pub fn new(domain: &str, data: T) -> Holder { + let map = domain + .split('.') + .rev() + .map(|p| { + if p.starts_with(":") { + Param::Param(p[1..].to_owned()) + } else { + Param::String(p.to_owned()) + } + }) + .collect(); + Holder { data, map } + } -struct SubdomainMapper { - pub mapper: HashMap>, + pub fn compare(&self, parts: &Vec<&str>) -> Option> { + if self.map.len() != parts.len() { + return None; + } + let mut m = Match { + data: &self.data, + params: BTreeMap::new(), + }; + for (url_part, subdomain_part) in parts.iter().zip(&self.map) { + match subdomain_part { + Param::Param(param_name) => { + m.params.insert(param_name, url_part.to_string()); + } + Param::String(exact_name) => { + if exact_name == "*" { + continue; + } else if url_part != exact_name { + return None; + } + } + } + } + return Some(m); + } + + pub fn data(&mut self) -> &mut T { + &mut self.data + } +} + +pub struct Namespace { + subdomain_map: SubdomainMapper>, } -impl SubdomainMapper { +struct SubdomainMapper { + pub subdomains: HashMap>, +} + +impl SubdomainMapper { pub fn new() -> Self { Self { - mapper: HashMap::new(), + subdomains: HashMap::new(), + } + } + + pub fn add(&mut self, subdomain: &str, element: T) -> &mut T { + self.subdomains + .entry(subdomain.to_owned()) + .or_insert_with(|| Holder::new(subdomain, element)) + .data() + } + + pub fn recognize(&self, domain: &str) -> Option> { + let domain = domain.split('.').rev().collect::>(); + for value in self.subdomains.values() { + if let Some(subdomain) = value.compare(&domain) { + return Some(subdomain); + } } + None } } @@ -31,25 +104,16 @@ impl Namespace { } pub fn add(&mut self, subdomain: String, router: Subdomain) -> &mut Subdomain { - self.subdomain_map - .mapper - .entry(subdomain) - .or_insert_with(|| router) + self.subdomain_map.add(&subdomain, router) } - /// ```rust - /// let app = tide::new(); - /// let sub = app.subdomain("api"); - /// sub.with(logging2); - /// sub.at("/").with(logging).get(Alec) - /// sub.at("/erik").with(logging).post(Erik) - /// ``` pub fn route( &self, domain: &str, path: &str, method: http_types::Method, - ) -> Selection<'_, State> { + global_middleware: &[Arc>], + ) -> NamespaceSelection<'_, State> { let subdomains = domain.split('.').rev().skip(2).collect::>(); let domain = if subdomains.len() == 0 { "".to_owned() @@ -60,9 +124,49 @@ impl Namespace { .fold(String::new(), |sub, part| sub + "." + part)[1..] .to_owned() }; - match self.subdomain_map.mapper.get(&domain) { - Some(d) => d.route(path, method), - None => Selection::not_found_endpoint(), + + match self.subdomain_map.recognize(&domain) { + Some(data) => { + let subdomain = data.data; + let params = data.params; + let selection = subdomain.route(path, method); + let subdomain_middleware = subdomain.middleware().as_slice(); + let global_middleware = global_middleware; + let mut middleware = vec![]; + middleware.extend_from_slice(global_middleware); + middleware.extend_from_slice(subdomain_middleware); + NamespaceSelection { + selection, + middleware, + params, + } + } + None => { + let selection = Selection::not_found_endpoint(); + let mut middleware = vec![]; + middleware.extend_from_slice(global_middleware); + NamespaceSelection { + selection, + middleware, + params: BTreeMap::new(), + } + } + } + } +} + +pub struct NamespaceSelection<'a, State> { + pub(crate) selection: Selection<'a, State>, + pub(crate) middleware: Vec>>, + pub(crate) params: BTreeMap<&'a String, String>, +} + +impl<'a, State> NamespaceSelection<'a, State> { + pub fn subdomain_params(&self) -> route_recognizer::Params { + let mut params = route_recognizer::Params::new(); + for (key, value) in &self.params { + params.insert(key.to_string(), value.to_owned()); } + params } } diff --git a/src/server.rs b/src/server.rs index 5adcdc1bb..eea6b4cc4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -5,7 +5,6 @@ use async_std::sync::Arc; use crate::log; use crate::middleware::{Middleware, Next}; -use crate::router::Selection; use crate::{cookies, namespace::Namespace}; use crate::{ listener::{Listener, ToListener}, @@ -224,13 +223,16 @@ impl Server { let method = req.method().to_owned(); let domain = req.host().unwrap_or(""); - let Selection { endpoint, params } = router.route(domain, &req.url().path(), method); - let route_params = vec![params]; + + let namespace = router.route(domain, &req.url().path(), method, &middleware); + let mut route_params = vec![]; + route_params.push(namespace.subdomain_params()); + route_params.push(namespace.selection.params); let req = Request::new(state, req, route_params); let next = Next { - endpoint, - next_middleware: &middleware, + endpoint: namespace.selection.endpoint, + next_middleware: &namespace.middleware, }; let res = next.run(req).await; @@ -275,19 +277,21 @@ impl Subdomain { namespace.add(router.subdomain.clone(), router) } - pub(crate) fn route(&self, path: &str, method: http_types::Method) -> Selection<'_, State> { + pub(crate) fn route<'a>(&self, path: &str, method: http_types::Method) -> Selection<'_, State> { self.router.route(path, method) } + pub(crate) fn middleware(&self) -> &Vec>> { + &self.middleware + } + pub fn with(&mut self, middleware: M) -> &mut Self where M: Middleware, From 154ef3086696d27a0eb28604edccc875708d3c38 Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Wed, 30 Sep 2020 20:59:13 -0400 Subject: [PATCH 06/15] Added tests for subdomain --- tests/namespaces.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 tests/namespaces.rs diff --git a/tests/namespaces.rs b/tests/namespaces.rs new file mode 100644 index 000000000..935ed54fc --- /dev/null +++ b/tests/namespaces.rs @@ -0,0 +1,151 @@ +use std::{future::Future, pin::Pin}; + +use http::Url; +use tide::http::{self, Method}; + +async fn success(_: tide::Request<()>) -> Result { + let mut res = tide::Response::new(200); + res.set_body("success"); + Ok(res) +} + +async fn echo_path(req: tide::Request<()>) -> Result { + match req.param::("path") { + Ok(path) => Ok(path), + Err(err) => Err(tide::Error::new(tide::StatusCode::BadRequest, err)), + } +} + +async fn multiple_echo_path(req: tide::Request<()>) -> Result { + let err = |err| tide::Error::new(tide::StatusCode::BadRequest, err); + let path = req.param::("path").map_err(err)?; + let user = req.param::("user").map_err(err)?; + Ok(format!("{} {}", path, user)) +} + +fn test_middleware<'a>( + _: tide::Request<()>, + _: tide::Next<'a, ()>, +) -> Pin + Send + 'a>> { + Box::pin(async { + let mut res = tide::Response::new(200); + res.set_body("middleware return"); + Ok(res) + }) +} + +#[async_std::test] +async fn subdomain() { + let mut app = tide::Server::new(); + app.subdomain("api").at("/").get(success); + let url: Url = "http://api.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "success"); +} + +#[async_std::test] +async fn multiple_subdomain() { + let mut app = tide::Server::new(); + app.subdomain("this.for.subdomain.length") + .at("/") + .get(success); + let url: Url = "http://this.for.subdomain.length.example.com/" + .parse() + .unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "success"); +} + +#[async_std::test] +async fn subdomain_with_path_params() { + let mut app = tide::Server::new(); + app.subdomain("api").at("/:path").get(echo_path); + let url: Url = "http://api.example.com/subdomain-work".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "subdomain-work"); +} + +#[async_std::test] +async fn multiple_registered_subdomains() { + let mut app = tide::Server::new(); + app.subdomain("blog").at("/").get(success); + app.subdomain("api").at("/:path").get(echo_path); + + let url: Url = "http://blog.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "success"); + + let url: Url = "http://api.example.com/subdomain-work".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "subdomain-work"); +} + +#[async_std::test] +async fn subdomain_with_middleware() { + let mut app = tide::Server::new(); + app.subdomain("api") + .with(test_middleware) + .at("/") + .get(success); + + let url: Url = "http://api.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "middleware return"); +} + +#[async_std::test] +async fn subdomain_params() { + let mut app = tide::Server::new(); + app.subdomain(":path").at("/").get(echo_path); + let url: Url = "http://example.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "example"); +} + +#[async_std::test] +async fn subdomain_multiple_params() { + let mut app = tide::Server::new(); + app.subdomain(":path.:user").at("/").get(multiple_echo_path); + let url: Url = "http://example.tommy.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "example tommy"); +} + +#[async_std::test] +async fn subdomain_wildcard() { + let mut app = tide::Server::new(); + app.subdomain("*").at("/").get(success); + + let url: Url = "http://example.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "success"); + + let url: Url = "http://user.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "success"); + + let url: Url = "http://example.user.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 404); +} From b579b74994de55d77db6d6b1319503fcc9582289 Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Wed, 30 Sep 2020 21:33:52 -0400 Subject: [PATCH 07/15] moved subdomain routing logic to module --- src/lib.rs | 1 + src/namespace.rs | 104 +++------------------------------ src/subdomain_router/holder.rs | 54 +++++++++++++++++ src/subdomain_router/mod.rs | 14 +++++ src/subdomain_router/router.rs | 32 ++++++++++ 5 files changed, 109 insertions(+), 96 deletions(-) create mode 100644 src/subdomain_router/holder.rs create mode 100644 src/subdomain_router/mod.rs create mode 100644 src/subdomain_router/router.rs diff --git a/src/lib.rs b/src/lib.rs index 25ab2c28c..85e99c46d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -212,6 +212,7 @@ mod response; mod response_builder; mod route; mod subdomain; +mod subdomain_router; #[cfg(not(feature = "__internal__bench"))] mod router; diff --git a/src/namespace.rs b/src/namespace.rs index 806103e83..563b1a054 100644 --- a/src/namespace.rs +++ b/src/namespace.rs @@ -1,110 +1,22 @@ -use std::{collections::BTreeMap, collections::HashMap, sync::Arc}; +use std::{collections::BTreeMap, sync::Arc}; -use crate::{router::Selection, subdomain::Subdomain, Middleware}; - -enum Param { - Param(String), - String(String), -} - -struct Holder { - data: T, - map: Vec, -} - -struct Match<'a, T> { - data: &'a T, - params: BTreeMap<&'a String, String>, -} - -impl Holder { - pub fn new(domain: &str, data: T) -> Holder { - let map = domain - .split('.') - .rev() - .map(|p| { - if p.starts_with(":") { - Param::Param(p[1..].to_owned()) - } else { - Param::String(p.to_owned()) - } - }) - .collect(); - Holder { data, map } - } - - pub fn compare(&self, parts: &Vec<&str>) -> Option> { - if self.map.len() != parts.len() { - return None; - } - let mut m = Match { - data: &self.data, - params: BTreeMap::new(), - }; - for (url_part, subdomain_part) in parts.iter().zip(&self.map) { - match subdomain_part { - Param::Param(param_name) => { - m.params.insert(param_name, url_part.to_string()); - } - Param::String(exact_name) => { - if exact_name == "*" { - continue; - } else if url_part != exact_name { - return None; - } - } - } - } - return Some(m); - } - - pub fn data(&mut self) -> &mut T { - &mut self.data - } -} +use crate::{ + router::Selection, subdomain::Subdomain, subdomain_router::router::SubdomainRouter, Middleware, +}; pub struct Namespace { - subdomain_map: SubdomainMapper>, -} - -struct SubdomainMapper { - pub subdomains: HashMap>, -} - -impl SubdomainMapper { - pub fn new() -> Self { - Self { - subdomains: HashMap::new(), - } - } - - pub fn add(&mut self, subdomain: &str, element: T) -> &mut T { - self.subdomains - .entry(subdomain.to_owned()) - .or_insert_with(|| Holder::new(subdomain, element)) - .data() - } - - pub fn recognize(&self, domain: &str) -> Option> { - let domain = domain.split('.').rev().collect::>(); - for value in self.subdomains.values() { - if let Some(subdomain) = value.compare(&domain) { - return Some(subdomain); - } - } - None - } + router: SubdomainRouter>, } impl Namespace { pub fn new() -> Self { Self { - subdomain_map: SubdomainMapper::new(), + router: SubdomainRouter::new(), } } pub fn add(&mut self, subdomain: String, router: Subdomain) -> &mut Subdomain { - self.subdomain_map.add(&subdomain, router) + self.router.add(&subdomain, router) } pub fn route( @@ -125,7 +37,7 @@ impl Namespace { .to_owned() }; - match self.subdomain_map.recognize(&domain) { + match self.router.recognize(&domain) { Some(data) => { let subdomain = data.data; let params = data.params; diff --git a/src/subdomain_router/holder.rs b/src/subdomain_router/holder.rs new file mode 100644 index 000000000..b9310d2e8 --- /dev/null +++ b/src/subdomain_router/holder.rs @@ -0,0 +1,54 @@ +use std::collections::BTreeMap; + +use super::{Match, SubdomainParams}; + +pub struct Holder { + data: T, + map: Vec, +} + +impl Holder { + pub fn new(domain: &str, data: T) -> Holder { + let map = domain + .split('.') + .rev() + .map(|p| { + if p.starts_with(":") { + SubdomainParams::Param(p[1..].to_owned()) + } else { + SubdomainParams::String(p.to_owned()) + } + }) + .collect(); + Holder { data, map } + } + + pub fn compare(&self, parts: &Vec<&str>) -> Option> { + if self.map.len() != parts.len() { + return None; + } + let mut m = Match { + data: &self.data, + params: BTreeMap::new(), + }; + for (url_part, subdomain_part) in parts.iter().zip(&self.map) { + match subdomain_part { + SubdomainParams::Param(param_name) => { + m.params.insert(param_name, url_part.to_string()); + } + SubdomainParams::String(exact_name) => { + if exact_name == "*" { + continue; + } else if url_part != exact_name { + return None; + } + } + } + } + return Some(m); + } + + pub fn data(&mut self) -> &mut T { + &mut self.data + } +} diff --git a/src/subdomain_router/mod.rs b/src/subdomain_router/mod.rs new file mode 100644 index 000000000..5878d84a3 --- /dev/null +++ b/src/subdomain_router/mod.rs @@ -0,0 +1,14 @@ +use std::collections::BTreeMap; + +pub enum SubdomainParams { + Param(String), + String(String), +} + +pub struct Match<'a, T> { + pub(crate) data: &'a T, + pub(crate) params: BTreeMap<&'a String, String>, +} + +mod holder; +pub mod router; diff --git a/src/subdomain_router/router.rs b/src/subdomain_router/router.rs new file mode 100644 index 000000000..cf27a179b --- /dev/null +++ b/src/subdomain_router/router.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; + +use super::{holder::Holder, Match}; + +pub struct SubdomainRouter { + subdomains: HashMap>, +} + +impl SubdomainRouter { + pub fn new() -> Self { + Self { + subdomains: HashMap::new(), + } + } + + pub fn add(&mut self, subdomain: &str, element: T) -> &mut T { + self.subdomains + .entry(subdomain.to_owned()) + .or_insert_with(|| Holder::new(subdomain, element)) + .data() + } + + pub fn recognize(&self, domain: &str) -> Option> { + let domain = domain.split('.').rev().collect::>(); + for value in self.subdomains.values() { + if let Some(subdomain) = value.compare(&domain) { + return Some(subdomain); + } + } + None + } +} From 8f1bdd064756ae99cde4c469d4f6c9a1afb8c631 Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Wed, 30 Sep 2020 22:50:28 -0400 Subject: [PATCH 08/15] Added comments --- src/namespace.rs | 34 +++++++++++++------------- src/server.rs | 44 ++++++++++++++++++++++++++++++++-- src/subdomain.rs | 20 +++++++++------- src/subdomain_router/holder.rs | 2 ++ src/subdomain_router/router.rs | 1 + 5 files changed, 74 insertions(+), 27 deletions(-) diff --git a/src/namespace.rs b/src/namespace.rs index 563b1a054..426a2e6e8 100644 --- a/src/namespace.rs +++ b/src/namespace.rs @@ -4,10 +4,28 @@ use crate::{ router::Selection, subdomain::Subdomain, subdomain_router::router::SubdomainRouter, Middleware, }; +/// The routing for subdomains used by `Server` pub struct Namespace { router: SubdomainRouter>, } +/// The result of routing a subdomain and a URL +pub struct NamespaceSelection<'a, State> { + pub(crate) selection: Selection<'a, State>, + pub(crate) middleware: Vec>>, + pub(crate) params: BTreeMap<&'a String, String>, +} + +impl<'a, State> NamespaceSelection<'a, State> { + pub fn subdomain_params(&self) -> route_recognizer::Params { + let mut params = route_recognizer::Params::new(); + for (key, value) in &self.params { + params.insert(key.to_string(), value.to_owned()); + } + params + } +} + impl Namespace { pub fn new() -> Self { Self { @@ -66,19 +84,3 @@ impl Namespace { } } } - -pub struct NamespaceSelection<'a, State> { - pub(crate) selection: Selection<'a, State>, - pub(crate) middleware: Vec>>, - pub(crate) params: BTreeMap<&'a String, String>, -} - -impl<'a, State> NamespaceSelection<'a, State> { - pub fn subdomain_params(&self) -> route_recognizer::Params { - let mut params = route_recognizer::Params::new(); - for (key, value) in &self.params { - params.insert(key.to_string(), value.to_owned()); - } - params - } -} diff --git a/src/server.rs b/src/server.rs index eea6b4cc4..d203a797a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -105,10 +105,50 @@ impl Server { server } - pub fn subdomain<'a>(&'a mut self, domain: &str) -> &'a mut Subdomain { + /// Add a new subdomain route given a `subdomain`, relative to the apex domain. + /// + /// Routing subdomains only works if you are listening for an apex domain. + /// Routing works by putting all subdomains into a list and looping over all + /// of them until the correct route has been found. Be sure to place routes + /// that require parameters at the bottom of your routing. After a subdomain + /// has been picked you can use whatever you like. An example of subdomain + /// routing would look like: + /// + /// ```rust,no_run + /// let mut app = tide::Server::new(); + /// app.subdomain("blog").get(|_| async { Ok("Hello blogger")}); + /// ``` + /// + /// A subdomain is comprised of zero or more non-empty string segments that + /// are separated by '.'. Like `Route` there are two kinds of segments: + /// concrete and wildcard. A concrete segment is used to exactly match the + /// respective part of the subdomain of the incoming request. A wildcard + /// segment on the other hand extracts and parses the respective part of the + /// subdomain of the incoming request to pass it along to the endpoint as an + /// argument. A wildcard segment is written as `:user`, which creates an + /// endpoint parameter called `user`. Something to remember is that this + /// parameter feature is also used inside of path routing so if you use a + /// wildcard for your subdomain and path that share the same key name, it + /// will replace the subdomain value with the paths value. + /// + /// Alternatively a wildcard definition can only be a `*`, for example + /// `blog.*`, which means that the wildcard will match any subdomain from + /// the first part. + /// + /// Here are some examples omitting the path routing selection: + /// + /// ```rust,no_run + /// # let mut app = tide::Server::new(); + /// app.subdomain(""); + /// app.subdomain("blog"); + /// app.subdomain(":user.blog"); + /// app.subdomain(":user.*"); + /// app.subdomain(":context.:.api"); + /// ``` + pub fn subdomain<'a>(&'a mut self, subdomain: &str) -> &'a mut Subdomain { let namespace = Arc::get_mut(&mut self.router) .expect("Registering namespaces is not possible after the server has started"); - Subdomain::new(namespace, domain) + Subdomain::new(namespace, subdomain) } /// Add a new route at the given `path`, relative to root. diff --git a/src/subdomain.rs b/src/subdomain.rs index fda87bae3..a405d7a2c 100644 --- a/src/subdomain.rs +++ b/src/subdomain.rs @@ -1,13 +1,13 @@ use crate::{log, namespace::Namespace, router::Router, router::Selection, Middleware, Route}; use std::sync::Arc; -/// Filter route on the subdomain the user registers +/// A handle to a subdomain /// -/// This middleware is a helper function that router uses to give users -/// access to route on subdomains on certain routes. If routes include -/// subdomains that can be parameters then it attaches it to the request -/// object. +/// All routes can be nested inside of a subdomain using [`Server::subdomain`] +/// to establish a subdomain. The `Subdomain` type can be used to establish +/// various `Route`. /// +/// [`Server::subdomain`]: ./struct.Server.html#method.subdomain #[allow(missing_debug_implementations)] pub struct Subdomain { subdomain: String, @@ -36,6 +36,12 @@ impl Subdomain { &self.middleware } + /// Create a route on the given subdomain + pub fn at<'b>(&'b mut self, path: &str) -> Route<'b, State> { + Route::new(&mut self.router, path.to_owned()) + } + + /// Apply the given middleware to the current route pub fn with(&mut self, middleware: M) -> &mut Self where M: Middleware, @@ -48,8 +54,4 @@ impl Subdomain { self.middleware.push(Arc::new(middleware)); self } - - pub fn at<'b>(&'b mut self, path: &str) -> Route<'b, State> { - Route::new(&mut self.router, path.to_owned()) - } } diff --git a/src/subdomain_router/holder.rs b/src/subdomain_router/holder.rs index b9310d2e8..d0fec6cad 100644 --- a/src/subdomain_router/holder.rs +++ b/src/subdomain_router/holder.rs @@ -23,6 +23,8 @@ impl Holder { Holder { data, map } } + /// Compare a subdomain that has been split into parts to the subdomain + /// that the holder implements pub fn compare(&self, parts: &Vec<&str>) -> Option> { if self.map.len() != parts.len() { return None; diff --git a/src/subdomain_router/router.rs b/src/subdomain_router/router.rs index cf27a179b..56de27711 100644 --- a/src/subdomain_router/router.rs +++ b/src/subdomain_router/router.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use super::{holder::Holder, Match}; +/// A router made for routing subdomain strings to a resource pub struct SubdomainRouter { subdomains: HashMap>, } From 574413935d8a596bc188ca2550594101ffe0e5a6 Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Wed, 30 Sep 2020 23:02:47 -0400 Subject: [PATCH 09/15] removed pub from private 404 and 405 methods --- src/router.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/router.rs b/src/router.rs index 8cf4905ea..6daa80730 100644 --- a/src/router.rs +++ b/src/router.rs @@ -94,13 +94,13 @@ impl Router { } } -pub async fn not_found_endpoint( +async fn not_found_endpoint( _req: Request, ) -> crate::Result { Ok(Response::new(StatusCode::NotFound)) } -pub async fn method_not_allowed( +async fn method_not_allowed( _req: Request, ) -> crate::Result { Ok(Response::new(StatusCode::MethodNotAllowed)) From b67ef5c594ddb6676621f2bce03e812ea3b1e05d Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Wed, 30 Sep 2020 23:38:31 -0400 Subject: [PATCH 10/15] Added subdomain example --- examples/subdomain.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 examples/subdomain.rs diff --git a/examples/subdomain.rs b/examples/subdomain.rs new file mode 100644 index 000000000..faf9a168d --- /dev/null +++ b/examples/subdomain.rs @@ -0,0 +1,26 @@ +#[async_std::main] +async fn main() -> Result<(), std::io::Error> { + tide::log::start(); + let mut app = tide::new(); + app.subdomain(":user") + .at("/") + .get(|req: tide::Request<()>| async move { + let user = req.param::("user").unwrap(); + Ok(format!("Welcome user {}", user)) + }); + app.at("/") + .get(|_| async { Ok("Welcome to my landing page") }); + app.subdomain("blog") + .at("/") + .get(|_| async { Ok("Welcome to my blog") }); + + // to be able to use this example, please note some domains down inside of + // your /etc/hosts file. Add the following: + // 127.0.0.1 example.local + // 127.0.0.1 blog.example.local + // 127.0.0.1 tom.example.local + + // After add the following urls. Test it inside of your browser. + app.listen("http://example.local:8080").await?; + Ok(()) +} From d93b367068d176407ab4e23bdc8d37e921b56c4c Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Fri, 9 Oct 2020 13:42:41 -0400 Subject: [PATCH 11/15] Added more testing --- examples/subdomain.rs | 15 ++++--- src/subdomain_router/holder.rs | 25 +++++++---- src/subdomain_router/router.rs | 11 +++-- tests/namespaces.rs | 82 ++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 20 deletions(-) diff --git a/examples/subdomain.rs b/examples/subdomain.rs index faf9a168d..3ef85a5de 100644 --- a/examples/subdomain.rs +++ b/examples/subdomain.rs @@ -2,17 +2,17 @@ async fn main() -> Result<(), std::io::Error> { tide::log::start(); let mut app = tide::new(); + app.at("/") + .get(|_| async { Ok("Welcome to my landing page") }); + app.subdomain("blog") + .at("/") + .get(|_| async { Ok("Welcome to my blog") }); app.subdomain(":user") .at("/") .get(|req: tide::Request<()>| async move { let user = req.param::("user").unwrap(); Ok(format!("Welcome user {}", user)) }); - app.at("/") - .get(|_| async { Ok("Welcome to my landing page") }); - app.subdomain("blog") - .at("/") - .get(|_| async { Ok("Welcome to my blog") }); // to be able to use this example, please note some domains down inside of // your /etc/hosts file. Add the following: @@ -20,7 +20,10 @@ async fn main() -> Result<(), std::io::Error> { // 127.0.0.1 blog.example.local // 127.0.0.1 tom.example.local - // After add the following urls. Test it inside of your browser. + // After adding the urls. Test it inside of your browser. Try: + // - example.local:8080 + // - blog.example.local:8080 + // - tom.example.local:8080 app.listen("http://example.local:8080").await?; Ok(()) } diff --git a/src/subdomain_router/holder.rs b/src/subdomain_router/holder.rs index d0fec6cad..6c69cd928 100644 --- a/src/subdomain_router/holder.rs +++ b/src/subdomain_router/holder.rs @@ -34,15 +34,24 @@ impl Holder { params: BTreeMap::new(), }; for (url_part, subdomain_part) in parts.iter().zip(&self.map) { - match subdomain_part { - SubdomainParams::Param(param_name) => { - m.params.insert(param_name, url_part.to_string()); + // this check is for checking the apex domain which has a parts of [""] + if *url_part == "" { + match subdomain_part { + SubdomainParams::Param(_) => return None, + SubdomainParams::String(_) => continue, } - SubdomainParams::String(exact_name) => { - if exact_name == "*" { - continue; - } else if url_part != exact_name { - return None; + // everything else will run into this else block + } else { + match subdomain_part { + SubdomainParams::Param(param_name) => { + m.params.insert(param_name, url_part.to_string()); + } + SubdomainParams::String(exact_name) => { + if exact_name == "*" { + continue; + } else if url_part != exact_name { + return None; + } } } } diff --git a/src/subdomain_router/router.rs b/src/subdomain_router/router.rs index 56de27711..b7436279f 100644 --- a/src/subdomain_router/router.rs +++ b/src/subdomain_router/router.rs @@ -1,29 +1,28 @@ -use std::collections::HashMap; - use super::{holder::Holder, Match}; +use std::collections::BTreeMap; /// A router made for routing subdomain strings to a resource pub struct SubdomainRouter { - subdomains: HashMap>, + subdomains: BTreeMap<(usize, String), Holder>, } impl SubdomainRouter { pub fn new() -> Self { Self { - subdomains: HashMap::new(), + subdomains: BTreeMap::new(), } } pub fn add(&mut self, subdomain: &str, element: T) -> &mut T { self.subdomains - .entry(subdomain.to_owned()) + .entry((self.subdomains.len(), subdomain.to_owned())) .or_insert_with(|| Holder::new(subdomain, element)) .data() } pub fn recognize(&self, domain: &str) -> Option> { let domain = domain.split('.').rev().collect::>(); - for value in self.subdomains.values() { + for (_, value) in &self.subdomains { if let Some(subdomain) = value.compare(&domain) { return Some(subdomain); } diff --git a/tests/namespaces.rs b/tests/namespaces.rs index 935ed54fc..f7b340e27 100644 --- a/tests/namespaces.rs +++ b/tests/namespaces.rs @@ -149,3 +149,85 @@ async fn subdomain_wildcard() { let response: http::Response = app.respond(request).await.unwrap(); assert_eq!(response.status(), 404); } + +#[async_std::test] +async fn subdomain_routing() { + let mut app = tide::Server::new(); + // setup + app.at("/").get(|_| async { Ok("landing page") }); + app.subdomain("blog") + .at("/") + .get(|_| async { Ok("my blog") }); + app.subdomain(":user") + .at("/") + .get(|req: tide::Request<()>| async move { + let user = req.param::("user").unwrap(); + Ok(format!("user {}", user)) + }); + + // testing + let url: Url = "http://example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "landing page"); + + let url: Url = "http://blog.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "my blog"); + + let url: Url = "http://tom.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "user tom"); + + let url: Url = "http://user.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "user user"); +} + +#[async_std::test] +async fn subdomain_routing_wildcard() { + let mut app = tide::Server::new(); + // setup + app.subdomain(":user") + .at("/") + .get(|req: tide::Request<()>| async move { + let user = req.param::("user").unwrap(); + Ok(format!("user {}", user)) + }); + app.at("/").get(|_| async { Ok("landing page") }); + app.subdomain("blog") + .at("/") + .get(|_| async { Ok("my blog") }); + + // testing + let url: Url = "http://example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "landing page"); + + let url: Url = "http://blog.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "user blog"); + + let url: Url = "http://tom.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "user tom"); + + let url: Url = "http://user.example.com/".parse().unwrap(); + let request = http::Request::new(Method::Get, url); + let mut response: http::Response = app.respond(request).await.unwrap(); + assert_eq!(response.status(), 200); + assert_eq!(response.body_string().await.unwrap(), "user user"); +} From 45a09b24b797afdbf7d29ac1deaf214e177671d1 Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Fri, 9 Oct 2020 16:31:04 -0400 Subject: [PATCH 12/15] updated subdomain routing to deal with static routes first --- src/server.rs | 5 +++-- src/subdomain_router/holder.rs | 12 +++++++++++- src/subdomain_router/mod.rs | 5 +++++ src/subdomain_router/router.rs | 22 ++++++++++++++++------ tests/namespaces.rs | 2 +- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/server.rs b/src/server.rs index d203a797a..bf85625df 100644 --- a/src/server.rs +++ b/src/server.rs @@ -116,7 +116,7 @@ impl Server { /// /// ```rust,no_run /// let mut app = tide::Server::new(); - /// app.subdomain("blog").get(|_| async { Ok("Hello blogger")}); + /// app.subdomain("blog").at("/").get(|_| async { Ok("Hello blogger")}); /// ``` /// /// A subdomain is comprised of zero or more non-empty string segments that @@ -261,10 +261,11 @@ impl Server { middleware, } = self.clone(); + let path = req.url().path(); let method = req.method().to_owned(); let domain = req.host().unwrap_or(""); - let namespace = router.route(domain, &req.url().path(), method, &middleware); + let namespace = router.route(domain, &path, method, &middleware); let mut route_params = vec![]; route_params.push(namespace.subdomain_params()); route_params.push(namespace.selection.params); diff --git a/src/subdomain_router/holder.rs b/src/subdomain_router/holder.rs index 6c69cd928..65525e79f 100644 --- a/src/subdomain_router/holder.rs +++ b/src/subdomain_router/holder.rs @@ -1,12 +1,22 @@ use std::collections::BTreeMap; -use super::{Match, SubdomainParams}; +use super::{Match, SubdomainParams, SubdomainType}; pub struct Holder { data: T, map: Vec, } +pub fn domain_type(subdomain: &str) -> SubdomainType { + let parts = subdomain.split('.').rev(); + for part in parts { + if part.starts_with(":") { + return SubdomainType::Parametrized; + } + } + SubdomainType::Static +} + impl Holder { pub fn new(domain: &str, data: T) -> Holder { let map = domain diff --git a/src/subdomain_router/mod.rs b/src/subdomain_router/mod.rs index 5878d84a3..003348118 100644 --- a/src/subdomain_router/mod.rs +++ b/src/subdomain_router/mod.rs @@ -5,6 +5,11 @@ pub enum SubdomainParams { String(String), } +pub enum SubdomainType { + Parametrized, + Static, +} + pub struct Match<'a, T> { pub(crate) data: &'a T, pub(crate) params: BTreeMap<&'a String, String>, diff --git a/src/subdomain_router/router.rs b/src/subdomain_router/router.rs index b7436279f..19497c92e 100644 --- a/src/subdomain_router/router.rs +++ b/src/subdomain_router/router.rs @@ -1,28 +1,38 @@ -use super::{holder::Holder, Match}; +use super::{holder::domain_type, holder::Holder, Match, SubdomainType}; use std::collections::BTreeMap; /// A router made for routing subdomain strings to a resource pub struct SubdomainRouter { - subdomains: BTreeMap<(usize, String), Holder>, + static_subdomains: BTreeMap>, + parameterized_subdomains: BTreeMap>, } impl SubdomainRouter { pub fn new() -> Self { Self { - subdomains: BTreeMap::new(), + static_subdomains: BTreeMap::new(), + parameterized_subdomains: BTreeMap::new(), } } pub fn add(&mut self, subdomain: &str, element: T) -> &mut T { - self.subdomains - .entry((self.subdomains.len(), subdomain.to_owned())) + let list = match domain_type(subdomain) { + SubdomainType::Static => &mut self.static_subdomains, + SubdomainType::Parametrized => &mut self.parameterized_subdomains, + }; + list.entry(subdomain.to_owned()) .or_insert_with(|| Holder::new(subdomain, element)) .data() } pub fn recognize(&self, domain: &str) -> Option> { let domain = domain.split('.').rev().collect::>(); - for (_, value) in &self.subdomains { + for (_, value) in &self.static_subdomains { + if let Some(subdomain) = value.compare(&domain) { + return Some(subdomain); + } + } + for (_, value) in &self.parameterized_subdomains { if let Some(subdomain) = value.compare(&domain) { return Some(subdomain); } diff --git a/tests/namespaces.rs b/tests/namespaces.rs index f7b340e27..d15e48399 100644 --- a/tests/namespaces.rs +++ b/tests/namespaces.rs @@ -217,7 +217,7 @@ async fn subdomain_routing_wildcard() { let request = http::Request::new(Method::Get, url); let mut response: http::Response = app.respond(request).await.unwrap(); assert_eq!(response.status(), 200); - assert_eq!(response.body_string().await.unwrap(), "user blog"); + assert_eq!(response.body_string().await.unwrap(), "my blog"); let url: Url = "http://tom.example.com/".parse().unwrap(); let request = http::Request::new(Method::Get, url); From ceb11e8efec8fa538f2032c51f1ed119a3670add Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Fri, 9 Oct 2020 17:12:35 -0400 Subject: [PATCH 13/15] Subdomain checks static routes first, then dynamic ones --- src/subdomain_router/holder.rs | 37 +++++-------------- src/subdomain_router/mod.rs | 65 +++++++++++++++++++++++++++++----- src/subdomain_router/router.rs | 23 ++++-------- 3 files changed, 73 insertions(+), 52 deletions(-) diff --git a/src/subdomain_router/holder.rs b/src/subdomain_router/holder.rs index 65525e79f..d0fec6cad 100644 --- a/src/subdomain_router/holder.rs +++ b/src/subdomain_router/holder.rs @@ -1,22 +1,12 @@ use std::collections::BTreeMap; -use super::{Match, SubdomainParams, SubdomainType}; +use super::{Match, SubdomainParams}; pub struct Holder { data: T, map: Vec, } -pub fn domain_type(subdomain: &str) -> SubdomainType { - let parts = subdomain.split('.').rev(); - for part in parts { - if part.starts_with(":") { - return SubdomainType::Parametrized; - } - } - SubdomainType::Static -} - impl Holder { pub fn new(domain: &str, data: T) -> Holder { let map = domain @@ -44,24 +34,15 @@ impl Holder { params: BTreeMap::new(), }; for (url_part, subdomain_part) in parts.iter().zip(&self.map) { - // this check is for checking the apex domain which has a parts of [""] - if *url_part == "" { - match subdomain_part { - SubdomainParams::Param(_) => return None, - SubdomainParams::String(_) => continue, + match subdomain_part { + SubdomainParams::Param(param_name) => { + m.params.insert(param_name, url_part.to_string()); } - // everything else will run into this else block - } else { - match subdomain_part { - SubdomainParams::Param(param_name) => { - m.params.insert(param_name, url_part.to_string()); - } - SubdomainParams::String(exact_name) => { - if exact_name == "*" { - continue; - } else if url_part != exact_name { - return None; - } + SubdomainParams::String(exact_name) => { + if exact_name == "*" { + continue; + } else if url_part != exact_name { + return None; } } } diff --git a/src/subdomain_router/mod.rs b/src/subdomain_router/mod.rs index 003348118..45eaf303c 100644 --- a/src/subdomain_router/mod.rs +++ b/src/subdomain_router/mod.rs @@ -1,19 +1,68 @@ -use std::collections::BTreeMap; +use std::{cmp::Ordering, collections::BTreeMap}; + +mod holder; +pub mod router; pub enum SubdomainParams { Param(String), String(String), } -pub enum SubdomainType { - Parametrized, - Static, -} - pub struct Match<'a, T> { pub(crate) data: &'a T, pub(crate) params: BTreeMap<&'a String, String>, } -mod holder; -pub mod router; +#[derive(Eq)] +pub enum SubdomainType { + Static(String), + Parametrized(String), +} + +impl SubdomainType { + pub fn new(subdomain: &str) -> SubdomainType { + let parts = subdomain.split('.').rev(); + for part in parts { + if part.starts_with(":") { + return SubdomainType::Parametrized(subdomain.to_owned()); + } + } + SubdomainType::Static(subdomain.to_owned()) + } +} + +impl Ord for SubdomainType { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self { + SubdomainType::Static(me) => match other { + SubdomainType::Static(you) => me.cmp(you), + SubdomainType::Parametrized(_) => Ordering::Less, + }, + SubdomainType::Parametrized(me) => match other { + SubdomainType::Static(_) => Ordering::Greater, + SubdomainType::Parametrized(you) => me.cmp(you), + }, + } + } +} + +impl PartialOrd for SubdomainType { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for SubdomainType { + fn eq(&self, other: &Self) -> bool { + match self { + SubdomainType::Static(me) => match other { + SubdomainType::Static(you) => me == you, + SubdomainType::Parametrized(_) => false, + }, + SubdomainType::Parametrized(me) => match other { + SubdomainType::Static(_) => false, + SubdomainType::Parametrized(you) => me == you, + }, + } + } +} diff --git a/src/subdomain_router/router.rs b/src/subdomain_router/router.rs index 19497c92e..1ff078aa8 100644 --- a/src/subdomain_router/router.rs +++ b/src/subdomain_router/router.rs @@ -1,38 +1,29 @@ -use super::{holder::domain_type, holder::Holder, Match, SubdomainType}; +use super::{holder::Holder, Match, SubdomainType}; use std::collections::BTreeMap; /// A router made for routing subdomain strings to a resource pub struct SubdomainRouter { - static_subdomains: BTreeMap>, - parameterized_subdomains: BTreeMap>, + subdomains: BTreeMap>, } impl SubdomainRouter { pub fn new() -> Self { Self { - static_subdomains: BTreeMap::new(), - parameterized_subdomains: BTreeMap::new(), + subdomains: BTreeMap::new(), } } pub fn add(&mut self, subdomain: &str, element: T) -> &mut T { - let list = match domain_type(subdomain) { - SubdomainType::Static => &mut self.static_subdomains, - SubdomainType::Parametrized => &mut self.parameterized_subdomains, - }; - list.entry(subdomain.to_owned()) + let subdomain_type = SubdomainType::new(subdomain); + self.subdomains + .entry(subdomain_type) .or_insert_with(|| Holder::new(subdomain, element)) .data() } pub fn recognize(&self, domain: &str) -> Option> { let domain = domain.split('.').rev().collect::>(); - for (_, value) in &self.static_subdomains { - if let Some(subdomain) = value.compare(&domain) { - return Some(subdomain); - } - } - for (_, value) in &self.parameterized_subdomains { + for (_, value) in &self.subdomains { if let Some(subdomain) = value.compare(&domain) { return Some(subdomain); } From 0152dd6bc0e1fa8c71fc16a64bf678ec8f2fc38b Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Fri, 9 Oct 2020 17:26:48 -0400 Subject: [PATCH 14/15] Fixed subdomain example --- examples/subdomain.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/subdomain.rs b/examples/subdomain.rs index 3ef85a5de..c49f054a9 100644 --- a/examples/subdomain.rs +++ b/examples/subdomain.rs @@ -10,7 +10,7 @@ async fn main() -> Result<(), std::io::Error> { app.subdomain(":user") .at("/") .get(|req: tide::Request<()>| async move { - let user = req.param::("user").unwrap(); + let user = req.param("user").unwrap(); Ok(format!("Welcome user {}", user)) }); From ce236a965446b3dbb88c4e8f716ecee401708321 Mon Sep 17 00:00:00 2001 From: Alec Di Vito Date: Fri, 9 Oct 2020 17:35:58 -0400 Subject: [PATCH 15/15] Updated Subdomain tests --- tests/namespaces.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/namespaces.rs b/tests/namespaces.rs index d15e48399..ea1a1275e 100644 --- a/tests/namespaces.rs +++ b/tests/namespaces.rs @@ -10,16 +10,16 @@ async fn success(_: tide::Request<()>) -> Result { } async fn echo_path(req: tide::Request<()>) -> Result { - match req.param::("path") { - Ok(path) => Ok(path), - Err(err) => Err(tide::Error::new(tide::StatusCode::BadRequest, err)), + match req.param("path") { + Ok(path) => Ok(path.to_owned()), + Err(err) => Err(tide::Error::from_str(tide::StatusCode::BadRequest, err)), } } async fn multiple_echo_path(req: tide::Request<()>) -> Result { - let err = |err| tide::Error::new(tide::StatusCode::BadRequest, err); - let path = req.param::("path").map_err(err)?; - let user = req.param::("user").map_err(err)?; + let err = |err| tide::Error::from_str(tide::StatusCode::BadRequest, err); + let path = req.param("path").map_err(err)?; + let user = req.param("user").map_err(err)?; Ok(format!("{} {}", path, user)) } @@ -161,7 +161,7 @@ async fn subdomain_routing() { app.subdomain(":user") .at("/") .get(|req: tide::Request<()>| async move { - let user = req.param::("user").unwrap(); + let user = req.param("user").unwrap(); Ok(format!("user {}", user)) }); @@ -198,7 +198,7 @@ async fn subdomain_routing_wildcard() { app.subdomain(":user") .at("/") .get(|req: tide::Request<()>| async move { - let user = req.param::("user").unwrap(); + let user = req.param("user").unwrap(); Ok(format!("user {}", user)) }); app.at("/").get(|_| async { Ok("landing page") });