From f4eeb343098c5357e924253e9a18d2634f6f2a49 Mon Sep 17 00:00:00 2001 From: "Adam H. Leventhal" Date: Fri, 28 May 2021 14:56:09 -0700 Subject: [PATCH] Add support for wildcard routes: `/foo/bar/{rest:.*}` This will match paths such as `/foo/bar` and `/foo/bar/baz.css` We can use this to serve static assets such as css, html, and image files. Express uses this syntax: `/foo/bar/:rest(.*)` Dropwizard / Jersey / JAX-RS does: `/foo/bar/{rest:.*}` Actix does: `/foo/bar/{rest:.*}` This also adds support for a tag (unpublished = true) in the #[endpoint] macro to omit certain endpoints from the OpenAPI output. This is both useful for non-API endpoints and necessary in that OpenAPI doesn't support any type of multi-segment matching of routes. --- dropshot/examples/index.rs | 97 ++++++++ dropshot/src/api_description.rs | 17 +- dropshot/src/router.rs | 415 ++++++++++++++++++++++++++++---- dropshot/tests/test_openapi.rs | 21 +- dropshot_endpoint/src/lib.rs | 88 +++---- 5 files changed, 541 insertions(+), 97 deletions(-) create mode 100644 dropshot/examples/index.rs diff --git a/dropshot/examples/index.rs b/dropshot/examples/index.rs new file mode 100644 index 00000000..c90aa242 --- /dev/null +++ b/dropshot/examples/index.rs @@ -0,0 +1,97 @@ +// Copyright 2021 Oxide Computer Company +/*! + * Example use of Dropshot for matching wildcard paths to serve static content. + */ + +use dropshot::ApiDescription; +use dropshot::ConfigDropshot; +use dropshot::ConfigLogging; +use dropshot::ConfigLoggingLevel; +use dropshot::HttpError; +use dropshot::HttpServerStarter; +use dropshot::RequestContext; +use dropshot::{endpoint, Path}; +use http::{Response, StatusCode}; +use hyper::Body; +use schemars::JsonSchema; +use serde::Deserialize; +use std::sync::Arc; + +#[tokio::main] +async fn main() -> Result<(), String> { + /* + * We must specify a configuration with a bind address. We'll use 127.0.0.1 + * since it's available and won't expose this server outside the host. We + * request port 0, which allows the operating system to pick any available + * port. + */ + let config_dropshot: ConfigDropshot = Default::default(); + + /* + * For simplicity, we'll configure an "info"-level logger that writes to + * stderr assuming that it's a terminal. + */ + let config_logging = ConfigLogging::StderrTerminal { + level: ConfigLoggingLevel::Info, + }; + let log = config_logging + .to_logger("example-basic") + .map_err(|error| format!("failed to create logger: {}", error))?; + + /* + * Build a description of the API. + */ + let mut api = ApiDescription::new(); + api.register(index).unwrap(); + + /* + * Set up the server. + */ + let server = HttpServerStarter::new(&config_dropshot, api, (), &log) + .map_err(|error| format!("failed to create server: {}", error))? + .start(); + + /* + * Wait for the server to stop. Note that there's not any code to shut down + * this server, so we should never get past this point. + */ + server.await +} + +#[derive(Deserialize, JsonSchema)] +struct AllPath { + path: String, +} + +/** + * Return static content.for all paths. + */ +#[endpoint { + method = GET, + + /* + * Match literally every path including the empty path. + */ + path = "/{path:.*}", + + /* + * This isn't an API so we don't want this to appear in the OpenAPI + * description if we were to generate it. + */ + unpublished = true, +}] +async fn index( + _rqctx: Arc>, + path: Path, +) -> Result, HttpError> { + Ok(Response::builder() + .status(StatusCode::OK) + .header(http::header::CONTENT_TYPE, "text/html") + .body( + format!( + "nothing at {}", + path.into_inner().path + ) + .into(), + )?) +} diff --git a/dropshot/src/api_description.rs b/dropshot/src/api_description.rs index 20486ee6..4564c153 100644 --- a/dropshot/src/api_description.rs +++ b/dropshot/src/api_description.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Oxide Computer Company +// Copyright 2021 Oxide Computer Company /*! * Describes the endpoints and handler functions in your API */ @@ -36,6 +36,7 @@ pub struct ApiEndpoint { pub description: Option, pub tags: Vec, pub paginated: bool, + pub visible: bool, } impl<'a, Context: ServerContext> ApiEndpoint { @@ -61,6 +62,7 @@ impl<'a, Context: ServerContext> ApiEndpoint { description: None, tags: vec![], paginated: func_parameters.paginated, + visible: true, } } @@ -73,6 +75,11 @@ impl<'a, Context: ServerContext> ApiEndpoint { self.tags.push(tag.to_string()); self } + + pub fn visible(mut self, visible: bool) -> Self { + self.visible = visible; + self + } } /** @@ -230,8 +237,9 @@ impl ApiDescription { let path = path_to_segments(&e.path) .iter() .filter_map(|segment| match PathSegment::from(segment) { - PathSegment::Varname(v) => Some(v), - _ => None, + PathSegment::VarnameSegment(v) => Some(v), + PathSegment::VarnameRegEx(v, _) => Some(v), + PathSegment::Literal(_) => None, }) .collect::>(); let vars = e @@ -380,6 +388,9 @@ impl ApiDescription { indexmap::IndexMap::::new(); for (path, method, endpoint) in &self.router { + if !endpoint.visible { + continue; + } let path = openapi.paths.entry(path).or_insert( openapiv3::ReferenceOr::Item(openapiv3::PathItem::default()), ); diff --git a/dropshot/src/router.rs b/dropshot/src/router.rs index 42eff6b9..4c1bfd26 100644 --- a/dropshot/src/router.rs +++ b/dropshot/src/router.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Oxide Computer Company +// Copyright 2021 Oxide Computer Company /*! * Routes incoming HTTP requests to handler functions */ @@ -91,21 +91,27 @@ struct HttpRouterNode { enum HttpRouterEdges { /** Outgoing edges for literal paths. */ Literals(BTreeMap>>), - /** Outgoing edges for variable-named paths. */ - Variable(String, Box>), + /** Outgoing edge for variable-named paths. */ + VariableSingle(String, Box>), + /** Outgoing edge that consumes all remaining components. */ + VariableRest(String, Box>), } /** * `PathSegment` represents a segment in a URI path when the router is being * configured. Each segment may be either a literal string or a variable (the - * latter indicated by being wrapped in braces. + * latter indicated by being wrapped in braces). Variables may consume a single + * /-delimited segment or several as defined by a regex (currently only `.*` is + * supported). */ -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum PathSegment { /** a path segment for a literal string */ Literal(String), /** a path segment for a variable */ - Varname(String), + VarnameSegment(String), + /** a path segment for a variable with a regex */ + VarnameRegEx(String, String), } impl PathSegment { @@ -127,18 +133,61 @@ impl PathSegment { "{}", "HTTP URI path segment variable missing trailing \"}\"" ); + + let var = &segment[1..segment.len() - 1]; + + let (var, pat) = if let Some(index) = var.find(':') { + (&var[..index], Some(&var[index + 1..])) + } else { + (var, None) + }; + assert!( - segment.len() > 2, - "HTTP URI path segment variable name cannot be empty" + valid_identifier(var), + "HTTP URI path segment variable name must be a valid \ + identifier: '{}'", + var ); - PathSegment::Varname((&segment[1..segment.len() - 1]).to_string()) + if let Some(pat) = pat { + assert!( + pat == ".*", + "Only the pattern '.*' is currently supported" + ); + PathSegment::VarnameRegEx(var.to_string(), pat.to_string()) + } else { + PathSegment::VarnameSegment(var.to_string()) + } } else { PathSegment::Literal(segment.to_string()) } } } +/** + * Validate the pattern for Rust identifiers: + * [a-zA-Z][a-zA-Z0-9_]* | _[a-zA-Z0-9_]+ + */ +fn valid_identifier(var: &str) -> bool { + match var.chars().nth(0) { + Some('_') => { + if var.len() == 1 { + // A lone '_' is not a valid identifier. + return false; + } + } + Some('a'..='z') | Some('A'..='Z') => {} + _ => return false, + } + + for ch in var.chars().skip(1) { + if !ch.is_ascii_alphanumeric() && ch != '_' { + return false; + } + } + true +} + /** * `RouterLookupResult` represents the result of invoking * `HttpRouter::lookup_route()`. A successful route lookup includes both the @@ -179,11 +228,11 @@ impl HttpRouter { let method = endpoint.method.clone(); let path = endpoint.path.clone(); - let all_segments = path_to_segments(path.as_str()); + let mut all_segments = path_to_segments(path.as_str()).into_iter(); let mut varnames: BTreeSet = BTreeSet::new(); let mut node: &mut Box> = &mut self.root; - for raw_segment in all_segments { + while let Some(raw_segment) = all_segments.next() { let segment = PathSegment::from(raw_segment); node = match segment { @@ -198,7 +247,8 @@ impl HttpRouter { * caveats about how matching would work), but it seems * more likely to be a mistake. */ - HttpRouterEdges::Variable(varname, _) => { + HttpRouterEdges::VariableSingle(varname, _) + | HttpRouterEdges::VariableRest(varname, _) => { panic!( "URI path \"{}\": attempted to register route \ for literal path segment \"{}\" when a route \ @@ -213,26 +263,82 @@ impl HttpRouter { } } - PathSegment::Varname(new_varname) => { + PathSegment::VarnameSegment(new_varname) => { + insert_var(&path, &mut varnames, &new_varname); + + let edges = node.edges.get_or_insert( + HttpRouterEdges::VariableSingle( + new_varname.clone(), + Box::new(HttpRouterNode::new()), + ), + ); + match edges { + /* + * See the analogous check above about combining literal + * and variable path segments from the same resource. + */ + HttpRouterEdges::Literals(_) => panic!( + "URI path \"{}\": attempted to register route for \ + variable path segment (variable name: \"{}\") \ + when a route already exists for a literal path \ + segment", + path, new_varname + ), + + HttpRouterEdges::VariableRest(varname, _) => panic!( + "URI path \"{}\": attempted to register route for \ + variable path segment (variable name: \"{}\") \ + when a route already exists for the remainder of \ + the path as {}", + path, new_varname, varname, + ), + + HttpRouterEdges::VariableSingle( + varname, + ref mut node, + ) => { + if *new_varname != *varname { + /* + * Don't allow people to use different names for + * the same part of the path. Again, this could + * be supported, but it seems likely to be + * confusing and probably a mistake. + */ + panic!( + "URI path \"{}\": attempted to use \ + variable name \"{}\", but a different \ + name (\"{}\") has already been used for \ + this", + path, new_varname, varname + ); + } + + node + } + } + } + PathSegment::VarnameRegEx(new_varname, pat) => { + assert_eq!(pat, ".*"); + /* - * Do not allow the same variable name to be used more than - * once in the path. Again, this could be supported (with - * some caveats), but it seems more likely to be a mistake. + * We don't accept further path segments after the .*. */ - if varnames.contains(&new_varname) { + if all_segments.next().is_some() { panic!( - "URI path \"{}\": variable name \"{}\" is used \ - more than once", - path, new_varname + "URI path \"{}\": attempted to match segments \ + after the wildcard variable \"{}\"", + path, new_varname, ); } - varnames.insert(new_varname.clone()); - let edges = - node.edges.get_or_insert(HttpRouterEdges::Variable( + insert_var(&path, &mut varnames, &new_varname); + + let edges = node.edges.get_or_insert( + HttpRouterEdges::VariableRest( new_varname.clone(), Box::new(HttpRouterNode::new()), - )); + ), + ); match edges { /* * See the analogous check above about combining literal @@ -240,13 +346,22 @@ impl HttpRouter { */ HttpRouterEdges::Literals(_) => panic!( "URI path \"{}\": attempted to register route for \ - variable path segment (variable name: \"{}\") \ - when a route already exists for a literal path \ - segment", + variable path regex (variable name: \"{}\") when \ + a route already exists for a literal path segment", path, new_varname ), - HttpRouterEdges::Variable(varname, ref mut node) => { + HttpRouterEdges::VariableSingle(varname, _) => panic!( + "URI path \"{}\": attempted to register route for \ + variable path regex (variable name: \"{}\") when \ + a route already exists for a segment {}", + path, new_varname, varname, + ), + + HttpRouterEdges::VariableRest( + varname, + ref mut node, + ) => { if *new_varname != *varname { /* * Don't allow people to use different names for @@ -296,22 +411,38 @@ impl HttpRouter { pub fn lookup_route<'a, 'b>( &'a self, method: &'b Method, - path: &'b str, + mut path: &'b str, ) -> Result, HttpError> { - let all_segments = path_to_segments(path); let mut node = &self.root; let mut variables: BTreeMap = BTreeMap::new(); - for segment in all_segments { - let segment_string = segment.to_string(); + assert_eq!(path.chars().nth(0), Some('/')); + /* Skip initial slashes */ + while path.chars().nth(0) == Some('/') { + path = &path[1..]; + } + + while !path.is_empty() { node = match &node.edges { None => None, + Some(HttpRouterEdges::Literals(edges)) => { - edges.get(&segment_string) + let (segment, rest) = get_path_segment(path); + path = rest; + edges.get(&segment.to_string()) } - Some(HttpRouterEdges::Variable(varname, ref node)) => { - variables.insert(varname.clone(), segment_string); + Some(HttpRouterEdges::VariableSingle(varname, ref node)) => { + let (segment, rest) = get_path_segment(path); + path = rest; + variables.insert(varname.clone(), segment.to_string()); + Some(node) + } + Some(HttpRouterEdges::VariableRest(varname, node)) => { + variables.insert(varname.clone(), path.to_string()); + path = ""; + /* There should be no outgoing edges */ + assert!(node.edges.is_none()); Some(node) } } @@ -323,6 +454,21 @@ impl HttpRouter { })? } + /* + * The wildcard match consumes the empty path segment + * TODO: this continues our loosey-goosey treatment regarding the presence + * or absence of trailing slashes. + */ + match &node.edges { + Some(HttpRouterEdges::VariableRest(varname, new_node)) => { + variables.insert(varname.clone(), path.to_string()); + /* There should be no outgoing edges */ + assert!(new_node.edges.is_none()); + node = new_node; + } + _ => {} + } + /* * As a somewhat special case, if one requests a node with no handlers * at all, report a 404. We could probably treat this as a 405 as well. @@ -347,6 +493,28 @@ impl HttpRouter { } } +/** + * Insert a variable into the set after checking for duplicates. + */ +fn insert_var( + path: &str, + varnames: &mut BTreeSet, + new_varname: &String, +) -> () { + /* + * Do not allow the same variable name to be used more than + * once in the path. Again, this could be supported (with + * some caveats), but it seems more likely to be a mistake. + */ + if varnames.contains(new_varname) { + panic!( + "URI path \"{}\": variable name \"{}\" is used more than once", + path, new_varname + ); + } + varnames.insert(new_varname.clone()); +} + impl<'a, Context: ServerContext> IntoIterator for &'a HttpRouter { type Item = (String, String, &'a ApiEndpoint); type IntoIter = HttpRouterIter<'a, Context>; @@ -398,9 +566,18 @@ impl<'a, Context: ServerContext> HttpRouterIter<'a, Context> { map.iter() .map(|(s, node)| (PathSegment::Literal(s.clone()), node)), ), - Some(HttpRouterEdges::Variable(ref varname, ref node)) => Box::new( - std::iter::once((PathSegment::Varname(varname.clone()), node)), - ), + Some(HttpRouterEdges::VariableSingle(varname, node)) => { + Box::new(std::iter::once(( + PathSegment::VarnameSegment(varname.clone()), + node, + ))) + } + Some(HttpRouterEdges::VariableRest(varname, node)) => { + Box::new(std::iter::once(( + PathSegment::VarnameSegment(varname.clone()), + node, + ))) + } None => Box::new(std::iter::empty()), } } @@ -414,7 +591,8 @@ impl<'a, Context: ServerContext> HttpRouterIter<'a, Context> { .iter() .map(|(c, _)| match c { PathSegment::Literal(s) => s.clone(), - PathSegment::Varname(s) => format!("{{{}}}", s), + PathSegment::VarnameSegment(s) => format!("{{{}}}", s), + PathSegment::VarnameRegEx(s, p) => format!("{{{}:{}}}", s, p), }) .collect(); @@ -463,7 +641,7 @@ impl<'a, Context: ServerContext> Iterator for HttpRouterIter<'a, Context> { } /** - * Helper function for taking a Uri path and producing a `Vec` of + * Helper function for taking a Uri path and producing a `Vec<&str>` of * URL-encoded strings, each representing one segment of the path. */ pub fn path_to_segments(path: &str) -> Vec<&str> { @@ -504,6 +682,31 @@ pub fn path_to_segments(path: &str) -> Vec<&str> { path.split('/').filter(|segment| !segment.is_empty()).collect::>() } +/** + * Helper function for splitting a Uri path into the first segment and the + * remainder of the path. + */ +pub fn get_path_segment(mut path: &str) -> (&str, &str) { + assert!(!path.is_empty()); + + /* Skip leading slashes */ + while path.starts_with('/') { + path = &path[1..]; + } + + match path.find('/') { + Some(index) => { + let segment = &path[..index]; + let mut rest = &path[index + 1..]; + while rest.starts_with('/') { + rest = &rest[1..]; + } + (segment, rest) + } + None => (path, ""), + } +} + #[cfg(test)] mod test { use super::super::error::HttpError; @@ -511,6 +714,7 @@ mod test { use super::super::handler::RequestContext; use super::super::handler::RouteHandler; use super::HttpRouter; + use super::PathSegment; use crate::ApiEndpoint; use crate::ApiEndpointResponse; use http::Method; @@ -552,13 +756,13 @@ mod test { description: None, tags: vec![], paginated: false, + visible: true, } } #[test] - #[should_panic( - expected = "HTTP URI path segment variable name cannot be empty" - )] + #[should_panic(expected = "HTTP URI path segment variable name must be a \ + valid identifier: ''")] fn test_variable_name_empty() { let mut router = HttpRouter::new(); router.insert(new_endpoint(new_handler(), Method::GET, "/foo/{}")); @@ -686,6 +890,49 @@ mod test { )); } + #[test] + #[should_panic(expected = "URI path \"/projects/default\": attempted to \ + register route for literal path segment \ + \"default\" when a route exists for variable \ + path segment (variable name: \"rest\")")] + fn test_literal_after_regex() { + let mut router = HttpRouter::new(); + router.insert(new_endpoint( + new_handler(), + Method::GET, + "/projects/{rest:.*}", + )); + router.insert(new_endpoint( + new_handler(), + Method::GET, + "/projects/default", + )); + } + + #[test] + #[should_panic(expected = "Only the pattern '.*' is currently supported")] + fn test_bogus_regex() { + let mut router = HttpRouter::new(); + router.insert(new_endpoint( + new_handler(), + Method::GET, + "/word/{rest:[a-z]*}", + )); + } + + #[test] + #[should_panic(expected = "URI path \"/some/{more:.*}/{stuff}\": \ + attempted to match segments after the \ + wildcard variable \"more\"")] + fn test_after_regex() { + let mut router = HttpRouter::new(); + router.insert(new_endpoint( + new_handler(), + Method::GET, + "/some/{more:.*}/{stuff}", + )); + } + #[test] fn test_error_cases() { let mut router = HttpRouter::new(); @@ -925,6 +1172,25 @@ mod test { assert_eq!(result.handler.label(), "h7"); } + #[test] + fn test_variables_glob() { + let mut router = HttpRouter::new(); + router.insert(new_endpoint( + new_handler_named("h8"), + Method::OPTIONS, + "/console/{path:.*}", + )); + + let result = router + .lookup_route(&Method::OPTIONS, "/console/missiles/launch") + .unwrap(); + + assert_eq!( + result.variables.get("path"), + Some(&"missiles/launch".to_string()), + ); + } + #[test] fn test_iter_null() { let router = HttpRouter::<()>::new(); @@ -971,4 +1237,67 @@ mod test { ("/".to_string(), "POST".to_string(),), ]); } + + #[test] + fn test_path_segment() { + let seg = PathSegment::from("abc"); + assert_eq!(seg, PathSegment::Literal("abc".to_string())); + + let seg = PathSegment::from("{words}"); + assert_eq!(seg, PathSegment::VarnameSegment("words".to_string())); + + let seg = PathSegment::from("{rest:.*}"); + assert_eq!( + seg, + PathSegment::VarnameRegEx("rest".to_string(), ".*".to_string()) + ); + } + + #[test] + #[should_panic] + fn test_bad_path_segment1() { + let _ = PathSegment::from("{foo"); + } + + #[test] + #[should_panic] + fn test_bad_path_segment2() { + let _ = PathSegment::from("bar}"); + } + + #[test] + #[should_panic] + fn test_bad_path_segment3() { + let _ = PathSegment::from("{867_5309}"); + } + + #[test] + #[should_panic] + fn test_bad_path_segment4() { + let _ = PathSegment::from("{_}"); + } + + #[test] + #[should_panic] + fn test_bad_path_segment5() { + let _ = PathSegment::from("{...}"); + } + + #[test] + #[should_panic] + fn test_bad_path_segment6() { + let _ = PathSegment::from("{}"); + } + + #[test] + #[should_panic] + fn test_bad_path_segment7() { + let _ = PathSegment::from("{}"); + } + + #[test] + #[should_panic] + fn test_bad_path_segment8() { + let _ = PathSegment::from("{varname:abc+}"); + } } diff --git a/dropshot/tests/test_openapi.rs b/dropshot/tests/test_openapi.rs index 81658056..9f9f5610 100644 --- a/dropshot/tests/test_openapi.rs +++ b/dropshot/tests/test_openapi.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Oxide Computer Company +// Copyright 2021 Oxide Computer Company use dropshot::{ endpoint, ApiDescription, HttpError, HttpResponseAccepted, @@ -283,6 +283,24 @@ async fn handler15( unimplemented!(); } +#[allow(dead_code)] +#[derive(JsonSchema, Deserialize)] +struct AllPath { + path: String, +} + +#[endpoint { + method = GET, + path = "/ceci_nes_pas_une_endpoint/{path:.*}", + unpublished = true, +}] +async fn handler16( + _rqctx: Arc>, + _path: Path, +) -> Result, HttpError> { + unimplemented!(); +} + fn make_api() -> Result, String> { let mut api = ApiDescription::new(); api.register(handler1)?; @@ -300,6 +318,7 @@ fn make_api() -> Result, String> { api.register(handler13)?; api.register(handler14)?; api.register(handler15)?; + api.register(handler16)?; Ok(api) } diff --git a/dropshot_endpoint/src/lib.rs b/dropshot_endpoint/src/lib.rs index 2eefc7f3..bb802f06 100644 --- a/dropshot_endpoint/src/lib.rs +++ b/dropshot_endpoint/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Oxide Computer Company +// Copyright 2021 Oxide Computer Company //! This package defines macro attributes associated with HTTP handlers. These //! attributes are used both to define an HTTP API and to generate an OpenAPI @@ -49,6 +49,7 @@ struct Metadata { method: MethodType, path: String, tags: Option>, + unpublished: Option, _dropshot_crate: Option, } @@ -80,8 +81,10 @@ fn usage(err_msg: &str, fn_name: &str) -> String { /// method = { DELETE | GET | PATCH | POST | PUT }, /// path = "/path/name/with/{named}/{variables}", /// -/// // Optional fields +/// // Optional tags for the API description /// tags = [ "all", "your", "OpenAPI", "tags" ], +/// // A value of `true` causes the API to be omitted from the API description +/// unpublished = { true | false }, /// }] /// ``` /// @@ -150,6 +153,14 @@ fn do_endpoint( }) .unwrap_or_default(); + let visible = if let Some(true) = metadata.unpublished { + quote! { + .visible(false) + } + } else { + quote! {} + }; + let dropshot = get_crate(metadata._dropshot_crate); let first_arg = ast.sig.inputs.first().ok_or_else(|| { @@ -240,6 +251,7 @@ fn do_endpoint( ) #description #(#tags)* + #visible } } }; @@ -313,12 +325,10 @@ mod tests { quote! { method = GET, path = "/a/b/c" - } - .into(), + }, quote! { pub async fn handler_xyz(_rqctx: Arc>) {} - } - .into(), + }, ); let expected = quote! { #[allow(non_camel_case_types, missing_docs)] @@ -351,12 +361,10 @@ mod tests { quote! { method = GET, path = "/a/b/c" - } - .into(), + }, quote! { pub async fn handler_xyz(_rqctx: std::sync::Arc>) {} - } - .into(), + }, ); let expected = quote! { #[allow(non_camel_case_types, missing_docs)] @@ -389,12 +397,10 @@ mod tests { quote! { method = GET, path = "/a/b/c" - } - .into(), + }, quote! { async fn handler_xyz(_rqctx: Arc>, q: Query) {} - } - .into(), + }, ); let query = quote! { Query @@ -439,12 +445,10 @@ mod tests { quote! { method = GET, path = "/a/b/c" - } - .into(), + }, quote! { pub(crate) async fn handler_xyz(_rqctx: Arc>, q: Query) {} - } - .into(), + }, ); let query = quote! { Query @@ -490,12 +494,10 @@ mod tests { method = GET, path = "/a/b/c", tags = ["stuff", "things"], - } - .into(), + }, quote! { async fn handler_xyz(_rqctx: Arc>) {} - } - .into(), + }, ); let expected = quote! { #[allow(non_camel_case_types, missing_docs)] @@ -528,13 +530,11 @@ mod tests { quote! { method = GET, path = "/a/b/c" - } - .into(), + }, quote! { /** handle "xyz" requests */ async fn handler_xyz(_rqctx: Arc>) {} - } - .into(), + }, ); let expected = quote! { #[allow(non_camel_case_types, missing_docs)] @@ -567,12 +567,10 @@ mod tests { quote! { method = GET, path = "/a/b/c" - } - .into(), + }, quote! { const POTATO = "potato"; - } - .into(), + }, ); let msg = format!("{}", ret.err().unwrap()); @@ -585,12 +583,10 @@ mod tests { quote! { method = GET, path = /a/b/c - } - .into(), + }, quote! { const POTATO = "potato"; - } - .into(), + }, ); let msg = format!("{}", ret.err().unwrap()); @@ -603,12 +599,10 @@ mod tests { quote! { methud = GET, path = "/a/b/c" - } - .into(), + }, quote! { const POTATO = "potato"; - } - .into(), + }, ); let msg = format!("{}", ret.err().unwrap()); @@ -621,12 +615,10 @@ mod tests { quote! { method = GET, path = "/a/b/c", - } - .into(), + }, quote! { fn handler_xyz(_rqctx: Arc) {} - } - .into(), + }, ); let msg = format!("{}", ret.err().unwrap()); @@ -639,12 +631,10 @@ mod tests { quote! { method = GET, path = "/a/b/c", - } - .into(), + }, quote! { async fn handler_xyz(&self) {} - } - .into(), + }, ); let msg = format!("{}", ret.err().unwrap()); @@ -660,12 +650,10 @@ mod tests { quote! { method = GET, path = "/a/b/c", - } - .into(), + }, quote! { async fn handler_xyz() {} - } - .into(), + }, ); let msg = format!("{}", ret.err().unwrap());