Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subdomain routing #720

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions examples/subdomain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#[async_std::main]
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))
});

// 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 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(())
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,14 @@ mod cookies;
mod endpoint;
mod fs;
mod middleware;
mod namespace;
mod redirect;
mod request;
mod response;
mod response_builder;
mod route;
mod subdomain;
mod subdomain_router;

#[cfg(not(feature = "__internal__bench"))]
mod router;
Expand Down
86 changes: 86 additions & 0 deletions src/namespace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use std::{collections::BTreeMap, sync::Arc};

use crate::{
router::Selection, subdomain::Subdomain, subdomain_router::router::SubdomainRouter, Middleware,
};

/// The routing for subdomains used by `Server`
pub struct Namespace<State> {
router: SubdomainRouter<Subdomain<State>>,
}

/// The result of routing a subdomain and a URL
pub struct NamespaceSelection<'a, State> {
pub(crate) selection: Selection<'a, State>,
pub(crate) middleware: Vec<Arc<dyn Middleware<State>>>,
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<State: Clone + Send + Sync + 'static> Namespace<State> {
pub fn new() -> Self {
Self {
router: SubdomainRouter::new(),
}
}

pub fn add(&mut self, subdomain: String, router: Subdomain<State>) -> &mut Subdomain<State> {
self.router.add(&subdomain, router)
}

pub fn route(
&self,
domain: &str,
path: &str,
method: http_types::Method,
global_middleware: &[Arc<dyn Middleware<State>>],
) -> NamespaceSelection<'_, State> {
let subdomains = domain.split('.').rev().skip(2).collect::<Vec<&str>>();
let domain = if subdomains.len() == 0 {
"".to_owned()
} else {
subdomains
.iter()
.rev()
.fold(String::new(), |sub, part| sub + "." + part)[1..]
.to_owned()
};

match self.router.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(),
}
}
}
}
}
29 changes: 21 additions & 8 deletions src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: &not_found_endpoint,
params: Params::new(),
}
}

pub fn method_not_allowed() -> Selection<'a, State> {
Selection {
endpoint: &method_not_allowed,
params: Params::new(),
}
}
}

impl<State: Clone + Send + Sync + 'static> Router<State> {
pub fn new() -> Self {
Router {
Expand Down Expand Up @@ -68,15 +87,9 @@ impl<State: Clone + Send + Sync + 'static> Router<State> {
{
// 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: &not_found_endpoint,
params: Params::new(),
}
Selection::not_found_endpoint()
}
}
}
Expand Down
89 changes: 72 additions & 17 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
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::{cookies, namespace::Namespace};
use crate::{
listener::{Listener, ToListener},
subdomain::Subdomain,
};
use crate::{Endpoint, Request, Route};

/// An HTTP server.
Expand All @@ -26,7 +28,7 @@ use crate::{Endpoint, Request, Route};
/// response processing, such as compression, default headers, or logging. To
/// add middleware to an app, use the [`Server::middleware`] method.
pub struct Server<State> {
router: Arc<Router<State>>,
router: Arc<Namespace<State>>,
state: State,
/// Holds the middleware stack.
///
Expand Down Expand Up @@ -101,7 +103,7 @@ impl<State: Clone + Send + Sync + 'static> Server<State> {
/// ```
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,
};
Expand All @@ -111,6 +113,52 @@ impl<State: Clone + Send + Sync + 'static> Server<State> {
server
}

/// 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").at("/").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<State> {
let namespace = Arc::get_mut(&mut self.router)
.expect("Registering namespaces is not possible after the server has started");
Subdomain::new(namespace, subdomain)
}

/// Add a new route at the given `path`, relative to root.
///
/// Routing means mapping an HTTP request to an endpoint. Here Tide applies
Expand Down Expand Up @@ -158,9 +206,8 @@ impl<State: Clone + Send + Sync + 'static> Server<State> {
/// 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.
Expand Down Expand Up @@ -222,14 +269,19 @@ impl<State: Clone + Send + Sync + 'static> Server<State> {
middleware,
} = self.clone();

let path = req.url().path();
let method = req.method().to_owned();
let Selection { endpoint, params } = router.route(&req.url().path(), method);
let route_params = vec![params];
let domain = req.host().unwrap_or("");

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);
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;
Expand Down Expand Up @@ -279,19 +331,22 @@ impl<State: Clone + Sync + Send + 'static, InnerState: Clone + Sync + Send + 'st
mut route_params,
..
} = req;
let path = req.url().path().to_owned();
let domain = req.host().unwrap_or("");
let path = req.url().path();
let method = req.method().to_owned();

let router = self.router.clone();
let middleware = self.middleware.clone();
let state = self.state.clone();

let Selection { endpoint, params } = router.route(&path, method);
route_params.push(params);
let namespace = router.route(domain, path, method, &middleware);
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,
};

Ok(next.run(req).await)
Expand Down
57 changes: 57 additions & 0 deletions src/subdomain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::{log, namespace::Namespace, router::Router, router::Selection, Middleware, Route};
use std::sync::Arc;

/// A handle to a subdomain
///
/// 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<State> {
subdomain: String,
router: Router<State>,
middleware: Vec<Arc<dyn Middleware<State>>>,
}

impl<State: Clone + Send + Sync + 'static> Subdomain<State> {
pub(crate) fn new<'a>(
namespace: &'a mut Namespace<State>,
subdomain: &str,
) -> &'a mut Subdomain<State> {
let router = Self {
subdomain: subdomain.to_owned(),
router: Router::new(),
middleware: Vec::new(),
};
namespace.add(router.subdomain.clone(), router)
}

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<Arc<dyn Middleware<State>>> {
&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<M>(&mut self, middleware: M) -> &mut Self
where
M: Middleware<State>,
{
log::trace!(
"Adding middleware {} to subdomain {:?}",
middleware.name(),
self.subdomain
);
self.middleware.push(Arc::new(middleware));
self
}
}
Loading