Skip to content

Commit 12248fa

Browse files
authored
Merge pull request #35 from conduit-rust/workspace
Convert repository into multi-crate workspace
2 parents 9079b9e + 64d71c7 commit 12248fa

File tree

8 files changed

+532
-15
lines changed

8 files changed

+532
-15
lines changed

Cargo.toml

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
1-
[package]
2-
3-
name = "conduit"
4-
version = "0.9.0-alpha.5"
5-
authors = ["[email protected]",
6-
"Alex Crichton <[email protected]>"]
7-
description = "Common HTTP server interface"
8-
license = "MIT"
9-
repository = "https://github.com/conduit-rust/conduit"
10-
edition = "2018"
11-
121
[workspace]
13-
members = ["example"]
14-
15-
[dependencies]
16-
http = "0.2"
2+
members = [
3+
"conduit",
4+
"conduit-router",
5+
"conduit-test",
6+
"example",
7+
]

conduit-router/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "conduit-router"
3+
version = "0.9.0-alpha.6"
4+
authors = ["[email protected]"]
5+
license = "MIT"
6+
description = "Router middleware for conduit based on route-recognizer"
7+
repository = "https://github.com/conduit-rust/conduit-router"
8+
edition = "2018"
9+
10+
[dependencies]
11+
conduit = { version = "0.9.0-alpha.5", path = "../conduit" }
12+
route-recognizer = "0.3"
13+
tracing = "0.1.26"
14+
15+
[dev-dependencies]
16+
conduit-test = { version = "0.9.0-alpha.5", path = "../conduit-test" }
17+
lazy_static = "1.4.0"
18+
tracing-subscriber = "0.2.19"

