From 6bed693df5b5fbd76883d27242557a86d90260a8 Mon Sep 17 00:00:00 2001 From: oboard Date: Sat, 22 Aug 2026 16:05:37 +0800 Subject: [PATCH 1/2] feat: add HttpRequest::query() parsing and tidy legacy API remnants - Add parse_query and HttpRequest::query() to expose URL-decoded query parameters, matching the ergonomics of event.params.get for path params (example: GET /search?q=moon&page=2 -> { "q": "moon", "page": "2" }). - Simplify get_cookie to a single case-insensitive "Cookie" lookup now that headers are case-insensitive. - Drop the stale `noraise` annotation in the routes example and obsolete header-type comment in static.mbt. - Ignore the local .claude/ worktree cache. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 1 + cookie.mbt | 3 +-- examples/route/main.mbt | 2 +- request.mbt | 30 ++++++++++++++++++++++++++++++ request_conformance.mbt | 17 +++++++++++++++++ static.mbt | 1 - utils.mbt | 24 ++++++++++++++++++++++++ 7 files changed, 74 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 33a7e61..dcd98bc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ __pycache__/ benchmarks/results/ benchmarks/nitro/.nitro/ benchmarks/nitro/.output/ +.claude/ diff --git a/cookie.mbt b/cookie.mbt index 4683921..b99efc1 100644 --- a/cookie.mbt +++ b/cookie.mbt @@ -28,10 +28,9 @@ pub fn HttpRequest::get_cookie( self : HttpRequest, name : String, ) -> CookieItem? { + // 请求头现在大小写不敏感,`Cookie` 一次查找即可。 if self.headers.get("Cookie") is Some(cookie) { parse_cookie(cookie).get(name) - } else if self.headers.get("cookie") is Some(cookie) { - parse_cookie(cookie).get(name) } else { None } diff --git a/examples/route/main.mbt b/examples/route/main.mbt index ecaab3d..7d390a7 100644 --- a/examples/route/main.mbt +++ b/examples/route/main.mbt @@ -34,7 +34,7 @@ async fn main { }) // Async Response - ..get("/async_data", fn(_event) noraise { + ..get("/async_data", _event => { ({ "name": "John Doe", "age": 30, "city": "New York" } : Json) }) diff --git a/request.mbt b/request.mbt index b51d24e..42de13b 100644 --- a/request.mbt +++ b/request.mbt @@ -27,6 +27,13 @@ pub fn[T : FromJson] HttpRequest::json(self : HttpRequest) -> T raise { @json.from_json(self.body()) } +///| +// 返回 URL 解码后的查询参数键值对。例如 `GET /search?q=moon&page=2` +// 得到 `{ "q": "moon", "page": "2" }`。 +pub fn HttpRequest::query(self : HttpRequest) -> Map[String, String] { + parse_query(self.query) +} + ///| pub impl BodyReader for String with fn from_request(req : HttpRequest) -> String raise { let bytes = req.raw_body @@ -87,3 +94,26 @@ test "read_body" { ) json_inspect(json, content={ "Hello": "World!" }) } + +///| +test "query_parsing" { + let req = HttpRequest::{ + http_method: "GET", + url: "/search", + query: "q=moon&page=2&tag=hello+world", + headers: Map([]), + raw_body: b"", + } + let map = req.query() + @test.assert_eq(map.get("q").unwrap_or(""), "moon") + @test.assert_eq(map.get("page").unwrap_or(""), "2") + @test.assert_eq(map.get("tag").unwrap_or(""), "hello world") + let empty = HttpRequest::{ + http_method: "GET", + url: "/plain", + query: "", + headers: Map([]), + raw_body: b"", + } + @test.assert_eq(empty.query().length(), 0) +} diff --git a/request_conformance.mbt b/request_conformance.mbt index 7d16331..cf6109f 100644 --- a/request_conformance.mbt +++ b/request_conformance.mbt @@ -51,6 +51,23 @@ async test "route with no query string yields an empty query" { @test.assert_eq(captured, [""]) } +///| +async test "query parameters are URL-decoded and exposed via HttpRequest::query" { + let app = new() + let captured : Array[String] = [] + app.get("/search", event => { + let q = event.req.query() + captured.push(q.get("q").unwrap_or("")) + captured.push(q.get("page").unwrap_or("")) + captured.push(q.get("tag").unwrap_or("")) + "ok" + }) + ignore( + dispatch_http(app, "GET", "/search?q=moon&page=2&tag=hello+world", {}, b""), + ) + @test.assert_eq(captured, ["moon", "2", "hello world"]) +} + ///| async test "PUT and PATCH bodies are delivered to the handler" { let app = new() diff --git a/static.mbt b/static.mbt index 52605c5..bca29b3 100644 --- a/static.mbt +++ b/static.mbt @@ -139,7 +139,6 @@ pub fn Mocket::static_assets( ), ) // Parse Accept-Encoding - // Headers are Map[StringView, StringView] let accept_encoding = event.req.headers.get("Accept-Encoding").unwrap_or("") let encodings = provider.get_encodings() let matched_encodings = [] diff --git a/utils.mbt b/utils.mbt index 3a1572f..acfb5fc 100644 --- a/utils.mbt +++ b/utils.mbt @@ -72,6 +72,30 @@ pub fn parse_form_data(bytes : BytesView) -> Map[String, String] { res } +///| +// 解析 URL 查询字符串(不含 `?`)成键值对,键值会做 URL 解码。 +// 与 parse_form_data 共享相同的 `&`/`=` 分割逻辑。 +pub fn parse_query(query_string : StringView) -> Map[String, String] { + let res = Map([]) + if query_string.length() == 0 { + return res + } + // Split by '&' + let mut start = 0 + let len = query_string.length() + for i in 0.. Unit { let len = part.length() From 23c4311fe6e73a4c9cbede13443d9fd804adc8d2 Mon Sep 17 00:00:00 2001 From: oboard Date: Sat, 22 Aug 2026 16:19:43 +0800 Subject: [PATCH 2/2] chore: release v0.9.1 Bump version to 0.9.1 and regenerate package metadata (mbti) for the new parse_query / HttpRequest::query symbols. Co-Authored-By: Claude Opus 4.8 (1M context) --- moon.mod | 2 +- pkg.generated.mbti | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/moon.mod b/moon.mod index 87c9599..c2ad7e6 100644 --- a/moon.mod +++ b/moon.mod @@ -1,6 +1,6 @@ name = "oboard/mocket" -version = "0.9.0" +version = "0.9.1" import { "moonbitlang/async@0.21.0", diff --git a/pkg.generated.mbti b/pkg.generated.mbti index fa81e64..59106c7 100644 --- a/pkg.generated.mbti +++ b/pkg.generated.mbti @@ -38,6 +38,8 @@ pub fn parse_form_data(BytesView) -> Map[String, String] pub fn parse_multipart(BytesView, String) -> Map[String, MultipartFormValue] +pub fn parse_query(StringView) -> Map[String, String] + pub fn register_ws_connection(String, (String) -> Unit, (Bytes) -> Unit, () -> Unit) -> Unit pub fn register_ws_handler(Mocket, Int) -> Unit @@ -99,6 +101,7 @@ pub(all) struct HttpRequest { pub fn[T : BodyReader] HttpRequest::body(Self) -> T raise pub fn HttpRequest::get_cookie(Self, String) -> CookieItem? pub fn[T : @json.FromJson] HttpRequest::json(Self) -> T raise +pub fn HttpRequest::query(Self) -> Map[String, String] pub impl Responder for HttpRequest pub(all) struct HttpResponse {