From 60e6ad96fe5e6840cee0f185387dea2902b226eb Mon Sep 17 00:00:00 2001 From: vshengbro Date: Fri, 9 Oct 2026 13:53:01 +0800 Subject: [PATCH] feat(server): infer route path type via impl AsRef parameter Drop the explicit P generic from Server::route so callers write route::("/") without naming the path type. The path now arrives as impl AsRef (the inference-shim exemption in rust-standards section 9.2a), which keeps &str, String and &String call sites compiling unchanged while removing the mandatory second turbofish parameter. BREAKING CHANGE: turbofish call sites that named the path type explicitly (route::(path)) no longer compile, because the method now takes exactly one generic argument. All 42 in-tree call sites in core/tests and plugin/websocket/tests were updated in this commit, and a new route_path_type_inference test proves &str, String, &String and chained registration all infer correctly. Bump the workspace version 21.11.0 -> 21.12.0 (minor carries the breaking change under the workspace release convention) and sync the workspace.dependencies path versions via crate sync. --- Cargo.toml | 20 ++++----- core/src/server/impl.rs | 5 +-- core/tests/route/fn.rs | 57 ++++++++++++++++---------- core/tests/server/fn.rs | 38 ++++++++--------- plugin/websocket/tests/websocket/fn.rs | 4 +- 5 files changed, 69 insertions(+), 55 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4ef710a..8cf6bc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ categories = ["network-programming", "web-programming"] exclude.workspace = true [workspace.package] -version = "21.11.0" +version = "21.12.0" readme = "README.md" edition = "2024" authors = ["root@ltpp.vip"] @@ -25,15 +25,15 @@ resolver = "2" members = ["compress", "constant", "type", "request", "core", "macros", "cli", "plugin/websocket"] [workspace.dependencies] -hyperlane = { path = ".", version = "21.11.0" } -http-type = { path = "type", version = "21.11.0" } -hyperlane-cli = { path = "cli", version = "21.11.0" } -hyperlane-core = { path = "core", version = "21.11.0" } -http-request = { path = "request", version = "21.11.0" } -http-compress = { path = "compress", version = "21.11.0" } -http-constant = { path = "constant", version = "21.11.0" } -hyperlane-macros = { path = "macros", version = "21.11.0" } -hyperlane-plugin-websocket = { path = "plugin/websocket", version = "21.11.0" } +hyperlane = { path = ".", version = "21.12.0" } +http-type = { path = "type", version = "21.12.0" } +hyperlane-cli = { path = "cli", version = "21.12.0" } +hyperlane-core = { path = "core", version = "21.12.0" } +http-request = { path = "request", version = "21.12.0" } +http-compress = { path = "compress", version = "21.12.0" } +http-constant = { path = "constant", version = "21.12.0" } +hyperlane-macros = { path = "macros", version = "21.12.0" } +hyperlane-plugin-websocket = { path = "plugin/websocket", version = "21.12.0" } hex = "0.4.3" md5 = "0.8.1" diff --git a/core/src/server/impl.rs b/core/src/server/impl.rs index 50b0c87..0afe14f 100644 --- a/core/src/server/impl.rs +++ b/core/src/server/impl.rs @@ -406,16 +406,15 @@ impl Server { /// /// # Arguments /// - /// - `P` - The route path pattern. + /// - `impl AsRef` - The route path pattern. /// /// # Returns /// /// - `&mut Self` - Reference to self for method chaining. #[inline(always)] - pub fn route(&mut self, path: P) -> &mut Self + pub fn route(&mut self, path: impl AsRef) -> &mut Self where S: ServerHook, - P: AsRef, { self.get_mut_route_matcher() .add(path.as_ref(), Hook::factory::()) diff --git a/core/tests/route/fn.rs b/core/tests/route/fn.rs index e38f868..8fd007c 100644 --- a/core/tests/route/fn.rs +++ b/core/tests/route/fn.rs @@ -3,24 +3,39 @@ use super::*; #[tokio::test] #[should_panic(expected = "EmptyPattern")] async fn empty_route() { - let _server: &Server = Server::default().route::(EMPTY_STR); + let _server: &Server = Server::default().route::(EMPTY_STR); } #[tokio::test] #[should_panic(expected = "DuplicatePattern")] async fn duplicate_route() { let _server: &Server = Server::default() - .route::(ROOT_PATH) - .route::(ROOT_PATH); + .route::(ROOT_PATH) + .route::(ROOT_PATH); +} + +#[test] +fn route_path_type_inference() { + let mut server: Server = Server::default(); + server.route::("/infer/str"); + let owned_path: String = "/infer/string".to_string(); + server.route::(owned_path); + let ref_path: String = "/infer/ref".to_string(); + server.route::(&ref_path); + server + .route::("/infer/chain/a") + .route::("/infer/chain/b"); + let route_matcher: RouteMatcher = server.get_route_matcher().clone(); + assert_eq!(route_matcher.get_static_route().len(), 5); } #[test] fn get_route() { let mut server: Server = Server::default(); server - .route::(ROOT_PATH) - .route::("/dynamic/{routing}") - .route::("/regex/{file:^.*$}"); + .route::(ROOT_PATH) + .route::("/dynamic/{routing}") + .route::("/regex/{file:^.*$}"); let route_matcher: RouteMatcher = server.get_route_matcher().clone(); for key in route_matcher.get_static_route().keys() { println!("Static route: {key}"); @@ -40,10 +55,10 @@ fn get_route() { #[test] fn segment_count_optimization() { let mut server: Server = Server::default(); - server.route::("/users/{id}"); - server.route::("/users/{id}/posts"); - server.route::("/users/{id}/posts/{post_id}"); - server.route::("/api/v1/users/{id}"); + server.route::("/users/{id}"); + server.route::("/users/{id}/posts"); + server.route::("/users/{id}/posts/{post_id}"); + server.route::("/api/v1/users/{id}"); let route_matcher: RouteMatcher = server.get_route_matcher().clone(); assert!( route_matcher.get_dynamic_route().contains_key(&2), @@ -65,9 +80,9 @@ fn segment_count_optimization() { #[test] fn regex_route_segment_count() { let mut server: Server = Server::default(); - server.route::("/files/{path:.*}"); - server.route::("/api/{version:\\d+}/users"); - server.route::("/api/{version:\\d+}/posts/{id:\\d+}"); + server.route::("/files/{path:.*}"); + server.route::("/api/{version:\\d+}/users"); + server.route::("/api/{version:\\d+}/posts/{id:\\d+}"); let route_matcher: RouteMatcher = server.get_route_matcher().clone(); assert!( route_matcher.get_regex_route().contains_key(&2), @@ -86,11 +101,11 @@ fn regex_route_segment_count() { #[test] fn mixed_route_types() { let mut server: Server = Server::default(); - server.route::("/"); - server.route::("/about"); - server.route::("/users/{id}"); - server.route::("/posts/{slug}"); - server.route::("/files/{path:.*}"); + server.route::("/"); + server.route::("/about"); + server.route::("/users/{id}"); + server.route::("/posts/{slug}"); + server.route::("/files/{path:.*}"); let route_matcher: RouteMatcher = server.get_route_matcher().clone(); assert_eq!(route_matcher.get_static_route().len(), 2); assert!(route_matcher.get_dynamic_route().contains_key(&2)); @@ -104,7 +119,7 @@ fn large_dynamic_routes() { let start_insert: Instant = Instant::now(); for i in 0..ROUTE_COUNT { let path: String = format!("/api/resource{i}/{{id}}"); - server.route::(&path); + server.route::(&path); } let insert_duration: Duration = start_insert.elapsed(); println!( @@ -137,7 +152,7 @@ fn large_regex_routes() { let start_insert: Instant = Instant::now(); for i in 0..ROUTE_COUNT { let path: String = format!("/api/resource{i}/{{id:[0-9]+}}"); - server.route::(&path); + server.route::(&path); } let insert_duration: Duration = start_insert.elapsed(); println!( @@ -170,7 +185,7 @@ fn large_tail_regex_routes() { let start_insert: Instant = Instant::now(); for i in 0..ROUTE_COUNT { let path: String = format!("/api/resource{i}/{{path:.*}}"); - server.route::(&path); + server.route::(&path); } let insert_duration: Duration = start_insert.elapsed(); println!( diff --git a/core/tests/server/fn.rs b/core/tests/server/fn.rs index 72e7d23..2e7274f 100644 --- a/core/tests/server/fn.rs +++ b/core/tests/server/fn.rs @@ -146,7 +146,7 @@ fn server_send_sync() { #[tokio::test] async fn server_clone_across_threads() { let mut server: Server = Server::default(); - server.route::("/test"); + server.route::("/test"); let server_clone: Server = server.clone(); let handle: JoinHandle<&'static str> = spawn(async move { let _server_in_thread: Server = server_clone; @@ -159,7 +159,7 @@ async fn server_clone_across_threads() { #[tokio::test] async fn server_share_across_threads() { let mut server: Server = Server::default(); - server.route::("/test"); + server.route::("/test"); let server: Arc = Arc::new(server); let server1: Arc = server.clone(); let server2: Arc = server.clone(); @@ -190,12 +190,12 @@ async fn main() { server.request_middleware::(); server.request_middleware::(); server.response_middleware::(); - server.route::("/"); - server.route::("/sse"); - server.route::("/websocket"); - server.route::("/get/all/routes"); - server.route::("/dynamic/{routing}"); - server.route::("/regex/{file:^.*$}"); + server.route::("/"); + server.route::("/sse"); + server.route::("/websocket"); + server.route::("/get/all/routes"); + server.route::("/dynamic/{routing}"); + server.route::("/regex/{file:^.*$}"); let _: Result<(), Server> = SERVER_REF.set(server.clone()); let server_control_hook_1: ServerControlHook = server.run().await.unwrap_or_default(); let server_control_hook_2: ServerControlHook = server_control_hook_1.clone(); @@ -328,7 +328,7 @@ async fn client_handle_hook_dispatches_to_correct_handler_list() { #[tokio::test] async fn client_route_basic_serial_e2e() { let (control, port) = start_server_with(|register: &mut Server| { - register.route::("/"); + register.route::("/"); }) .await; let request: &[u8] = b"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"; @@ -340,7 +340,7 @@ async fn client_route_basic_serial_e2e() { #[tokio::test] async fn client_dynamic_route_serial_e2e() { let (control, port) = start_server_with(|register: &mut Server| { - register.route::("/dynamic/:id"); + register.route::("/dynamic/:id"); }) .await; let request: &[u8] = @@ -353,7 +353,7 @@ async fn client_dynamic_route_serial_e2e() { #[tokio::test] async fn client_regex_route_serial_e2e() { let (control, port) = start_server_with(|register: &mut Server| { - register.route::("/ws"); + register.route::("/ws"); }) .await; let request: &[u8] = b"GET /ws HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"; @@ -365,7 +365,7 @@ async fn client_regex_route_serial_e2e() { #[tokio::test] async fn client_request_error_404_serial_e2e() { let (control, port) = start_server_with(|register: &mut Server| { - register.route::("/"); + register.route::("/"); register.request_error::(); }) .await; @@ -378,7 +378,7 @@ async fn client_request_error_404_serial_e2e() { #[tokio::test] async fn client_request_middleware_serial_e2e() { let (control, port) = start_server_with(|register: &mut Server| { - register.route::("/"); + register.route::("/"); register.request_middleware::(); }) .await; @@ -391,7 +391,7 @@ async fn client_request_middleware_serial_e2e() { #[tokio::test] async fn client_response_middleware_serial_e2e() { let (control, port) = start_server_with(|register: &mut Server| { - register.route::("/"); + register.route::("/"); register.response_middleware::(); }) .await; @@ -404,7 +404,7 @@ async fn client_response_middleware_serial_e2e() { #[tokio::test] async fn client_task_panic_handler_serial_e2e() { let (control, port) = start_server_with(|register: &mut Server| { - register.route::("/panic/:msg"); + register.route::("/panic/:msg"); register.task_panic::(); }) .await; @@ -418,7 +418,7 @@ async fn client_task_panic_handler_serial_e2e() { #[tokio::test] async fn client_two_servers_on_distinct_ports_serial_e2e() { let (control_a, port_a) = start_server_with(|register: &mut Server| { - register.route::("/"); + register.route::("/"); }) .await; let request: &[u8] = b"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"; @@ -426,7 +426,7 @@ async fn client_two_servers_on_distinct_ports_serial_e2e() { control_a.shutdown().await; control_a.wait().await; let (control_b, port_b) = start_server_with(|register: &mut Server| { - register.route::("/"); + register.route::("/"); }) .await; assert_ne!(port_a, port_b); @@ -438,11 +438,11 @@ async fn client_two_servers_on_distinct_ports_serial_e2e() { #[tokio::test] async fn client_concurrent_servers_on_distinct_ports_e2e() { let (control_a, port_a) = start_server_with(|register: &mut Server| { - register.route::("/"); + register.route::("/"); }) .await; let (control_b, port_b) = start_server_with(|register: &mut Server| { - register.route::("/"); + register.route::("/"); }) .await; assert_ne!(port_a, port_b); diff --git a/plugin/websocket/tests/websocket/fn.rs b/plugin/websocket/tests/websocket/fn.rs index 41e1333..482eaa9 100644 --- a/plugin/websocket/tests/websocket/fn.rs +++ b/plugin/websocket/tests/websocket/fn.rs @@ -9,8 +9,8 @@ async fn main() { server.request_error::(); server.request_middleware::(); server.request_middleware::(); - server.route::("/{group_name}"); - server.route::("/{my_name}/{your_name}"); + server.route::("/{group_name}"); + server.route::("/{my_name}/{your_name}"); let server_control_hook_1: ServerControlHook = server.run().await.unwrap_or_default(); let server_control_hook_2: ServerControlHook = server_control_hook_1.clone(); spawn(async move {