conduit-router/src/lib.rs

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
#![warn(rust_2018_idioms)]
2+
3+
#[macro_use]
4+
extern crate tracing;
5+
6+
use std::collections::hash_map::{Entry, HashMap};
7+
use std::error::Error;
8+
use std::fmt;
9+
10+
use conduit::{box_error, Handler, HandlerResult, Method, RequestExt};
11+
use route_recognizer::{Match, Params, Router};
12+
13+
static UNKNOWN_METHOD: &str = "Invalid method";
14+
static NOT_FOUND: &str = "Path not found";
15+
16+
#[derive(Default)]
17+
pub struct RouteBuilder {
18+
routers: HashMap<Method, Router<WrappedHandler>>,
19+
}
20+
21+
#[derive(Clone, Copy)]
22+
pub struct RoutePattern(&'static str);
23+
24+
impl RoutePattern {
25+
pub fn pattern(&self) -> &str {
26+
self.0
27+
}
28+
}
29+
30+
struct WrappedHandler {
31+
pattern: RoutePattern,
32+
handler: Box<dyn Handler>,
33+
}
34+
35+
impl conduit::Handler for WrappedHandler {
36+
fn call(&self, request: &mut dyn RequestExt) -> HandlerResult {
37+
self.handler.call(request)
38+
}
39+
}
40+
41+
#[derive(Debug)]
42+
pub enum RouterError {
43+
UnknownMethod,
44+
PathNotFound,
45+
}
46+
47+
impl RouteBuilder {
48+
pub fn new() -> Self {
49+
Self {
50+
routers: HashMap::new(),
51+
}
52+
}
53+
54+
#[instrument(level = "trace", skip(self))]
55+
fn recognize<'a>(
56+
&'a self,
57+
method: &Method,
58+
path: &str,
59+
) -> Result<Match<&WrappedHandler>, RouterError> {
60+
match self.routers.get(method) {
61+
Some(router) => router.recognize(path).or(Err(RouterError::PathNotFound)),
62+
None => Err(RouterError::UnknownMethod),
63+
}
64+
}
65+
66+
#[instrument(level = "trace", skip(self, handler))]
67+
pub fn map<H: Handler>(
68+
&mut self,
69+
method: Method,
70+
pattern: &'static str,
71+
handler: H,
72+
) -> &mut Self {
73+
{
74+
let router = match self.routers.entry(method) {
75+
Entry::Occupied(e) => e.into_mut(),
76+
Entry::Vacant(e) => e.insert(Router::new()),
77+
};
78+
let wrapped_handler = WrappedHandler {
79+
pattern: RoutePattern(pattern),
80+
handler: Box::new(handler),
81+
};
82+
router.add(pattern, wrapped_handler);
83+
}
84+
self
85+
}
86+
87+
pub fn get<H: Handler>(&mut self, pattern: &'static str, handler: H) -> &mut Self {
88+
self.map(Method::GET, pattern, handler)
89+
}
90+
91+
pub fn post<H: Handler>(&mut self, pattern: &'static str, handler: H) -> &mut Self {
92+
self.map(Method::POST, pattern, handler)
93+
}
94+
95+
pub fn put<H: Handler>(&mut self, pattern: &'static str, handler: H) -> &mut Self {
96+
self.map(Method::PUT, pattern, handler)
97+
}
98+
99+
pub fn delete<H: Handler>(&mut self, pattern: &'static str, handler: H) -> &mut Self {
100+
self.map(Method::DELETE, pattern, handler)
101+
}
102+
103+
pub fn head<H: Handler>(&mut self, pattern: &'static str, handler: H) -> &mut Self {
104+
self.map(Method::HEAD, pattern, handler)
105+
}
106+
}
107+
108+
impl conduit::Handler for RouteBuilder {
109+
#[instrument(level = "trace", skip(self, request))]
110+
fn call(&self, request: &mut dyn RequestExt) -> HandlerResult {
111+
let mut m = {
112+
let method = request.method();
113+
let path = request.path();
114+
115+
match self.recognize(&method, path) {
116+
Ok(m) => m,
117+
Err(e) => {
118+
info!("{}", e);
119+
return Err(box_error(e));
120+
}
121+
}
122+
};
123+
124+
// We don't have `pub` access to the fields to destructure `Params`, so swap with an empty
125+
// value to avoid an allocation.
126+
let mut params = Params::new();
127+
std::mem::swap(m.params_mut(), &mut params);
128+
129+
let pattern = m.handler().pattern;
130+
debug!(pattern = pattern.0, "matching route handler found");
131+
132+
{
133+
let extensions = request.mut_extensions();
134+
extensions.insert(pattern);
135+
extensions.insert(params);
136+
}
137+
138+
let span = trace_span!("handler", pattern = pattern.0);
139+
span.in_scope(|| m.handler().call(request))
140+
}
141+
}
142+
143+
impl Error for RouterError {
144+
fn description(&self) -> &str {
145+
match self {
146+
Self::UnknownMethod => UNKNOWN_METHOD,
147+
Self::PathNotFound => NOT_FOUND,
148+
}
149+
}
150+
}
151+
152+
impl fmt::Display for RouterError {
153+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154+
#[allow(deprecated)]
155+
self.description().fmt(f)
156+
}
157+
}
158+
159+
pub trait RequestParams<'a> {
160+
fn params(self) -> &'a Params;
161+
}
162+
163+
impl<'a> RequestParams<'a> for &'a (dyn RequestExt + 'a) {
164+
fn params(self) -> &'a Params {
165+
self.extensions().find::<Params>().expect("Missing params")
166+
}
167+
}
168+
169+
#[cfg(test)]
170+
mod tests {
171+
use super::{RequestParams, RouteBuilder, RoutePattern};
172+
173+
use conduit::{Body, Handler, Method, Response, StatusCode};
174+
use conduit_test::{MockRequest, ResponseExt};
175+
176+
lazy_static::lazy_static! {
177+
static ref TRACING: () = {
178+
tracing_subscriber::FmtSubscriber::builder()
179+
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
180+
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL)
181+
.with_test_writer()
182+
.init();
183+
};
184+
}
185+
186+
#[test]
187+
fn basic_get() {
188+
lazy_static::initialize(&TRACING);
189+
190+
let router = test_router();
191+
let mut req = MockRequest::new(Method::GET, "/posts/1");
192+
let res = router.call(&mut req).expect("No response");
193+
194+
assert_eq!(res.status(), StatusCode::OK);
195+
assert_eq!(*res.into_cow(), b"1, GET, /posts/:id"[..]);
196+
}
197+
198+
#[test]
199+
fn basic_post() {
200+
lazy_static::initialize(&TRACING);
201+
202+
let router = test_router();
203+
let mut req = MockRequest::new(Method::POST, "/posts/10");
204+
let res = router.call(&mut req).expect("No response");
205+
206+
assert_eq!(res.status(), StatusCode::OK);
207+
assert_eq!(*res.into_cow(), b"10, POST, /posts/:id"[..]);
208+
}
209+
210+
#[test]
211+
fn path_not_found() {
212+
lazy_static::initialize(&TRACING);
213+
214+
let router = test_router();
215+
let mut req = MockRequest::new(Method::POST, "/nonexistent");
216+
let err = router.call(&mut req).err().unwrap();
217+
218+
assert_eq!(err.to_string(), super::NOT_FOUND);
219+
}
220+
221+
#[test]
222+
fn unknown_method() {
223+
lazy_static::initialize(&TRACING);
224+
225+
let router = test_router();
226+
let mut req = MockRequest::new(Method::DELETE, "/posts/1");
227+
let err = router.call(&mut req).err().unwrap();
228+
229+
assert_eq!(err.to_string(), super::UNKNOWN_METHOD);
230+
}
231+
232+
#[test]
233+
fn catch_all() {
234+
lazy_static::initialize(&TRACING);
235+
236+
let mut router = RouteBuilder::new();
237+
router.get("/*", test_handler);
238+
239+
let mut req = MockRequest::new(Method::GET, "/foo");
240+
let res = router.call(&mut req).expect("No response");
241+
assert_eq!(res.status(), StatusCode::OK);
242+
assert_eq!(*res.into_cow(), b", GET, /*"[..]);
243+
}
244+
245+
fn test_router() -> RouteBuilder {
246+
let mut router = RouteBuilder::new();
247+
router.post("/posts/:id", test_handler);
248+
router.get("/posts/:id", test_handler);
249+
router
250+
}
251+
252+
fn test_handler(req: &mut dyn conduit::RequestExt) -> conduit::HttpResult {
253+
let res = vec![
254+
req.params().find("id").unwrap_or("").to_string(),
255+
format!("{:?}", req.method()),
256+
req.extensions()
257+
.find::<RoutePattern>()
258+
.unwrap()
259+
.pattern()
260+
.to_string(),
261+
];
262+
263+
let bytes = res.join(", ").into_bytes();
264+
Response::builder().body(Body::from_vec(bytes))
265+
}
266+
}

conduit-test/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "conduit-test"
3+
version = "0.9.0-alpha.5"
4+
authors = ["[email protected]",
5+
"Alex Crichton <[email protected]>"]
6+
description = "Testing utilities for conduit-based stacks"
7+
repository = "https://github.com/conduit-rust/conduit-test"
8+
license = "MIT"
9+
10+
[dependencies]
11+
conduit = { version = "0.9.0-alpha.5", path = "../conduit" }

0 commit comments

Comments
 (0)