Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ __pycache__/
benchmarks/results/
benchmarks/nitro/.nitro/
benchmarks/nitro/.output/
.claude/
3 changes: 1 addition & 2 deletions cookie.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion examples/route/main.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand Down
2 changes: 1 addition & 1 deletion moon.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "oboard/mocket"

version = "0.9.0"
version = "0.9.1"

import {
"moonbitlang/async@0.21.0",
Expand Down
3 changes: 3 additions & 0 deletions pkg.generated.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
30 changes: 30 additions & 0 deletions request.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
17 changes: 17 additions & 0 deletions request_conformance.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 0 additions & 1 deletion static.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
24 changes: 24 additions & 0 deletions utils.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -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..<len {
if query_string[i] == '&' {
let part = query_string[start:i]
parse_kv(@utf8.encode(part)[:], res)
start = i + 1
}
}
if start < len {
parse_kv(@utf8.encode(query_string[start:len])[:], res)
}
res
}

///|
fn parse_kv(part : BytesView, map : Map[String, String]) -> Unit {
let len = part.length()
Expand Down