diff --git a/path_match.mbt b/path_match.mbt index 6cf1dc3..a320dc2 100644 --- a/path_match.mbt +++ b/path_match.mbt @@ -34,7 +34,17 @@ fn match_path_segments( // 路径结束但模板还有 if path_idx >= path_parts.length() { let template_part = template_parts[template_idx] - return if template_part == "**" { Some(params) } else { None } + if template_part == "**" { + params.set("_", "") + return match_path_segments( + template_parts, + path_parts, + template_idx + 1, + path_idx, + params, + ) + } + return None } // 都有内容,继续匹配 @@ -63,15 +73,29 @@ fn match_path_segments( params, ) } else if template_part == "**" { - // 多级通配符 - 贪婪匹配剩余所有 - let remaining_path = [] - let mut i = path_idx - while i < path_parts.length() { - remaining_path.push(path_parts[i]) - i = i + 1 + for consumed = path_idx + consumed <= path_parts.length() + consumed = consumed + 1 { + let remaining : Array[StringView] = [] + for j = path_idx; j < consumed; j = j + 1 { + remaining.push(path_parts[j]) + } + let trial : Map[String, StringView] = Map([]) + params.each(fn(k, v) { trial.set(k, v) }) + trial.set("_", remaining.join("/")) + match + match_path_segments( + template_parts, + path_parts, + template_idx + 1, + consumed, + trial, + ) { + Some(result) => return Some(result) + None => continue + } } - params.set("_", remaining_path.join("/")) - Some(params) + None } else if template_part == path_part { // 静态段匹配 match_path_segments( @@ -351,17 +375,26 @@ fn Mocket::insert_dynamic_route( trie } } - trie.insert(path, handler, order) + let parts = path.split("/").to_array() + let mut deep_wild_mid = false + for i = 0; i < parts.length(); i = i + 1 { + if parts[i] == "**" && i + 1 < parts.length() { + deep_wild_mid = true + } + } + if !deep_wild_mid { + trie.insert(path, handler, order) + } } ///| fn DynamicRouteTrieNode::find_path( self : DynamicRouteTrieNode, path : String, -) -> (HttpHandler, Map[String, StringView])? { +) -> (HttpHandler, Map[String, StringView], Int)? { let path_parts = path.split("/").collect() match self.find(path_parts, 0, {}) { - Some(found) => Some((found.handler, found.params)) + Some(found) => Some((found.handler, found.params, found.order)) None => None } } @@ -393,17 +426,31 @@ fn Mocket::find_route( None => ignore(()) } - // 然后尝试动态路由 trie - if self.dynamic_route_tries.get(http_method) is Some(trie) { - if trie.find_path(path) is Some(found) { - return Some(found) + // 动态路由:按方法优先级(method-specific 优先于 wildcard), + // 每组内取 trie 和线性中 order 最小的匹配 + for meth in [http_method, "*"] { + let mut best_order = -1 + let mut best_result : (HttpHandler, Map[String, StringView])? = None + if self.dynamic_route_tries.get(meth) is Some(trie) { + if trie.find_path(path) is Some((handler, params, order)) { + best_order = order + best_result = Some((handler, params)) + } } - } - - // 最后检查通配符方法的动态路由 trie - if self.dynamic_route_tries.get("*") is Some(trie) { - if trie.find_path(path) is Some(found) { - return Some(found) + if self.dynamic_routes.get(meth) is Some(routes) { + for i = 0; i < routes.length(); i = i + 1 { + if best_order >= 0 && i >= best_order { + break + } + let (template, handler) = routes[i] + if match_path(template, path) is Some(params) { + best_order = i + best_result = Some((handler, params)) + } + } + } + if best_result is Some(_) { + return best_result } } None @@ -500,6 +547,41 @@ test "边界情况" { @test.assert_eq(result5, None) } +///| +test "** with trailing segments" { + // ** mid-pattern must check suffix + @test.assert_eq( + match_path("/admin/**/settings", "/admin/x/settings"), + Some({ "_": "x" }), + ) + @test.assert_eq( + match_path("/admin/**/settings", "/admin/x/y/settings"), + Some({ "_": "x/y" }), + ) + // path exhausted before suffix — must NOT match + @test.assert_eq(match_path("/admin/**/settings", "/admin"), None) + @test.assert_eq(match_path("/admin/**/settings", "/admin/x"), None) + // ** at end still works when path is exhausted + @test.assert_eq(match_path("/files/**", "/files"), Some({ "_": "" })) +} + +///| +test "** mid-pattern preserves registration order" { + let app = new() + let noop : HttpHandler = fn(_) noraise { text("") } + app.get("/admin/**/settings", noop) + app.get("/admin/:id/settings", noop) + let result = app.find_route("GET", "/admin/x/settings") + // first-registered ** route should win; its params use "_" not "id" + match result { + Some((_, params)) => { + @test.assert_eq(params.get("_"), Some("x")) + @test.assert_eq(params.get("id"), None) + } + None => @test.fail("expected a match") + } +} + ///| test "性能对比场景" { // 静态路径应该快速返回