From 26e0fd2702a9f023823490fc957e34e0064b363a Mon Sep 17 00:00:00 2001 From: Emi Tatsuo Date: Thu, 19 Nov 2020 21:37:02 -0500 Subject: [PATCH] Added some routing classes --- Cargo.toml | 1 + src/lib.rs | 6 ++- src/routing/mod.rs | 3 ++ src/routing/node.rs | 124 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/routing/mod.rs create mode 100644 src/routing/node.rs diff --git a/Cargo.toml b/Cargo.toml index 9ad991d..b1e09a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ documentation = "https://docs.rs/northstar" [features] default = ["serve_dir"] +routing = [] serve_dir = ["mime_guess", "tokio/fs"] [dependencies] diff --git a/src/lib.rs b/src/lib.rs index b8e00d6..2279027 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,8 @@ use crate::util::opt_timeout; pub mod types; pub mod util; +#[cfg(feature="routing")] +pub mod routing; pub use mime; pub use uriparse as uri; @@ -33,8 +35,8 @@ pub use types::*; pub const REQUEST_URI_MAX_LEN: usize = 1024; pub const GEMINI_PORT: u16 = 1965; -type Handler = Arc HandlerResponse + Send + Sync>; -pub (crate) type HandlerResponse = BoxFuture<'static, Result>; +pub type Handler = Arc HandlerResponse + Send + Sync>; +pub type HandlerResponse = BoxFuture<'static, Result>; #[derive(Clone)] pub struct Server { diff --git a/src/routing/mod.rs b/src/routing/mod.rs new file mode 100644 index 0000000..5a41d23 --- /dev/null +++ b/src/routing/mod.rs @@ -0,0 +1,3 @@ +//! Tools for adding routes to a [`Server`](crate::Server) +mod node; +pub use node::*; diff --git a/src/routing/node.rs b/src/routing/node.rs new file mode 100644 index 0000000..b8bce21 --- /dev/null +++ b/src/routing/node.rs @@ -0,0 +1,124 @@ +use uriparse::path::Path; + +use std::collections::HashMap; +use std::convert::TryInto; + +use crate::Handler; +use crate::types::Request; + +#[derive(Default)] +/// A node for routing requests +/// +/// Routing is processed by a tree, with each child being a single path segment. For +/// example, if a handler existed at "/trans/rights", then the root-level node would have +/// a child "trans", which would have a child "rights". "rights" would have no children, +/// but would have an attached handler. +/// +/// If one route is shorter than another, say "/trans/rights" and +/// "/trans/rights/r/human", then the longer route always matches first, so a request for +/// "/trans/rights/r/human/rights" would be routed to "/trans/rights/r/human", and +/// "/trans/rights/now" would route to "/trans/rights" +pub struct RoutingNode(Option, HashMap); + +impl RoutingNode { + /// Attempt to identify a handler based on path segments + /// + /// This searches the network of routing nodes attempting to match a specific request, + /// represented as a sequence of path segments. For example, "/dir/image.png?text" + /// should be represented as `&["dir", "image.png"]`. + /// + /// Routing is performed only on normalized paths, so if a route exists for + /// "/endpoint", "/endpoint/" will also match, and vice versa. Routes also match all + /// requests for which they are the base of, meaning a request of "/api/endpoint" will + /// match a route of "/api" if no route exists specifically for "/api/endpoint". + /// + /// Longer routes automatically match before shorter routes. + pub fn match_path(&self, path: I) -> Option<&Handler> + where + I: IntoIterator, + S: AsRef, + { + let mut node = self; + let mut path = path.into_iter(); + let mut last_seen_handler = None; + loop { + let Self(maybe_handler, map) = node; + + last_seen_handler = maybe_handler.as_ref().or(last_seen_handler); + + if let Some(segment) = path.next() { + if let Some(route) = map.get(segment.as_ref()) { + node = route; + } else { + return last_seen_handler; + } + } else { + return last_seen_handler; + } + } + } + + /// Attempt to identify a route for a given [`Request`] + /// + /// See [`match_path()`](Self::match_path()) for how matching works + pub fn match_request(&self, req: Request) -> Option<&Handler> { + let mut path = req.path().to_owned(); + path.normalize(false); + self.match_path(path.segments()) + } + + /// Add a route to the network + /// + /// This method wraps [`add_route_by_path()`](Self::add_route_by_path()) while + /// unwrapping any errors that might occur. For this reason, this method only takes + /// static strings. If you would like to add a string dynamically, please use + /// [`RoutingNode::add_route_by_path()`] in order to appropriately deal with any + /// errors that might arise. + pub fn add_route(&mut self, path: &'static str, handler: impl Into) { + let path: Path = path.try_into().expect("Malformed path route received"); + self.add_route_by_path(path, handler).unwrap(); + } + + /// Add a route to the network + /// + /// The path provided MUST be absolute. Callers should verify this before calling + /// this method. + /// + /// For information about how routes work, see [`RoutingNode::match_path()`] + pub fn add_route_by_path(&mut self, mut path: Path, handler: impl Into) -> Result<(), ConflictingRouteError>{ + debug_assert!(path.is_absolute()); + path.normalize(false); + + let mut node = self; + for segment in path.segments() { + node = node.1.entry(segment.to_string()).or_default(); + } + + if node.0.is_some() { + Err(ConflictingRouteError()) + } else { + node.0 = Some(handler.into()); + Ok(()) + } + } + + /// Recursively shrink maps to fit + pub fn shrink(&mut self) { + let mut to_shrink = vec![&mut self.1]; + while let Some(shrink) = to_shrink.pop() { + shrink.shrink_to_fit(); + to_shrink.extend(shrink.values_mut().map(|n| &mut n.1)); + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ConflictingRouteError(); + +impl std::error::Error for ConflictingRouteError { } + +impl std::fmt::Display for ConflictingRouteError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Attempted to create a route with the same matcher as an existing route") + } +}