From cfbd04f7806f6f2302d1519e24f831357045784a Mon Sep 17 00:00:00 2001 From: oboard Date: Mon, 24 Nov 2025 03:24:26 +0800 Subject: [PATCH 1/5] feat: Introduce `Responder` trait for flexible response handling, simplify request body access, and add static file serving. --- src/body_reader.mbt | 92 ++++++------ src/cors/cors.mbt | 2 +- src/examples/cookie/main.mbt | 4 +- src/examples/route/main.mbt | 22 +-- src/examples/websocket/websocket_echo.mbt | 9 +- src/handler.mbt | 2 +- src/index.mbt | 42 +----- src/middleware.mbt | 12 +- src/mocket.js.mbt | 41 ++--- src/moon.pkg.json | 3 +- src/responder.mbt | 108 ++++++++++++++ src/response.mbt | 53 +++++++ src/static.mbt | 173 ++++++++++++++++++++++ src/static_file/moon.pkg.json | 3 + src/static_file/static_file.mbt | 67 +++++++++ src/websocket.mbt | 2 +- 16 files changed, 492 insertions(+), 143 deletions(-) create mode 100644 src/responder.mbt create mode 100644 src/response.mbt create mode 100644 src/static.mbt create mode 100644 src/static_file/moon.pkg.json create mode 100644 src/static_file/static_file.mbt diff --git a/src/body_reader.mbt b/src/body_reader.mbt index e689c78..41b1e24 100644 --- a/src/body_reader.mbt +++ b/src/body_reader.mbt @@ -1,50 +1,50 @@ -///| -pub suberror BodyError { - InvalidJsonCharset - InvalidJson - InvalidText -} +// ///| +// pub suberror BodyError { +// InvalidJsonCharset +// InvalidJson +// InvalidText +// } -///| -fn read_body( - req_headers : Map[StringView, StringView], - body_bytes : BytesView, -) -> HttpBody raise BodyError { - let content_type = req_headers.get("Content-Type") - if content_type is Some(content_type) { - let content_type = parse_content_type(content_type) - if content_type is Some(content_type) { - return match content_type { - { subtype: "json", .. } => { - let json = @encoding/utf8.decode(body_bytes) catch { - _ => raise BodyError::InvalidJsonCharset - } - Json(@json.parse(json) catch { _ => raise BodyError::InvalidJson }) - } - { media_type: "text", .. } => - Text( - @encoding/utf8.decode(body_bytes) catch { - _ => raise BodyError::InvalidText - }, - ) - { subtype: "x-www-form-urlencoded", .. } => - Form(parse_form_data(body_bytes)) - { subtype: "form-data" | "multipart", params, .. } => { - let boundary = match params.get("boundary") { - Some(b) => Some(b) - None => params.get("BOUNDARY") - } - match boundary { - Some(b) => Multipart(parse_multipart(body_bytes, b.to_string())) - None => Bytes(body_bytes) - } - } - _ => Bytes(body_bytes) - } - } - } - Bytes(body_bytes) -} +// ///| +// fn read_body( +// req_headers : Map[StringView, StringView], +// body_bytes : BytesView, +// ) -> &HttpBody raise BodyError { +// let content_type = req_headers.get("Content-Type") +// if content_type is Some(content_type) { +// let content_type = parse_content_type(content_type) +// if content_type is Some(content_type) { +// return match content_type { +// { subtype: "json", .. } => { +// let json = @encoding/utf8.decode(body_bytes) catch { +// _ => raise BodyError::InvalidJsonCharset +// } +// @json.parse(json) catch { +// _ => raise BodyError::InvalidJson +// } +// } +// { media_type: "text", .. } => +// @encoding/utf8.decode(body_bytes) catch { +// _ => raise BodyError::InvalidText +// } +// { subtype: "x-www-form-urlencoded", .. } => +// Form(parse_form_data(body_bytes)) +// { subtype: "form-data" | "multipart", params, .. } => { +// let boundary = match params.get("boundary") { +// Some(b) => Some(b) +// None => params.get("BOUNDARY") +// } +// match boundary { +// Some(b) => Multipart(parse_multipart(body_bytes, b.to_string())) +// None => Bytes(body_bytes) +// } +// } +// _ => Bytes(body_bytes) +// } +// } +// } +// Bytes(body_bytes) +// } ///| priv struct ContentType { diff --git a/src/cors/cors.mbt b/src/cors/cors.mbt index 584321e..1f67793 100644 --- a/src/cors/cors.mbt +++ b/src/cors/cors.mbt @@ -66,7 +66,7 @@ pub fn handle_cors( max_age~, ) // 对于预检请求,直接返回空响应,不调用next() - @mocket.Empty + @mocket.HttpResponse::ok().to_responder() } else { append_cors_headers( event, diff --git a/src/examples/cookie/main.mbt b/src/examples/cookie/main.mbt index 3e07efa..4c70f5f 100644 --- a/src/examples/cookie/main.mbt +++ b/src/examples/cookie/main.mbt @@ -4,9 +4,9 @@ fn main { app.get("/", e => { e.res.set_cookie("session_id", "12345", max_age=3600) if e.req.get_cookie("session_id") is Some(session_id) { - Text(session_id.value) + session_id.value } else { - Text("No session_id") + "No session_id" } }) diff --git a/src/examples/route/main.mbt b/src/examples/route/main.mbt index ee9d666..7e15a94 100644 --- a/src/examples/route/main.mbt +++ b/src/examples/route/main.mbt @@ -11,10 +11,10 @@ fn main { ..use_middleware(@cors.handle_cors()) // Text Response - ..get("/", _event => Text("⚡️ Tadaa!")) + ..get("/", _event => "⚡️ Tadaa!") // Hello World - ..on("GET", "/hello", _ => Text("Hello world!")) + ..on("GET", "/hello", _ => "Hello world!") ..group("/api", group => { // 添加组级中间件 group.use_middleware((event, next) => { @@ -23,8 +23,8 @@ fn main { ) next() }) - group.get("/hello", _ => Text("Hello world!")) - group.get("/json", _ => Json({ + group.get("/hello", _ => "Hello world!") + group.get("/json", _ => @mocket.json({ "name": "John Doe", "age": 30, "city": "New York", @@ -32,7 +32,7 @@ fn main { }) // JSON Response - ..get("/json", _event => Json({ + ..get("/json", _event => @mocket.json({ "name": "John Doe", "age": 30, "city": "New York", @@ -40,35 +40,35 @@ fn main { // Async Response ..get("/async_data", async fn(_event) noraise { - Json({ "name": "John Doe", "age": 30, "city": "New York" }) + @mocket.json({ "name": "John Doe", "age": 30, "city": "New York" }) }) // Dynamic Routes // /hello2/World = Hello, World! ..get("/hello/:name", event => { let name = event.params.get("name").unwrap_or("World") - Text("Hello, \{name}!") + "Hello, \{name}!" }) // /hello2/World = Hello, World! ..get("/hello2/*", event => { let name = event.params.get("_").unwrap_or("World") - Text("Hello, \{name}!") + "Hello, \{name}!" }) // Wildcard Routes // /hello3/World/World = Hello, World/World! ..get("/hello3/**", event => { let name = event.params.get("_").unwrap_or("World") - Text("Hello, \{name}!") + "Hello, \{name}!" }) // Echo Server - ..post("/echo", e => e.req.body) + ..post("/echo", e => e.req) // 404 Page ..get("/404", e => { e.res.status_code = 404 - HTML( + @mocket.html( ( #| #| diff --git a/src/examples/websocket/websocket_echo.mbt b/src/examples/websocket/websocket_echo.mbt index ec8eb2d..d0cd8aa 100644 --- a/src/examples/websocket/websocket_echo.mbt +++ b/src/examples/websocket/websocket_echo.mbt @@ -4,10 +4,11 @@ fn main { app.ws("/ws", event => match event { Open(peer) => println("WS open: " + peer.to_string()) Message(peer, body) => { - let msg = match body { - Text(s) => s.to_string() - _ => "" - } + // let msg = match body { + // Text(s) => s.to_string() + // _ => "" + // } + let msg = body.to_string() println("WS message: " + msg) peer.send(msg) } diff --git a/src/handler.mbt b/src/handler.mbt index e052d39..7718114 100644 --- a/src/handler.mbt +++ b/src/handler.mbt @@ -1,2 +1,2 @@ ///| -pub type HttpHandler = async (MocketEvent) -> HttpBody noraise +pub type HttpHandler = async (MocketEvent) -> &Responder noraise diff --git a/src/index.mbt b/src/index.mbt index f112110..c1f8332 100644 --- a/src/index.mbt +++ b/src/index.mbt @@ -5,38 +5,6 @@ pub(all) struct MultipartFormValue { data : BytesView } -///| -pub(all) enum HttpBody { - Json(Json) - Text(StringView) - HTML(StringView) - Bytes(BytesView) - Form(Map[String, String]) - Multipart(Map[String, MultipartFormValue]) - Empty -} - -///| -pub fn HttpBody::content_type( - self : HttpBody, - boundary? : String = "", -) -> String { - match self { - Bytes(_) => "application/octet-stream" - HTML(_) => "text/html; charset=utf-8" - Text(_) => "text/plain; charset=utf-8" - Json(_) => "application/json; charset=utf-8" - Form(_) => "application/x-www-form-urlencoded; charset=utf-8" - Multipart(_) => - if boundary == "" { - "multipart/form-data" - } else { - "multipart/form-data; boundary=" + boundary - } - Empty => "" - } -} - ///| #alias(T) pub(all) struct Mocket { @@ -82,7 +50,7 @@ pub(all) struct HttpRequest { http_method : String url : String headers : Map[StringView, StringView] - mut body : HttpBody + mut raw_body : Bytes } ///| @@ -151,14 +119,6 @@ pub impl Show for CookieItem with to_string(self : CookieItem) -> String { buf.to_string() } -///| -pub(all) struct HttpResponse { - mut status_code : Int - headers : Map[StringView, StringView] - cookies : Map[String, CookieItem] - // body : Body -} - ///| pub fn Mocket::on( self : Mocket, diff --git a/src/middleware.mbt b/src/middleware.mbt index e164e2d..598c9e2 100644 --- a/src/middleware.mbt +++ b/src/middleware.mbt @@ -1,6 +1,6 @@ ///| // 中间件类型:接受 HttpEvent 和 next 函数,返回 HttpBody -pub type Middleware = async (MocketEvent, async () -> HttpBody noraise) -> HttpBody noraise +pub type Middleware = async (MocketEvent, async () -> &Responder noraise) -> &Responder noraise ///| // 注册中间件,支持路径匹配 @@ -20,17 +20,17 @@ pub async fn execute_middlewares( middlewares : Array[(String, Middleware)], event : MocketEvent, final_handler : HttpHandler, -) -> HttpBody noraise { +) -> &Responder noraise { // 过滤出匹配路径的中间件 let matched_middlewares = [] - for i = 0; i < middlewares.length(); i = i + 1 { - let (base_path, middleware) = middlewares[i] + middlewares.each(middleware => { + let (base_path, middleware) = middleware // 如果 base_path 为空字符串,则为全局中间件 // 否则检查请求路径是否匹配 base_path if base_path == "" || event.req.url.has_prefix(base_path) { matched_middlewares.push(middleware) } - } + }) // 递归构建中间件链(洋葱模型) execute_middleware_chain(matched_middlewares, 0, event, final_handler) @@ -43,7 +43,7 @@ async fn execute_middleware_chain( index : Int, event : MocketEvent, final_handler : HttpHandler, -) -> HttpBody noraise { +) -> &Responder noraise { if index >= middlewares.length() { // 所有中间件都执行完毕,调用最终处理器 final_handler(event) diff --git a/src/mocket.js.mbt b/src/mocket.js.mbt index eb3c224..1e31be2 100644 --- a/src/mocket.js.mbt +++ b/src/mocket.js.mbt @@ -197,7 +197,7 @@ pub extern "js" fn create_server( #| } #| // Text #| if (frame.opcode === 0x1) { - #| const msg = (frame.data || Buffer.alloc(0)).toString('utf8'); + #| const msg = frame.data || Buffer.alloc(0); #| // console.log('[ws-bridge] emit message', port, connectionId, msg); #| if (typeof globalThis.__ws_emit_port === 'function') { #| globalThis.__ws_emit_port('message', port, connectionId, msg); @@ -256,10 +256,10 @@ pub fn serve_ffi(mocket : Mocket, port~ : Int) -> Unit { req: { http_method: req.req_method(), url: req.url(), - body: Empty, headers: string_headers, + raw_body: "", }, - res: { status_code: 200, headers: {}, cookies: {} }, + res: HttpResponse::new(200), params, } async_run(() => { @@ -271,21 +271,12 @@ pub fn serve_ffi(mocket : Mocket, port~ : Int) -> Unit { req.on("end", _ => res(buffer.to_string())) })) |> ignore - event.req.body = read_body(string_headers, buffer.to_bytes()) catch { - _ => { - res.write_head(400, @js.Object::new().to_value()) - res.end(@js.Value::cast_from("Invalid body")) - return - } - } + event.req.raw_body = buffer.to_bytes() } // 执行中间件链和处理器 let body = execute_middlewares(mocket.middlewares, event, handler) - let boundary = "----------------moonbit-" + port.to_string() - if not(body is Empty) { - event.res.headers.set("Content-Type", body.content_type(boundary~)) - } + // let boundary = "----------------moonbit-" + port.to_string() res.write_head( event.res.status_code, { @@ -302,17 +293,9 @@ pub fn serve_ffi(mocket : Mocket, port~ : Int) -> Unit { headers_obj }, ) - res.end( - match body { - Bytes(b) => @js.Value::cast_from(b.to_bytes()) - HTML(s) => @js.Value::cast_from(s.to_string()) - Text(s) => @js.Value::cast_from(s.to_string()) - Json(j) => @js.Value::cast_from(j.stringify()) - Form(f) => @js.Value::cast_from(form_encode(f)) - Multipart(m) => @js.Value::cast_from(encode_multipart(m, boundary)) - Empty => @js.Value::cast_from("") - }, - ) + let buf = @buffer.new() + body.output(buf) + res.end(@js.Value::cast_from(buf.to_bytes())) }) }, port, @@ -355,7 +338,7 @@ pub fn __ws_emit_js_port( event_type : String, port : Int, connection_id : String, - payload : String, + payload : Bytes, ) -> Unit { let handler = match ws_handler_map.get(port) { Some(h) => h @@ -382,7 +365,7 @@ pub fn __ws_emit_js_port( let peer = WebSocketPeer::{ connection_id, subscribed_channels: [] } match event_type { "open" => handler(WebSocketEvent::Open(peer)) - "message" => handler(WebSocketEvent::Message(peer, Text(payload))) + "message" => handler(WebSocketEvent::Message(peer, payload)) "close" => handler(WebSocketEvent::Close(peer)) _ => () } @@ -391,7 +374,7 @@ pub fn __ws_emit_js_port( ///| /// 导出 MoonBit 函数到 JS 全局 pub extern "js" fn __ws_emit_js_export( - cb : (String, Int, String, String) -> Unit, + cb : (String, Int, String, Bytes) -> Unit, ) -> Unit = "(cb) => { globalThis.__ws_emit_port = (type, port, id, payload) => { try { cb(type, port, id, payload); } catch (_) {} }; }" ///| @@ -478,7 +461,7 @@ pub extern "js" fn ws_unsubscribe(id : String, channel : String) -> Unit = "(id, pub extern "js" fn ws_publish(channel : String, msg : String) -> Unit = "(ch, msg) => { const ids = globalThis.__ws_get_members_js ? globalThis.__ws_get_members_js(ch) : []; const bindings = globalThis.ws_port_bindings && globalThis.ws_port_bindings.values().next().value; if (bindings && bindings.send) { for (const id of ids) { bindings.send(id, msg); } } }" ///| -fn encode_multipart( +pub fn encode_multipart( m : Map[String, MultipartFormValue], boundary : String, ) -> String { diff --git a/src/moon.pkg.json b/src/moon.pkg.json index 94de9c4..89bf1c6 100644 --- a/src/moon.pkg.json +++ b/src/moon.pkg.json @@ -2,7 +2,8 @@ "import": [ "oboard/mocket/js", "illusory0x0/native", - "tonyfettes/uri" + "tonyfettes/uri", + "moonbitlang/x/fs" ], "expose": [ "__ws_emit_js", diff --git a/src/responder.mbt b/src/responder.mbt new file mode 100644 index 0000000..4a8bc02 --- /dev/null +++ b/src/responder.mbt @@ -0,0 +1,108 @@ +///| +pub(open) trait Responder { + options(Self, res : HttpResponse) -> Unit + output(Self, buf : @buffer.Buffer) -> Unit +} + +///| +pub impl Responder for HttpRequest with options(self, res) -> Unit { + res.status_code = 200 + res.headers.merge_in_place(self.headers) +} + +///| +pub impl Responder for HttpRequest with output(self, buf) -> Unit { + buf.write_bytes(self.raw_body) +} + +///| +pub impl Responder for HttpResponse with options(self, res) -> Unit { + res.status_code = self.status_code + res.headers.merge_in_place(self.headers) +} + +///| +pub impl Responder for HttpResponse with output(self, buf) -> Unit { + buf.write_bytes(self.raw_body) +} + +///| +pub impl Responder for Json with options(_, res) -> Unit { + res.status_code = 200 + res.headers["Content-Type"] = "application/json; charset=utf-8" +} + +///| +pub impl Responder for Json with output(self, buf) -> Unit { + buf.write_string(self.stringify()) +} + +///| +pub impl Responder for Bytes with options(_, res) -> Unit { + res.status_code = 200 + res.headers["Content-Type"] = "application/octet-stream" +} + +///| +pub impl Responder for Bytes with output(self, buf) -> Unit { + buf.write_bytes(self) +} + +///| +pub impl Responder for String with options(_, res) -> Unit { + res.status_code = 200 + res.headers["Content-Type"] = "text/plain; charset=utf-8" +} + +///| +pub impl Responder for String with output(self, buf) -> Unit { + buf.write_string(self) +} + +///| +pub impl Responder for StringView with options(_, res) -> Unit { + res.status_code = 200 + res.headers["Content-Type"] = "text/plain; charset=utf-8" +} + +///| +pub impl Responder for StringView with output(self, buf) -> Unit { + buf.write_string(self.to_string()) +} + +///| +struct Html(StringView) + +///| +type Empty + +///| +pub impl Responder for Html with options(_, res) -> Unit { + res.status_code = 200 + res.headers["Content-Type"] = "text/html; charset=utf-8" +} + +///| +pub impl Responder for Html with output(self, buf) -> Unit { + buf.write_string(self.0.to_string()) +} + +///| +pub fn json(json : Json) -> &Responder { + json +} + +///| +pub fn html(html : &Show) -> &Responder { + Html(html.to_string()) +} + +///| +pub fn text(text : &Show) -> &Responder { + text.to_string() +} + +///| +pub fn bytes(bytes : Bytes) -> &Responder { + bytes +} diff --git a/src/response.mbt b/src/response.mbt new file mode 100644 index 0000000..2390e6d --- /dev/null +++ b/src/response.mbt @@ -0,0 +1,53 @@ +///| +pub(all) struct HttpResponse { + mut status_code : Int + headers : Map[StringView, StringView] + cookies : Map[String, CookieItem] + mut raw_body : Bytes +} + +///| +pub fn HttpResponse::new( + status_code : Int, + headers? : Map[StringView, StringView], + cookies? : Map[String, CookieItem], + raw_body? : Bytes, +) -> HttpResponse { + HttpResponse::{ + status_code, + headers: headers.unwrap_or({}), + cookies: cookies.unwrap_or({}), + raw_body: raw_body.unwrap_or(""), + } +} + +///| +pub fn HttpResponse::ok() -> HttpResponse { + HttpResponse::new(200) +} + +///| +pub fn HttpResponse::not_found() -> HttpResponse { + HttpResponse::new(404) +} + +///| +pub fn HttpResponse::not_modified() -> HttpResponse { + HttpResponse::new(304) +} + +///| +pub fn HttpResponse::body( + self : HttpResponse, + body : &Responder, +) -> HttpResponse { + let buf = @buffer.new() + body.output(buf) + self.raw_body = buf.to_bytes() + self +} + +///| +pub fn HttpResponse::to_responder(self : HttpResponse) -> &Responder { + self +} diff --git a/src/static.mbt b/src/static.mbt new file mode 100644 index 0000000..1403e9a --- /dev/null +++ b/src/static.mbt @@ -0,0 +1,173 @@ +///| +pub(all) struct StaticAssetMeta { + asset_type : String? + etag : String? + mtime : Int64? + path : String? + size : Int64? + encoding : String? +} + +///| +pub(open) trait ServeStaticProvider { + // This function should resolve asset meta + get_meta(Self, id : String) -> StaticAssetMeta? + // This function should resolve asset content + get_contents(Self, id : String) -> &Responder + // Custom MIME type resolver function + get_type(Self, ext : String) -> String? + // Encodings map + get_encodings(Self) -> Map[String, String] + // Index names + get_index_names(Self) -> Array[String] + // Fallthrough + get_fallthrough(Self) -> Bool +} + +///| +pub fn Mocket::static_assets( + self : Mocket, + path : String, + options : &ServeStaticProvider, +) -> Unit { + self.use_middleware(async fn(event, next) noraise { + if not(event.req.url.has_prefix(path)) { + return next() + } + + // Method check + if event.req.http_method != "GET" && event.req.http_method != "HEAD" { + if options.get_fallthrough() { + return next() + } + event.res.headers.set("Allow", "GET, HEAD") + return HttpResponse::new(405) + } + let original_id = event.req.url[path.length():].to_string() catch { + _ => return next() + } + + // Parse Accept-Encoding + // Headers are Map[StringView, StringView] + let accept_encoding = event.req.headers + .get("Accept-Encoding") + .map(fn(v) { v.to_string() }) + .unwrap_or("") + let encodings = options.get_encodings() + let matched_encodings = [] + if accept_encoding != "" { + // split requires `chars` label + for pair in accept_encoding.split(",") { + let encoding = pair.trim(chars=" ").to_string() + match encodings.get(encoding) { + Some(mapped) => matched_encodings.push(mapped) + None => () + } + } + } + if matched_encodings.length() > 1 { + event.res.headers.set("Vary", "Accept-Encoding") + } + + // Search paths + let mut id = original_id + let mut meta : StaticAssetMeta? = None + let index_names = options.get_index_names() + if index_names.length() == 0 { + ignore(index_names.push("/index.html")) + } + + // Search logic: suffix -> encoding + let mut found = false + let suffixes = [""] + suffixes.append(index_names) + let try_encodings = matched_encodings.copy() + try_encodings.push("") // Add empty encoding (identity) + for suffix in suffixes { + if found { + break + } + for encoding in try_encodings { + let try_id = id + suffix + encoding + match options.get_meta(try_id) { + Some(m) => { + meta = Some(m) + id = try_id + found = true + break + } + None => () + } + } + } + match meta { + None => { + if options.get_fallthrough() { + return next() + } + return HttpResponse::not_found() + } + Some(meta) => { + // Handle caching + match meta.mtime { + Some(_mtime) => + // TODO: Date parsing/comparison is tricky without a library. + // For now, we just set Last-Modified. + // event.res.headers.set("Last-Modified", ... ) + () + None => () + } + match meta.etag { + Some(etag) => { + if not(event.res.headers.contains("ETag")) { + event.res.headers.set("ETag", etag) + } + if event.req.headers.get("If-None-Match") == Some(etag) { + return HttpResponse::not_modified() + } + } + None => () + } + + // Content-Type + if not(event.res.headers.contains("Content-Type")) { + match meta.asset_type { + Some(t) => event.res.headers.set("Content-Type", t) + None => { + // Simple extension extraction + let parts = id.split(".").collect() + if parts.length() > 1 { + // TODO: get extension + () + } + } + } + } + + // Content-Encoding + match meta.encoding { + Some(enc) => + if not(event.res.headers.contains("Content-Encoding")) { + event.res.headers.set("Content-Encoding", enc) + } + None => () + } + + // Content-Length + match meta.size { + Some(size) => + if size > 0L && not(event.res.headers.contains("Content-Length")) { + event.res.headers.set("Content-Length", size.to_string()) + } + None => () + } + if event.req.http_method == "HEAD" { + return HttpResponse::ok() + } + let contents = options.get_contents(id) + event.res.status_code = 200 + contents + } + } + }) +} diff --git a/src/static_file/moon.pkg.json b/src/static_file/moon.pkg.json new file mode 100644 index 0000000..1465224 --- /dev/null +++ b/src/static_file/moon.pkg.json @@ -0,0 +1,3 @@ +{ + "import": ["oboard/mocket", "moonbitlang/x/fs"] +} diff --git a/src/static_file/static_file.mbt b/src/static_file/static_file.mbt new file mode 100644 index 0000000..759e0b4 --- /dev/null +++ b/src/static_file/static_file.mbt @@ -0,0 +1,67 @@ +// // ///| + +// ///| +// using @mocket {trait ServeStaticProvider, trait Responder} + +// ///| +// pub struct StaticFileProvider { +// path : String +// } + +// ///| +// pub fn StaticFileProvider::new(path : String) -> StaticFileProvider { +// { path, } +// } + +// ///| +// pub impl ServeStaticProvider for StaticFileProvider with get_meta( +// self, +// id : String, +// ) -> StaticAssetMeta? { +// None +// } + +// ///| +// pub impl ServeStaticProvider for StaticFileProvider with get_contents( +// self, +// id : String, +// ) -> &Responder { +// let res : &Responder = @mocket.bytes( +// @fs.read_file_to_bytes(self.path + "/" + id), +// ) catch { +// _ => @mocket.HttpResponse::not_found() +// } +// res +// } +// // Custom MIME type resolver function + +// // ///| +// // impl ServeStaticProvider for StaticFileProvider with get_type( +// // self, +// // ext : String, +// // ) -> String? { + +// // } +// // // Encodings map + +// // ///| +// // impl ServeStaticProvider for StaticFileProvider with get_encodings(self) -> Map[ +// // String, +// // String, +// // ] { + +// // } +// // // Index names + +// // ///| +// // impl ServeStaticProvider for StaticFileProvider with get_index_names(self) -> Array[ +// // String, +// // ] { + +// // } +// // // Fallthrough + +// // ///| +// // impl ServeStaticProvider for StaticFileProvider with get_fallthrough(self) -> Bool { + +// // } diff --git a/src/websocket.mbt b/src/websocket.mbt index c7616eb..a3d7475 100644 --- a/src/websocket.mbt +++ b/src/websocket.mbt @@ -52,7 +52,7 @@ pub fn WebSocketPeer::to_string(self : WebSocketPeer) -> String { ///| pub enum WebSocketEvent { Open(WebSocketPeer) - Message(WebSocketPeer, HttpBody) + Message(WebSocketPeer, Bytes) Close(WebSocketPeer) } From d73a7c36842f5450ea0601b53eb9555bfcfd5d10 Mon Sep 17 00:00:00 2001 From: oboard Date: Tue, 25 Nov 2025 18:14:08 +0800 Subject: [PATCH 2/5] feat: enhance WebSocket support and improve HTTP response handling - Add binary message support and ping/pong handling for WebSockets - Refactor HTTP response system to use status code enums and traits - Implement BodyReader trait for request body parsing - Remove logger system and optimize route matching - Add static file serving with proper content type handling - Improve WebSocket API with text/binary/pong methods --- src/body_reader.mbt | 109 -------- src/content_type.mbt | 61 ++++ src/cors/cors.mbt | 2 +- src/cors/pkg.generated.mbti | 2 +- src/examples/responder/main.mbt | 41 +++ src/examples/responder/moon.pkg.json | 4 + src/examples/responder/pkg.generated.mbti | 15 + src/examples/route/main.mbt | 42 +-- src/examples/route/pkg.generated.mbti | 5 + src/examples/static_assets/main.mbt | 30 ++ src/examples/static_assets/moon.pkg.json | 7 + src/examples/static_assets/pkg.generated.mbti | 18 ++ src/examples/websocket/websocket_echo.mbt | 26 +- src/index.mbt | 29 +- src/js/pkg.generated.mbti | 194 +++++++++++++ src/logger.mbt | 162 ----------- src/mocket.js.mbt | 114 +++++--- src/mocket.native.mbt | 99 ++++--- src/mocket.stub.c | 54 +++- src/path_match.mbt | 17 +- src/pkg.generated.mbti | 264 ++++++++++++------ src/request.mbt | 65 +++++ src/responder.mbt | 46 ++- src/response.mbt | 24 +- src/static.mbt | 30 +- src/static_file/pkg.generated.mbti | 22 ++ src/static_file/static_file.mbt | 126 ++++----- src/status_code.mbt | 212 ++++++++++++++ src/websocket.mbt | 23 +- test_ws.js | 6 + 30 files changed, 1228 insertions(+), 621 deletions(-) delete mode 100644 src/body_reader.mbt create mode 100644 src/content_type.mbt create mode 100644 src/examples/responder/main.mbt create mode 100644 src/examples/responder/moon.pkg.json create mode 100644 src/examples/responder/pkg.generated.mbti create mode 100644 src/examples/static_assets/main.mbt create mode 100644 src/examples/static_assets/moon.pkg.json create mode 100644 src/examples/static_assets/pkg.generated.mbti delete mode 100644 src/logger.mbt create mode 100644 src/request.mbt create mode 100644 src/static_file/pkg.generated.mbti create mode 100644 src/status_code.mbt diff --git a/src/body_reader.mbt b/src/body_reader.mbt deleted file mode 100644 index 41b1e24..0000000 --- a/src/body_reader.mbt +++ /dev/null @@ -1,109 +0,0 @@ -// ///| -// pub suberror BodyError { -// InvalidJsonCharset -// InvalidJson -// InvalidText -// } - -// ///| -// fn read_body( -// req_headers : Map[StringView, StringView], -// body_bytes : BytesView, -// ) -> &HttpBody raise BodyError { -// let content_type = req_headers.get("Content-Type") -// if content_type is Some(content_type) { -// let content_type = parse_content_type(content_type) -// if content_type is Some(content_type) { -// return match content_type { -// { subtype: "json", .. } => { -// let json = @encoding/utf8.decode(body_bytes) catch { -// _ => raise BodyError::InvalidJsonCharset -// } -// @json.parse(json) catch { -// _ => raise BodyError::InvalidJson -// } -// } -// { media_type: "text", .. } => -// @encoding/utf8.decode(body_bytes) catch { -// _ => raise BodyError::InvalidText -// } -// { subtype: "x-www-form-urlencoded", .. } => -// Form(parse_form_data(body_bytes)) -// { subtype: "form-data" | "multipart", params, .. } => { -// let boundary = match params.get("boundary") { -// Some(b) => Some(b) -// None => params.get("BOUNDARY") -// } -// match boundary { -// Some(b) => Multipart(parse_multipart(body_bytes, b.to_string())) -// None => Bytes(body_bytes) -// } -// } -// _ => Bytes(body_bytes) -// } -// } -// } -// Bytes(body_bytes) -// } - -///| -priv struct ContentType { - media_type : StringView - subtype : StringView - params : Map[StringView, StringView] -} derive(Show) - -///| -fn parse_content_type(s : StringView) -> ContentType? { - let parts = s.split(";").to_array() - if parts.is_empty() { - None - } else { - let main_part_str = parts[0].trim_space() - let media_type_parts = main_part_str.split("/").to_array() - if media_type_parts.length() != 2 { - None - } else { - let media_type = media_type_parts[0].trim_space() - let subtype = media_type_parts[1].trim_space() - let params = {} - for i in 1.. { - let key = param_part[0:idx].trim_space() - let value = param_part[idx + 1:].trim_space() - params[key] = value - } - None => () // Ignore malformed parameters - } - } catch { - _ => () - } - } - Some({ media_type, subtype, params }) - } - } -} - -///| -test "parse_content_type" { - inspect( - parse_content_type("application/json; charset=utf-8"), - content=( - #|Some({media_type: "application", subtype: "json", params: {"charset": "utf-8"}}) - ), - ) -} - -///| -test "parse_form_data" { - inspect( - parse_form_data(b"name=John+Doe&age=30"), - content=( - #|{"name": "John Doe", "age": "30"} - ), - ) -} diff --git a/src/content_type.mbt b/src/content_type.mbt new file mode 100644 index 0000000..0c3479b --- /dev/null +++ b/src/content_type.mbt @@ -0,0 +1,61 @@ +///| +priv struct ContentType { + media_type : StringView + subtype : StringView + params : Map[StringView, StringView] +} derive(Show) + +///| +fn parse_content_type(s : StringView) -> ContentType? { + let parts = s.split(";").to_array() + if parts.is_empty() { + None + } else { + let main_part_str = parts[0].trim_space() + let media_type_parts = main_part_str.split("/").to_array() + if media_type_parts.length() != 2 { + None + } else { + let media_type = media_type_parts[0].trim_space() + let subtype = media_type_parts[1].trim_space() + let params = {} + for i in 1.. { + let key = param_part[0:idx].trim_space() + let value = param_part[idx + 1:].trim_space() + params[key] = value + } + None => () // Ignore malformed parameters + } + } catch { + _ => () + } + } + Some({ media_type, subtype, params }) + } + } +} + +///| +test "parse_content_type" { + inspect( + parse_content_type("application/json; charset=utf-8"), + content=( + #|Some({media_type: "application", subtype: "json", params: {"charset": "utf-8"}}) + ), + ) +} + +///| +test "parse_form_data" { + inspect( + parse_form_data(b"name=John+Doe&age=30"), + content=( + #|{"name": "John Doe", "age": "30"} + ), + ) +} diff --git a/src/cors/cors.mbt b/src/cors/cors.mbt index 1f67793..e8d09ec 100644 --- a/src/cors/cors.mbt +++ b/src/cors/cors.mbt @@ -66,7 +66,7 @@ pub fn handle_cors( max_age~, ) // 对于预检请求,直接返回空响应,不调用next() - @mocket.HttpResponse::ok().to_responder() + @mocket.HttpResponse::new(@mocket.OK).to_responder() } else { append_cors_headers( event, diff --git a/src/cors/pkg.generated.mbti b/src/cors/pkg.generated.mbti index 8b0d2bf..3c11881 100644 --- a/src/cors/pkg.generated.mbti +++ b/src/cors/pkg.generated.mbti @@ -10,7 +10,7 @@ fn append_cors_headers(@mocket.MocketEvent, origin? : String, methods? : String, fn append_cors_preflight_headers(@mocket.MocketEvent, origin? : String, methods? : String, allow_headers? : String, credentials? : Bool, max_age? : Int) -> Unit -fn handle_cors(origin? : String, methods? : String, allow_headers? : String, expose_headers? : String, credentials? : Bool, max_age? : Int) -> async (@mocket.MocketEvent, async () -> @mocket.HttpBody noraise) -> @mocket.HttpBody noraise +fn handle_cors(origin? : String, methods? : String, allow_headers? : String, expose_headers? : String, credentials? : Bool, max_age? : Int) -> async (@mocket.MocketEvent, async () -> &@mocket.Responder noraise) -> &@mocket.Responder noraise fn is_preflight_request(@mocket.MocketEvent) -> Bool diff --git a/src/examples/responder/main.mbt b/src/examples/responder/main.mbt new file mode 100644 index 0000000..dcc7b69 --- /dev/null +++ b/src/examples/responder/main.mbt @@ -0,0 +1,41 @@ +///| +struct Person { + name : String + age : Int +} derive(ToJson) + +///| +fn main { + let app = @mocket.new() + + // Text Response + app + ..get("/", _event => "⚡️ Tadaa!") + + // Object Response + ..get("/", _event => { name: "oboard", age: 21 }.to_json()) + + // Echo Server + ..post("/echo", e => e.req) + + // 404 Page + ..get("/404", _ => @mocket.HttpResponse::new(@mocket.NotFound).body( + @mocket.html( + ( + #| + #| + #|

404

+ #| + #| + ), + ), + )) + + // Print Server URL + for path in app.mappings.keys() { + println("\{path.0} http://localhost:4000\{path.1}") + } + + // Serve + app.serve(port=4000) +} diff --git a/src/examples/responder/moon.pkg.json b/src/examples/responder/moon.pkg.json new file mode 100644 index 0000000..c8c5197 --- /dev/null +++ b/src/examples/responder/moon.pkg.json @@ -0,0 +1,4 @@ +{ + "is-main": true, + "import": ["oboard/mocket"] +} diff --git a/src/examples/responder/pkg.generated.mbti b/src/examples/responder/pkg.generated.mbti new file mode 100644 index 0000000..683eaeb --- /dev/null +++ b/src/examples/responder/pkg.generated.mbti @@ -0,0 +1,15 @@ +// Generated using `moon info`, DON'T EDIT IT +package "oboard/mocket/examples/responder" + +// Values + +// Errors + +// Types and methods +type Person +impl ToJson for Person + +// Type aliases + +// Traits + diff --git a/src/examples/route/main.mbt b/src/examples/route/main.mbt index 7e15a94..ddf6eae 100644 --- a/src/examples/route/main.mbt +++ b/src/examples/route/main.mbt @@ -1,13 +1,10 @@ ///| fn main { - let app = @mocket.new(logger=@mocket.new_production_logger()) + let app = @mocket.new() // Register global middleware app - ..use_middleware((event, next) => { - println("📝 Request: \{event.req.http_method} \{event.req.url}") - next() - }) + ..use_middleware(logger_middleware) ..use_middleware(@cors.handle_cors()) // Text Response @@ -24,23 +21,17 @@ fn main { next() }) group.get("/hello", _ => "Hello world!") - group.get("/json", _ => @mocket.json({ - "name": "John Doe", - "age": 30, - "city": "New York", - })) + group.get("/json", _ => ( + { "name": "John Doe", "age": 30, "city": "New York" } : Json)) }) // JSON Response - ..get("/json", _event => @mocket.json({ - "name": "John Doe", - "age": 30, - "city": "New York", - })) + ..get("/json", _event => ( + { "name": "John Doe", "age": 30, "city": "New York" } : Json)) // Async Response ..get("/async_data", async fn(_event) noraise { - @mocket.json({ "name": "John Doe", "age": 30, "city": "New York" }) + ({ "name": "John Doe", "age": 30, "city": "New York" } : Json) }) // Dynamic Routes @@ -66,8 +57,7 @@ fn main { ..post("/echo", e => e.req) // 404 Page - ..get("/404", e => { - e.res.status_code = 404 + ..get("/404", _ => @mocket.HttpResponse::new(@mocket.NotFound).body( @mocket.html( ( #| @@ -76,8 +66,8 @@ fn main { #| #| ), - ) - }) + ), + )) // Print Server URL for path in app.mappings.keys() { @@ -87,3 +77,15 @@ fn main { // Serve app.serve(port=4000) } + +///| +pub async fn logger_middleware( + event : @mocket.MocketEvent, + next : async () -> &@mocket.Responder noraise, +) -> &@mocket.Responder noraise { + let start_time = @env.now() + let res = next() + let duration = @env.now() - start_time + println("\{event.req.http_method} \{event.req.url} - \{duration}ms") + res +} diff --git a/src/examples/route/pkg.generated.mbti b/src/examples/route/pkg.generated.mbti index 1799246..375d159 100644 --- a/src/examples/route/pkg.generated.mbti +++ b/src/examples/route/pkg.generated.mbti @@ -1,7 +1,12 @@ // Generated using `moon info`, DON'T EDIT IT package "oboard/mocket/examples/route" +import( + "oboard/mocket" +) + // Values +async fn logger_middleware(@mocket.MocketEvent, async () -> &@mocket.Responder noraise) -> &@mocket.Responder noraise // Errors diff --git a/src/examples/static_assets/main.mbt b/src/examples/static_assets/main.mbt new file mode 100644 index 0000000..6c10d08 --- /dev/null +++ b/src/examples/static_assets/main.mbt @@ -0,0 +1,30 @@ +///| +fn main { + let app = @mocket.new() + app.use_middleware(logger_middleware) + // Register global middleware + app.static_assets("/", @static_file.new("./")) + + // Text Response + app.get("/", _event => "⚡️ Tadaa!") + + // Print Server URL + for path in app.mappings.keys() { + println("\{path.0} http://localhost:4000\{path.1}") + } + + // Serve + app.serve(port=4000) +} + +///| +pub async fn logger_middleware( + event : @mocket.MocketEvent, + next : async () -> &@mocket.Responder noraise, +) -> &@mocket.Responder noraise { + let start_time = @env.now() + let res = next() + let duration = @env.now() - start_time + println("\{event.req.http_method} \{event.req.url} - \{duration}ms") + res +} diff --git a/src/examples/static_assets/moon.pkg.json b/src/examples/static_assets/moon.pkg.json new file mode 100644 index 0000000..17ad478 --- /dev/null +++ b/src/examples/static_assets/moon.pkg.json @@ -0,0 +1,7 @@ +{ + "is-main": true, + "import": [ + "oboard/mocket", + "oboard/mocket/static_file" + ] +} \ No newline at end of file diff --git a/src/examples/static_assets/pkg.generated.mbti b/src/examples/static_assets/pkg.generated.mbti new file mode 100644 index 0000000..f5f448c --- /dev/null +++ b/src/examples/static_assets/pkg.generated.mbti @@ -0,0 +1,18 @@ +// Generated using `moon info`, DON'T EDIT IT +package "oboard/mocket/examples/static_assets" + +import( + "oboard/mocket" +) + +// Values +async fn logger_middleware(@mocket.MocketEvent, async () -> &@mocket.Responder noraise) -> &@mocket.Responder noraise + +// Errors + +// Types and methods + +// Type aliases + +// Traits + diff --git a/src/examples/websocket/websocket_echo.mbt b/src/examples/websocket/websocket_echo.mbt index d0cd8aa..8cebc17 100644 --- a/src/examples/websocket/websocket_echo.mbt +++ b/src/examples/websocket/websocket_echo.mbt @@ -1,17 +1,23 @@ ///| fn main { - let app = @mocket.new(logger=@mocket.new_logger()) + let app = @mocket.new() app.ws("/ws", event => match event { Open(peer) => println("WS open: " + peer.to_string()) - Message(peer, body) => { - // let msg = match body { - // Text(s) => s.to_string() - // _ => "" - // } - let msg = body.to_string() - println("WS message: " + msg) - peer.send(msg) - } + Message(peer, msg) => + match msg { + Text(s) => { + println("WS message: " + s) + peer.text(s) + } + Binary(bytes) => { + println("WS binary: " + bytes.length().to_string() + " bytes") + peer.binary(bytes) + } + Ping => { + println("WS ping") + peer.pong() + } + } Close(peer) => println("WS close: " + peer.to_string()) }) println("WebSocket echo server listening on ws://localhost:8080/ws") diff --git a/src/index.mbt b/src/index.mbt index c1f8332..32da44e 100644 --- a/src/index.mbt +++ b/src/index.mbt @@ -15,8 +15,6 @@ pub(all) struct Mocket { static_routes : Map[String, Map[String, HttpHandler]] // 添加动态路由缓存(包含参数的路由) dynamic_routes : Map[String, Array[(String, HttpHandler)]] - // 日志记录器 - logger : Logger // WebSocket 路由(按路径匹配,不区分方法) ws_static_routes : Map[String, WebSocketHandler] ws_dynamic_routes : Array[(String, WebSocketHandler)] @@ -26,17 +24,13 @@ pub(all) struct Mocket { } ///| -pub fn new( - base_path? : String = "", - logger? : Logger = new_production_logger(), -) -> Mocket { +pub fn new(base_path? : String = "") -> Mocket { { base_path, mappings: {}, middlewares: [], static_routes: {}, dynamic_routes: {}, - logger, ws_static_routes: {}, ws_dynamic_routes: [], ws_clients: {}, @@ -45,14 +39,6 @@ pub fn new( } } -///| -pub(all) struct HttpRequest { - http_method : String - url : String - headers : Map[StringView, StringView] - mut raw_body : Bytes -} - ///| ///| @@ -127,30 +113,21 @@ pub fn Mocket::on( handler : HttpHandler, ) -> Unit { let path = self.base_path + path - self.logger.route_register(event, path) self.mappings.set((event, path), handler) // 优化:根据路径类型分别缓存 if path.find(":").unwrap_or(-1) == -1 && path.find("*").unwrap_or(-1) == -1 { // 静态路径,直接缓存 - self.logger.route_static(event, path) match self.static_routes.get(event) { - Some(http_methodroutes) => { - self.logger.route_merge_existing(event) - http_methodroutes.set(path, handler) - self.logger.route_added(path) - } + Some(http_methodroutes) => http_methodroutes.set(path, handler) None => { - self.logger.route_merge_new(event) let new_routes : Map[String, HttpHandler] = {} new_routes.set(path, handler) self.static_routes.set(event, new_routes) - self.logger.route_created(path) } } } else { // 动态路径,加入动态路由列表 - self.logger.route_dynamic(event, path) match self.dynamic_routes.get(event) { Some(routes) => routes.push((path, handler)) None => { @@ -246,7 +223,7 @@ pub fn Mocket::group( base_path : String, configure : (Mocket) -> Unit, ) -> Unit { - let group = new(base_path=self.base_path + base_path, logger=self.logger) + let group = new(base_path=self.base_path + base_path) configure(group) // 合并路由 group.mappings.iter().each(i => self.mappings.set(i.0, i.1)) diff --git a/src/js/pkg.generated.mbti b/src/js/pkg.generated.mbti index 499bd25..5506f59 100644 --- a/src/js/pkg.generated.mbti +++ b/src/js/pkg.generated.mbti @@ -1,13 +1,207 @@ // Generated using `moon info`, DON'T EDIT IT package "oboard/mocket/js" +import( + "moonbitlang/core/json" +) + // Values +async fn[T] async_all(Array[async () -> T]) -> Array[T] + +let async_iterator : Symbol + +fn async_run(async () -> Unit noraise) -> Unit + +fn async_test(async () -> Unit) -> Unit + +let globalThis : Value + +let iterator : Symbol + +fn require(String, keys? : Array[String]) -> Value + +fn[T, E : Error] spawn_detach(async () -> T raise E) -> Unit + +async fn[T, E : Error] suspend(((T) -> Unit, (E) -> Unit) -> Unit) -> T raise E // Errors +pub suberror Error_ Value +fn Error_::cause(Self) -> Value? +fn[T] Error_::wrap(() -> Value, map_ok? : (Value) -> T) -> T raise Self +impl Show for Error_ // Types and methods +type Nullable[_] +fn[T] Nullable::from_option(T?) -> Self[T] +fn[T] Nullable::get_exn(Self[T]) -> T +fn[T] Nullable::is_null(Self[T]) -> Bool +fn[T] Nullable::null() -> Self[T] +fn[T] Nullable::to_option(Self[T]) -> T? +fn[T] Nullable::unwrap(Self[T]) -> T + +pub struct Object(Value) +fn[K, V] Object::extend_iter(Self, Iter[(K, V)]) -> Unit +fn[K, V] Object::extend_iter2(Self, Iter2[K, V]) -> Unit +fn Object::extend_object(Self, Self) -> Self +fn[K, V] Object::from_iter(Iter[(K, V)]) -> Self +fn[K, V] Object::from_iter2(Iter2[K, V]) -> Self +fn Object::from_value(Value) -> Optional[Self] +fn Object::from_value_unchecked(Value) -> Self +#deprecated +fn Object::inner(Self) -> Value +fn Object::new() -> Self +fn[K, V] Object::op_get(Self, K) -> V +fn[K, V] Object::op_set(Self, K, V) -> Unit +fn Object::to_value(Self) -> Value + +type Optional[_] +fn[T] Optional::from_option(T?) -> Self[T] +fn[T] Optional::get_exn(Self[T]) -> T +fn[T] Optional::is_undefined(Self[T]) -> Bool +fn[T] Optional::to_option(Self[T]) -> T? +fn[T] Optional::undefined() -> Self[T] +fn[T] Optional::unwrap(Self[T]) -> T + +#external +pub type Promise +fn Promise::all(Array[Self]) -> Self +fn[T] Promise::unsafe_new(async () -> T) -> Self +async fn Promise::wait(Self) -> Value + +type Symbol +fn Symbol::make() -> Self +fn Symbol::make_with_number(Double) -> Self +fn Symbol::make_with_string(String) -> Self +fn Symbol::make_with_string_js(String) -> Self + +type Union2[_, _] +fn[A : Cast, B] Union2::from0(A) -> Self[A, B] +fn[A, B : Cast] Union2::from1(B) -> Self[A, B] +fn[A : Cast, B] Union2::to0(Self[A, B]) -> A? +fn[A, B : Cast] Union2::to1(Self[A, B]) -> B? + +type Union3[_, _, _] +fn[A : Cast, B, C] Union3::from0(A) -> Self[A, B, C] +fn[A, B : Cast, C] Union3::from1(B) -> Self[A, B, C] +fn[A, B, C : Cast] Union3::from2(C) -> Self[A, B, C] +fn[A : Cast, B, C] Union3::to0(Self[A, B, C]) -> A? +fn[A, B : Cast, C] Union3::to1(Self[A, B, C]) -> B? +fn[A, B, C : Cast] Union3::to2(Self[A, B, C]) -> C? + +type Union4[_, _, _, _] +fn[A : Cast, B, C, D] Union4::from0(A) -> Self[A, B, C, D] +fn[A, B : Cast, C, D] Union4::from1(B) -> Self[A, B, C, D] +fn[A, B, C : Cast, D] Union4::from2(C) -> Self[A, B, C, D] +fn[A, B, C, D : Cast] Union4::from3(D) -> Self[A, B, C, D] +fn[A : Cast, B, C, D] Union4::to0(Self[A, B, C, D]) -> A? +fn[A, B : Cast, C, D] Union4::to1(Self[A, B, C, D]) -> B? +fn[A, B, C : Cast, D] Union4::to2(Self[A, B, C, D]) -> C? +fn[A, B, C, D : Cast] Union4::to3(Self[A, B, C, D]) -> D? + +type Union5[_, _, _, _, _] +fn[A : Cast, B, C, D, E] Union5::from0(A) -> Self[A, B, C, D, E] +fn[A, B : Cast, C, D, E] Union5::from1(B) -> Self[A, B, C, D, E] +fn[A, B, C : Cast, D, E] Union5::from2(C) -> Self[A, B, C, D, E] +fn[A, B, C, D : Cast, E] Union5::from3(D) -> Self[A, B, C, D, E] +fn[A, B, C, D, E : Cast] Union5::from4(E) -> Self[A, B, C, D, E] +fn[A : Cast, B, C, D, E] Union5::to0(Self[A, B, C, D, E]) -> A? +fn[A, B : Cast, C, D, E] Union5::to1(Self[A, B, C, D, E]) -> B? +fn[A, B, C : Cast, D, E] Union5::to2(Self[A, B, C, D, E]) -> C? +fn[A, B, C, D : Cast, E] Union5::to3(Self[A, B, C, D, E]) -> D? +fn[A, B, C, D, E : Cast] Union5::to4(Self[A, B, C, D, E]) -> E? + +type Union6[_, _, _, _, _, _] +fn[A : Cast, B, C, D, E, F] Union6::from0(A) -> Self[A, B, C, D, E, F] +fn[A, B : Cast, C, D, E, F] Union6::from1(B) -> Self[A, B, C, D, E, F] +fn[A, B, C : Cast, D, E, F] Union6::from2(C) -> Self[A, B, C, D, E, F] +fn[A, B, C, D : Cast, E, F] Union6::from3(D) -> Self[A, B, C, D, E, F] +fn[A, B, C, D, E : Cast, F] Union6::from4(E) -> Self[A, B, C, D, E, F] +fn[A, B, C, D, E, F : Cast] Union6::from5(F) -> Self[A, B, C, D, E, F] +fn[A : Cast, B, C, D, E, F] Union6::to0(Self[A, B, C, D, E, F]) -> A? +fn[A, B : Cast, C, D, E, F] Union6::to1(Self[A, B, C, D, E, F]) -> B? +fn[A, B, C : Cast, D, E, F] Union6::to2(Self[A, B, C, D, E, F]) -> C? +fn[A, B, C, D : Cast, E, F] Union6::to3(Self[A, B, C, D, E, F]) -> D? +fn[A, B, C, D, E : Cast, F] Union6::to4(Self[A, B, C, D, E, F]) -> E? +fn[A, B, C, D, E, F : Cast] Union6::to5(Self[A, B, C, D, E, F]) -> F? + +type Union7[_, _, _, _, _, _, _] +fn[A : Cast, B, C, D, E, F, G] Union7::from0(A) -> Self[A, B, C, D, E, F, G] +fn[A, B : Cast, C, D, E, F, G] Union7::from1(B) -> Self[A, B, C, D, E, F, G] +fn[A, B, C : Cast, D, E, F, G] Union7::from2(C) -> Self[A, B, C, D, E, F, G] +fn[A, B, C, D : Cast, E, F, G] Union7::from3(D) -> Self[A, B, C, D, E, F, G] +fn[A, B, C, D, E : Cast, F, G] Union7::from4(E) -> Self[A, B, C, D, E, F, G] +fn[A, B, C, D, E, F : Cast, G] Union7::from5(F) -> Self[A, B, C, D, E, F, G] +fn[A, B, C, D, E, F, G : Cast] Union7::from6(G) -> Self[A, B, C, D, E, F, G] +fn[A : Cast, B, C, D, E, F, G] Union7::to0(Self[A, B, C, D, E, F, G]) -> A? +fn[A, B : Cast, C, D, E, F, G] Union7::to1(Self[A, B, C, D, E, F, G]) -> B? +fn[A, B, C : Cast, D, E, F, G] Union7::to2(Self[A, B, C, D, E, F, G]) -> C? +fn[A, B, C, D : Cast, E, F, G] Union7::to3(Self[A, B, C, D, E, F, G]) -> D? +fn[A, B, C, D, E : Cast, F, G] Union7::to4(Self[A, B, C, D, E, F, G]) -> E? +fn[A, B, C, D, E, F : Cast, G] Union7::to5(Self[A, B, C, D, E, F, G]) -> F? +fn[A, B, C, D, E, F, G : Cast] Union7::to6(Self[A, B, C, D, E, F, G]) -> G? + +type Union8[_, _, _, _, _, _, _, _] +fn[A : Cast, B, C, D, E, F, G, H] Union8::from0(A) -> Self[A, B, C, D, E, F, G, H] +fn[A, B : Cast, C, D, E, F, G, H] Union8::from1(B) -> Self[A, B, C, D, E, F, G, H] +fn[A, B, C : Cast, D, E, F, G, H] Union8::from2(C) -> Self[A, B, C, D, E, F, G, H] +fn[A, B, C, D : Cast, E, F, G, H] Union8::from3(D) -> Self[A, B, C, D, E, F, G, H] +fn[A, B, C, D, E : Cast, F, G, H] Union8::from4(E) -> Self[A, B, C, D, E, F, G, H] +fn[A, B, C, D, E, F : Cast, G, H] Union8::from5(F) -> Self[A, B, C, D, E, F, G, H] +fn[A, B, C, D, E, F, G : Cast, H] Union8::from6(G) -> Self[A, B, C, D, E, F, G, H] +fn[A, B, C, D, E, F, G, H : Cast] Union8::from7(H) -> Self[A, B, C, D, E, F, G, H] +fn[A : Cast, B, C, D, E, F, G, H] Union8::to0(Self[A, B, C, D, E, F, G, H]) -> A? +fn[A, B : Cast, C, D, E, F, G, H] Union8::to1(Self[A, B, C, D, E, F, G, H]) -> B? +fn[A, B, C : Cast, D, E, F, G, H] Union8::to2(Self[A, B, C, D, E, F, G, H]) -> C? +fn[A, B, C, D : Cast, E, F, G, H] Union8::to3(Self[A, B, C, D, E, F, G, H]) -> D? +fn[A, B, C, D, E : Cast, F, G, H] Union8::to4(Self[A, B, C, D, E, F, G, H]) -> E? +fn[A, B, C, D, E, F : Cast, G, H] Union8::to5(Self[A, B, C, D, E, F, G, H]) -> F? +fn[A, B, C, D, E, F, G : Cast, H] Union8::to6(Self[A, B, C, D, E, F, G, H]) -> G? +fn[A, B, C, D, E, F, G, H : Cast] Union8::to7(Self[A, B, C, D, E, F, G, H]) -> H? + +#external +pub type Value +fn[Arg, Result] Value::apply(Self, Array[Arg]) -> Result +fn[Arg, Result] Value::apply_with_index(Self, Int, Array[Arg]) -> Result +fn[Arg, Result] Value::apply_with_string(Self, String, Array[Arg]) -> Result +fn[Arg, Result] Value::apply_with_symbol(Self, Symbol, Array[Arg]) -> Result +fn[T] Value::cast(Self) -> T +fn[T] Value::cast_from(T) -> Self +fn Value::extends(Self, Self) -> Self +fn Value::from_json(Json) -> Self raise +fn Value::from_json_string(String) -> Self raise +fn[T] Value::get_with_index(Self, Int) -> T +fn[T] Value::get_with_string(Self, String) -> T +fn[T] Value::get_with_symbol(Self, Symbol) -> T +fn Value::is_bool(Self) -> Bool +fn Value::is_null(Self) -> Bool +fn Value::is_number(Self) -> Bool +fn Value::is_object(Self) -> Bool +fn Value::is_string(Self) -> Bool +fn Value::is_symbol(Self) -> Bool +fn Value::is_undefined(Self) -> Bool +fn[Arg, Result] Value::new(Self, Array[Arg]) -> Result +fn[Arg, Result] Value::new_with_index(Self, Int, Array[Arg]) -> Result +fn[Arg, Result] Value::new_with_string(Self, String, Array[Arg]) -> Result +fn[Arg, Result] Value::new_with_symbol(Self, Symbol, Array[Arg]) -> Result +fn[T] Value::set_with_index(Self, Int, T) -> Unit +fn[T] Value::set_with_string(Self, String, T) -> Unit +fn[T] Value::set_with_symbol(Self, Symbol, T) -> Unit +fn Value::to_json(Self) -> Json raise +fn Value::to_json_string(Self) -> String raise +fn Value::to_string(Self) -> String +impl Show for Value +impl @json.FromJson for Value // Type aliases // Traits +pub(open) trait Cast { + into(Value) -> Self? + from(Self) -> Value +} +impl Cast for Bool +impl Cast for Int +impl Cast for Double +impl Cast for String +impl[A : Cast] Cast for Array[A] diff --git a/src/logger.mbt b/src/logger.mbt deleted file mode 100644 index 716ca2a..0000000 --- a/src/logger.mbt +++ /dev/null @@ -1,162 +0,0 @@ -///| -// Logger for Mocket framework -pub struct Logger { - enabled : Bool - level : LogLevel -} - -///| -pub enum LogLevel { - Debug - Info - Warn - Error -} - -///| -// 生产环境优化:完全禁用日志以获得零开销 -pub fn new_logger(enabled? : Bool = true, level? : LogLevel = Debug) -> Logger { - { enabled, level } -} - -///| -// 生产环境专用:零开销logger(编译时优化) -pub fn new_production_logger() -> Logger { - { enabled: false, level: Error } -} - -///| -// 开发环境专用:全功能logger -pub fn new_debug_logger() -> Logger { - { enabled: true, level: Debug } -} - -///| -// 零成本抽象:使用lazy evaluation避免不必要的字符串构造 -pub fn Logger::debug(self : Logger, message_fn : () -> String) -> Unit { - if self.enabled && self.level_allows(Debug) { - println("🐛 [DEBUG] \{message_fn()}") - } -} - -///| -pub fn Logger::info(self : Logger, message_fn : () -> String) -> Unit { - if self.enabled && self.level_allows(Info) { - println("ℹ️ [INFO] \{message_fn()}") - } -} - -///| -pub fn Logger::warn(self : Logger, message_fn : () -> String) -> Unit { - if self.enabled && self.level_allows(Warn) { - println("⚠️ [WARN] \{message_fn()}") - } -} - -///| -pub fn Logger::error(self : Logger, message_fn : () -> String) -> Unit { - if self.enabled && self.level_allows(Error) { - println("❌ [ERROR] \{message_fn()}") - } -} - -///| -// 便利方法:直接传字符串(性能稍差但使用简单) -pub fn Logger::debug_str(self : Logger, message : String) -> Unit { - if self.enabled && self.level_allows(Debug) { - println("🐛 [DEBUG] \{message}") - } -} - -///| -fn Logger::level_allows(self : Logger, target_level : LogLevel) -> Bool { - match (self.level, target_level) { - (Debug, _) => true - (Info, Info) | (Info, Warn) | (Info, Error) => true - (Warn, Warn) | (Warn, Error) => true - (Error, Error) => true - _ => false - } -} - -///| -// 路由相关的专用日志方法 - 零成本抽象版本 -pub fn Logger::route_register( - self : Logger, - http_method : String, - path : String, -) -> Unit { - self.debug(fn() { "🔧 Registering route: \{http_method} \{path}" }) -} - -///| -pub fn Logger::route_static( - self : Logger, - http_method : String, - path : String, -) -> Unit { - self.debug(fn() { "📌 Static route: \{http_method} \{path}" }) -} - -///| -pub fn Logger::route_dynamic( - self : Logger, - http_method : String, - path : String, -) -> Unit { - self.debug(fn() { "🎯 Dynamic route: \{http_method} \{path}" }) -} - -///| -pub fn Logger::route_lookup( - self : Logger, - http_method : String, - path : String, -) -> Unit { - self.debug(fn() { "🔍 Looking for route: \{http_method} \{path}" }) -} - -///| -pub fn Logger::route_found( - self : Logger, - http_method : String, - path : String, -) -> Unit { - self.debug(fn() { "✅ Found static route match!" }) - self.debug(fn() { "📝 Request: \{http_method} \{path}" }) -} - -///| -pub fn Logger::route_not_found(self : Logger, path : String) -> Unit { - self.debug(fn() { "❌ No static route match for \{path}" }) -} - -///| -pub fn Logger::routes_available(self : Logger, routes : Array[String]) -> Unit { - self.debug(fn() { "📋 Found method routes" }) - self.debug(fn() { "🗂️ Available static routes:" }) - routes.iter().each(fn(route) { self.debug(fn() { " - \{route}" }) }) -} - -///| -pub fn Logger::route_merge_existing( - self : Logger, - http_method : String, -) -> Unit { - self.debug(fn() { "📝 Adding to existing method routes for \{http_method}" }) -} - -///| -pub fn Logger::route_merge_new(self : Logger, http_method : String) -> Unit { - self.debug(fn() { "🆕 Creating new method routes for \{http_method}" }) -} - -///| -pub fn Logger::route_added(self : Logger, path : String) -> Unit { - self.debug(fn() { "✅ Added \{path} to existing routes" }) -} - -///| -pub fn Logger::route_created(self : Logger, path : String) -> Unit { - self.debug(fn() { "✅ Created new routes and added \{path}" }) -} diff --git a/src/mocket.js.mbt b/src/mocket.js.mbt index 1e31be2..8ffbade 100644 --- a/src/mocket.js.mbt +++ b/src/mocket.js.mbt @@ -81,20 +81,34 @@ pub extern "js" fn create_server( #| // 供 FFI 调用的发送函数 #| function sendText(socket, str) { #| const payload = Buffer.from(String(str), 'utf8'); + #| sendFrame(socket, payload, 0x1); + #| } + #| + #| function sendBinary(socket, bytes) { + #| const payload = Buffer.from(bytes); + #| sendFrame(socket, payload, 0x2); + #| } + #| + #| function sendPong(socket) { + #| const payload = Buffer.alloc(0); + #| sendFrame(socket, payload, 0xA); + #| } + #| + #| function sendFrame(socket, payload, opcode) { #| const len = payload.length; #| let header; #| if (len < 126) { #| header = Buffer.alloc(2); - #| header[0] = 0x81; // FIN + text + #| header[0] = 0x80 | opcode; // FIN + opcode #| header[1] = len; #| } else if (len < 65536) { #| header = Buffer.alloc(4); - #| header[0] = 0x81; + #| header[0] = 0x80 | opcode; #| header[1] = 126; #| header.writeUInt16BE(len, 2); #| } else { #| header = Buffer.alloc(10); - #| header[0] = 0x81; + #| header[0] = 0x80 | opcode; #| header[1] = 127; #| header.writeBigUInt64BE(BigInt(len), 2); #| } @@ -140,6 +154,14 @@ pub extern "js" fn create_server( #| send: (id, msg) => { #| const s = clientsById.get(id); #| if (s) sendText(s, msg); + #| }, + #| sendBytes: (id, bytes) => { + #| const s = clientsById.get(id); + #| if (s) sendBinary(s, bytes); + #| }, + #| pong: (id) => { + #| const s = clientsById.get(id); + #| if (s) sendPong(s); #| } #| }); #| @@ -183,37 +205,42 @@ pub extern "js" fn create_server( #| try { socket.end(); } catch (_) {} #| if (clientsById.delete(connectionId)) { #| if (typeof globalThis.__ws_emit_port === 'function') { - #| globalThis.__ws_emit_port('close', port, connectionId, ''); + #| globalThis.__ws_emit_port('close', port, connectionId, Buffer.alloc(0)); #| } #| } #| return; #| } - #| // Ping -> Pong - #| if (frame.opcode === 0x9) { - #| const payload = frame.data || Buffer.alloc(0); - #| const header = Buffer.from([0x8A, payload.length]); - #| try { socket.write(Buffer.concat([header, payload])); } catch (_) {} - #| return; - #| } - #| // Text - #| if (frame.opcode === 0x1) { - #| const msg = frame.data || Buffer.alloc(0); - #| // console.log('[ws-bridge] emit message', port, connectionId, msg); - #| if (typeof globalThis.__ws_emit_port === 'function') { - #| globalThis.__ws_emit_port('message', port, connectionId, msg); - #| } else { - #| // console.log('[ws-bridge] __ws_emit not found'); - #| } - #| } - #| }); - #| - #| socket.on('close', () => { - #| if (clientsById.delete(connectionId)) { - #| if (typeof globalThis.__ws_emit_port === 'function') { - #| globalThis.__ws_emit_port('close', port, connectionId, ''); - #| } - #| } - #| }); + #| // Ping -> Pong + #| if (frame.opcode === 0x9) { + #| // Emit ping event + #| if (typeof globalThis.__ws_emit_port === 'function') { + #| globalThis.__ws_emit_port('ping', port, connectionId, Buffer.alloc(0)); + #| } + #| return; + #| } + #| // Text + #| if (frame.opcode === 0x1) { + #| const msg = frame.data || Buffer.alloc(0); + #| if (typeof globalThis.__ws_emit_port === 'function') { + #| globalThis.__ws_emit_port('message', port, connectionId, msg); + #| } + #| } + #| // Binary + #| if (frame.opcode === 0x2) { + #| const msg = frame.data || Buffer.alloc(0); + #| if (typeof globalThis.__ws_emit_port === 'function') { + #| globalThis.__ws_emit_port('binary', port, connectionId, msg); + #| } + #| } + #| }); + #| + #| socket.on('close', () => { + #| if (clientsById.delete(connectionId)) { + #| if (typeof globalThis.__ws_emit_port === 'function') { + #| globalThis.__ws_emit_port('close', port, connectionId, Buffer.alloc(0)); + #| } + #| } + #| }); #| socket.on('error', () => { #| clientsById.delete(connectionId); #| try { socket.destroy(); } catch (_) {} @@ -222,7 +249,7 @@ pub extern "js" fn create_server( #| #| // console.log('[ws-bridge] WebSocket upgrade handler attached for port', port); #| }; - #| start(server, port); server.listen(port, () => {}) + #| start(server, port); server.listen(port, () => {}); #|} ///| @@ -259,7 +286,7 @@ pub fn serve_ffi(mocket : Mocket, port~ : Int) -> Unit { headers: string_headers, raw_body: "", }, - res: HttpResponse::new(200), + res: HttpResponse::new(OK), params, } async_run(() => { @@ -275,10 +302,11 @@ pub fn serve_ffi(mocket : Mocket, port~ : Int) -> Unit { } // 执行中间件链和处理器 - let body = execute_middlewares(mocket.middlewares, event, handler) + let responder = execute_middlewares(mocket.middlewares, event, handler) // let boundary = "----------------moonbit-" + port.to_string() + responder.options(event.res) res.write_head( - event.res.status_code, + event.res.status_code.to_int(), { let headers_obj = (try? @js.Value::from_json( event.res.headers.to_json(), @@ -294,8 +322,9 @@ pub fn serve_ffi(mocket : Mocket, port~ : Int) -> Unit { }, ) let buf = @buffer.new() - body.output(buf) - res.end(@js.Value::cast_from(buf.to_bytes())) + responder.output(buf) + event.res.raw_body = buf.to_bytes() + res.end(@js.Value::cast_from(event.res.raw_body)) }) }, port, @@ -365,7 +394,12 @@ pub fn __ws_emit_js_port( let peer = WebSocketPeer::{ connection_id, subscribed_channels: [] } match event_type { "open" => handler(WebSocketEvent::Open(peer)) - "message" => handler(WebSocketEvent::Message(peer, payload)) + "message" => { + let msg = @encoding/utf8.decode(payload) catch { _ => "" } + handler(WebSocketEvent::Message(peer, Text(msg))) + } + "binary" => handler(WebSocketEvent::Message(peer, Binary(payload))) + "ping" => handler(WebSocketEvent::Message(peer, Ping)) "close" => handler(WebSocketEvent::Close(peer)) _ => () } @@ -451,6 +485,12 @@ pub extern "js" fn __get_port_by_connection_js_export( /// 四个 FFI 包装,供 WebSocketPeer 调用 pub extern "js" fn ws_send(id : String, msg : String) -> Unit = "(id, msg) => { const port = globalThis.__get_port_by_connection(id); const bindings = globalThis.ws_port_bindings && globalThis.ws_port_bindings.get(port); if (bindings && bindings.send) bindings.send(id, msg); }" +///| +pub extern "js" fn ws_send_bytes(id : String, msg : Bytes) -> Unit = "(id, msg) => { const port = globalThis.__get_port_by_connection(id); const bindings = globalThis.ws_port_bindings && globalThis.ws_port_bindings.get(port); if (bindings && bindings.sendBytes) bindings.sendBytes(id, msg); }" + +///| +pub extern "js" fn ws_pong(id : String) -> Unit = "(id) => { const port = globalThis.__get_port_by_connection(id); const bindings = globalThis.ws_port_bindings && globalThis.ws_port_bindings.get(port); if (bindings && bindings.pong) bindings.pong(id); }" + ///| pub extern "js" fn ws_subscribe(id : String, channel : String) -> Unit = "(id, ch) => { if (globalThis.__ws_subscribe_js) globalThis.__ws_subscribe_js(id, ch); }" diff --git a/src/mocket.native.mbt b/src/mocket.native.mbt index 41b91f8..e9fc8cc 100644 --- a/src/mocket.native.mbt +++ b/src/mocket.native.mbt @@ -191,49 +191,31 @@ fn handle_request_native( } } let event = { - req: { http_method, url, body: Empty, headers: req_headers }, - res: { status_code: 200, headers: {}, cookies: {} }, + req: { http_method, url, raw_body: "", headers: req_headers }, + res: HttpResponse::new(OK), params, } if http_method == "POST" { let req_body_len = req.req_body_len() let body_bytes = req.body()[0:req_body_len] - let body = read_body(req_headers, body_bytes) catch { - _ => { - res.status(400) - res.end(to_cstr("Invalid body")) - return - } - } - event.req.body = body + event.req.raw_body = body_bytes.to_bytes() } async_run(async fn() noraise { // 执行中间件链和处理器 - let body = execute_middlewares(mocket.middlewares, event, handler) - if not(body is Empty) { - event.res.headers.set("Content-Type", body.content_type()) - } - res.status(event.res.status_code) - event.res.headers.each(fn(key, value) { - res.set_header(to_cstr(key), to_cstr(value)) - }) - event.res.cookies.each(fn(_, cookie) { - res.set_header(to_cstr("Set-Cookie"), to_cstr(cookie.to_string())) - }) - if body is Bytes(bytes) { - res.end_bytes(bytes.to_bytes()) - } else { - res.end( - match body { - HTML(s) => to_cstr(s.to_string()) - Text(s) => to_cstr(s.to_string()) - Json(j) => to_cstr(j.stringify()) - Form(m) => to_cstr(form_encode(m)) - Multipart(_) => to_cstr("") - _ => to_cstr("") - }, - ) - } + let responder = execute_middlewares(mocket.middlewares, event, handler) + responder.options(event.res) + res.status(event.res.status_code.to_int()) + event.res.headers.each((key, value) => res.set_header( + to_cstr(key), + to_cstr(value), + )) + event.res.cookies + .values() + .each(cookie => res.set_header( + to_cstr("Set-Cookie"), + to_cstr(cookie.to_string()), + )) + res.end_bytes(event.res.raw_body) }) } @@ -245,7 +227,6 @@ pub fn __ws_emit( ) -> Unit { let et = from_cstr(event_type) let cid = from_cstr(connection_id) - let pl = from_cstr(payload) let handler = if ws_handler_map.is_empty() { fn(_) { } } else { @@ -254,7 +235,19 @@ pub fn __ws_emit( let peer = WebSocketPeer::{ connection_id: cid, subscribed_channels: [] } match et { "open" => handler(WebSocketEvent::Open(peer)) - "message" => handler(WebSocketEvent::Message(peer, Text(pl))) + "message" => { + let pl = from_cstr(payload) + handler(WebSocketEvent::Message(peer, Text(pl))) + } + "binary" => { + let len = ws_msg_body_len() + let arr = Array::make(len, b'\x00') + let buf = Bytes::from_array(arr) + let copied = ws_msg_copy(buf) + let body = buf[0:copied].to_bytes() + handler(WebSocketEvent::Message(peer, Binary(body))) + } + "ping" => handler(WebSocketEvent::Message(peer, Ping)) "close" => handler(WebSocketEvent::Close(peer)) _ => () } @@ -285,11 +278,43 @@ extern "c" fn ws_publish_native( msg : @native.CStr, ) -> Unit = "ws_publish" +///| +#owned(id, msg) +extern "c" fn ws_send_bytes_len_native( + id : @native.CStr, + msg : Bytes, + len : Int, +) -> Unit = "ws_send_bytes_len" + +///| +#owned(id) +extern "c" fn ws_pong_native(id : @native.CStr) -> Unit = "ws_pong" + +///| +extern "c" fn ws_msg_body() -> Bytes = "ws_msg_body" + +///| +extern "c" fn ws_msg_body_len() -> Int = "ws_msg_body_len" + +///| +#owned(dst) +extern "c" fn ws_msg_copy(dst : Bytes) -> Int = "ws_msg_copy" + ///| pub fn ws_send(id : String, msg : String) -> Unit { ws_send_native(to_cstr(id), to_cstr(msg)) } +///| +pub fn ws_send_bytes(id : String, msg : Bytes) -> Unit { + ws_send_bytes_len_native(to_cstr(id), msg, msg.length()) +} + +///| +pub fn ws_pong(id : String) -> Unit { + ws_pong_native(to_cstr(id)) +} + ///| pub fn ws_subscribe(id : String, channel : String) -> Unit { ws_subscribe_native(to_cstr(id), to_cstr(channel)) diff --git a/src/mocket.stub.c b/src/mocket.stub.c index 50fe419..3bfac2e 100644 --- a/src/mocket.stub.c +++ b/src/mocket.stub.c @@ -23,6 +23,36 @@ static int WS_CLIENT_COUNT = 0; static channel_t CHANNELS[MAX_CHANNELS]; static int CHANNEL_COUNT = 0; +// Last received binary WS message buffer +static uint8_t *WS_LAST_MSG = NULL; +static size_t WS_LAST_MSG_LEN = 0; + +void ws_set_last_msg(const unsigned char *data, size_t len) { + if (WS_LAST_MSG) { + free(WS_LAST_MSG); + WS_LAST_MSG = NULL; + WS_LAST_MSG_LEN = 0; + } + if (data && len > 0) { + WS_LAST_MSG = (uint8_t *) malloc(len); + if (WS_LAST_MSG) { + memcpy(WS_LAST_MSG, data, len); + WS_LAST_MSG_LEN = len; + } + } +} + +uint8_t *ws_msg_body(void) { return WS_LAST_MSG; } +size_t ws_msg_body_len(void) { return WS_LAST_MSG_LEN; } + +size_t ws_msg_copy(uint8_t *dst, size_t max_len) { + if (!dst || WS_LAST_MSG_LEN == 0 || WS_LAST_MSG == NULL) return 0; + size_t n = WS_LAST_MSG_LEN; + if (n > max_len) n = max_len; + memcpy(dst, WS_LAST_MSG, n); + return n; +} + typedef void (*ws_emit_cb_t)(const char *type, const char *id, const char *payload); static ws_emit_cb_t WS_EMIT_CB = NULL; void set_ws_emit(ws_emit_cb_t cb) { WS_EMIT_CB = cb; } @@ -97,6 +127,22 @@ void ws_send(const char *id, const char *msg) { mg_ws_send(cl->c, msg, strlen(msg), WEBSOCKET_OP_TEXT); } +// Send binary WebSocket message with explicit length +void ws_send_bytes_len(const char *id, const uint8_t *msg, size_t msg_len) { + ws_client_t *cl = find_client_by_id(id); + if (!cl || !cl->c) return; + const char *data = msg ? (const char *) msg : ""; + size_t len = msg ? msg_len : 0; + mg_ws_send(cl->c, data, len, WEBSOCKET_OP_BINARY); +} + +// Send PONG frame +void ws_pong(const char *id) { + ws_client_t *cl = find_client_by_id(id); + if (!cl || !cl->c) return; + mg_ws_send(cl->c, "", 0, WEBSOCKET_OP_PONG); +} + void ws_subscribe(const char *id, const char *channel) { if (!id || !channel) return; channel_t *ch = get_channel(channel, 1); @@ -401,7 +447,8 @@ static void ev_handler(struct mg_connection *c, int ev, void *ev_data) struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; ws_client_t *cl = find_client_by_conn(c); if (!cl) return; - if ((wm->flags & WEBSOCKET_OP_TEXT) == WEBSOCKET_OP_TEXT) { + unsigned char op = wm->flags & 0x0F; + if (op == WEBSOCKET_OP_TEXT) { size_t len = wm->data.len; char *tmp = (char *) malloc(len + 1); if (!tmp) return; @@ -409,6 +456,11 @@ static void ev_handler(struct mg_connection *c, int ev, void *ev_data) tmp[len] = '\0'; if (WS_EMIT_CB) WS_EMIT_CB("message", cl->id, tmp); free(tmp); + } else if (op == WEBSOCKET_OP_BINARY) { + ws_set_last_msg((const unsigned char *) wm->data.buf, wm->data.len); + if (WS_EMIT_CB) WS_EMIT_CB("binary", cl->id, ""); + } else if (op == WEBSOCKET_OP_PING) { + if (WS_EMIT_CB) WS_EMIT_CB("ping", cl->id, ""); } } else if (ev == MG_EV_CLOSE) diff --git a/src/path_match.mbt b/src/path_match.mbt index 5f06480..f6dd748 100644 --- a/src/path_match.mbt +++ b/src/path_match.mbt @@ -94,28 +94,17 @@ fn Mocket::find_route( http_method : String, path : String, ) -> (HttpHandler, Map[String, StringView])? { - self.logger.route_lookup(http_method, path) // 优化:首先尝试静态路由缓存 match self.static_routes.get(http_method) { Some(http_methodroutes) => { let routes = [] http_methodroutes.iter().each(fn(item) { routes.push(item.0) }) - self.logger.routes_available(routes) match http_methodroutes.get(path) { - Some(handler) => { - self.logger.route_found(http_method, path) - return Some((handler, {})) - } - None => { - self.logger.route_not_found(path) - ignore(()) - } + Some(handler) => return Some((handler, {})) + None => ignore(()) } } - None => { - self.logger.debug(fn() { "No method routes for \{http_method}" }) - ignore(()) - } + None => ignore(()) } // 检查通配符方法的静态路由 diff --git a/src/pkg.generated.mbti b/src/pkg.generated.mbti index 9e264ef..f6fdf89 100644 --- a/src/pkg.generated.mbti +++ b/src/pkg.generated.mbti @@ -2,28 +2,42 @@ package "oboard/mocket" import( - "illusory0x0/native" - "moonbitlang/core/builtin" + "moonbitlang/core/buffer" + "oboard/mocket/js" ) // Values -fn __ws_emit(@native.CStr, @native.CStr, @native.CStr) -> Unit +fn __get_port_by_connection_js(String) -> Int + +fn __get_port_by_connection_js_export((String) -> Int) -> Unit + +fn __ws_emit_js_export((String, Int, String, Bytes) -> Unit) -> Unit + +fn __ws_emit_js_port(String, Int, String, Bytes) -> Unit + +fn __ws_get_members_js(String) -> Array[String] + +fn __ws_state_js_export((String, String) -> Unit, (String, String) -> Unit, (String) -> Array[String]) -> Unit + +fn __ws_subscribe_js(String, String) -> Unit + +fn __ws_unsubscribe_js(String, String) -> Unit fn async_run(async () -> Unit noraise) -> Unit fn cookie_to_string(Array[CookieItem]) -> String -async fn execute_middlewares(Array[(String, async (MocketEvent, async () -> HttpBody noraise) -> HttpBody noraise)], MocketEvent, async (MocketEvent) -> HttpBody noraise) -> HttpBody noraise +fn create_server((HttpRequestInternal, HttpResponseInternal, () -> Unit) -> Unit, Int) -> Unit -fn form_encode(Map[String, String]) -> String +fn encode_multipart(Map[String, MultipartFormValue], String) -> String -fn new(base_path? : String, logger? : Logger) -> Mocket +async fn execute_middlewares(Array[(String, async (MocketEvent, async () -> &Responder noraise) -> &Responder noraise)], MocketEvent, async (MocketEvent) -> &Responder noraise) -> &Responder noraise -fn new_debug_logger() -> Logger +fn form_encode(Map[String, String]) -> String -fn new_logger(enabled? : Bool, level? : LogLevel) -> Logger +fn html(&Show) -> &Responder -fn new_production_logger() -> Logger +fn new(base_path? : String) -> Mocket fn parse_cookie(StringView) -> Map[String, CookieItem] @@ -37,25 +51,25 @@ fn serve_ffi(Mocket, port~ : Int) -> Unit async fn[T, E : Error] suspend(((T) -> Unit, (E) -> Unit) -> Unit) -> T raise E +fn text(&Show) -> &Responder + fn url_decode(BytesView) -> String fn url_encode(String) -> String +fn ws_pong(String) -> Unit + fn ws_publish(String, String) -> Unit fn ws_send(String, String) -> Unit +fn ws_send_bytes(String, Bytes) -> Unit + fn ws_subscribe(String, String) -> Unit fn ws_unsubscribe(String, String) -> Unit // Errors -pub suberror BodyError { - InvalidJsonCharset - InvalidJson - InvalidText -} - pub suberror ExecError impl Show for ExecError @@ -79,100 +93,71 @@ pub(all) struct CookieItem { impl Eq for CookieItem impl Show for CookieItem -pub(all) enum HttpBody { - Json(Json) - Text(StringView) - HTML(StringView) - Bytes(BytesView) - Form(Map[String, String]) - Multipart(Map[String, MultipartFormValue]) - Empty -} -fn HttpBody::content_type(Self, boundary? : String) -> String +type Html +impl Responder for Html pub(all) struct HttpRequest { http_method : String url : String headers : Map[StringView, StringView] - mut body : HttpBody + mut raw_body : Bytes } +fn[T : BodyReader] HttpRequest::body(Self) -> T raise fn HttpRequest::get_cookie(Self, String) -> CookieItem? +impl Responder for HttpRequest #external pub type HttpRequestInternal -fn HttpRequestInternal::on_complete(Self, FuncRef[() -> Unit]) -> Unit -fn HttpRequestInternal::on_headers(Self, FuncRef[(@native.CStr) -> Unit]) -> Unit +fn HttpRequestInternal::req_method(Self) -> String +fn HttpRequestInternal::url(Self) -> String pub(all) struct HttpResponse { - mut status_code : Int + mut status_code : &StatusCoder headers : Map[StringView, StringView] cookies : Map[String, CookieItem] + mut raw_body : Bytes } +fn HttpResponse::body(Self, &Responder) -> Self fn HttpResponse::delete_cookie(Self, String) -> Unit +fn HttpResponse::json(Self, Json) -> Self +fn HttpResponse::new(&StatusCoder, headers? : Map[StringView, StringView], cookies? : Map[String, CookieItem], raw_body? : Bytes) -> Self fn HttpResponse::set_cookie(Self, String, String, max_age? : Int, path? : String, domain? : String, secure? : Bool, http_only? : Bool, same_site? : SameSiteOption) -> Unit +fn HttpResponse::to_responder(Self) -> &Responder +impl Responder for HttpResponse #external pub type HttpResponseInternal - -#external -pub type HttpServerInternal - -pub enum LogLevel { - Debug - Info - Warn - Error -} - -pub struct Logger { - enabled : Bool - level : LogLevel -} -fn Logger::debug(Self, () -> String) -> Unit -fn Logger::debug_str(Self, String) -> Unit -fn Logger::error(Self, () -> String) -> Unit -fn Logger::info(Self, () -> String) -> Unit -fn Logger::route_added(Self, String) -> Unit -fn Logger::route_created(Self, String) -> Unit -fn Logger::route_dynamic(Self, String, String) -> Unit -fn Logger::route_found(Self, String, String) -> Unit -fn Logger::route_lookup(Self, String, String) -> Unit -fn Logger::route_merge_existing(Self, String) -> Unit -fn Logger::route_merge_new(Self, String) -> Unit -fn Logger::route_not_found(Self, String) -> Unit -fn Logger::route_register(Self, String, String) -> Unit -fn Logger::route_static(Self, String, String) -> Unit -fn Logger::routes_available(Self, Array[String]) -> Unit -fn Logger::warn(Self, () -> String) -> Unit +fn HttpResponseInternal::end(Self, @js.Value) -> Unit +fn HttpResponseInternal::url(Self) -> String #alias(T) pub(all) struct Mocket { base_path : String - mappings : Map[(String, String), async (MocketEvent) -> HttpBody noraise] - middlewares : Array[(String, async (MocketEvent, async () -> HttpBody noraise) -> HttpBody noraise)] - static_routes : Map[String, Map[String, async (MocketEvent) -> HttpBody noraise]] - dynamic_routes : Map[String, Array[(String, async (MocketEvent) -> HttpBody noraise)]] - logger : Logger + mappings : Map[(String, String), async (MocketEvent) -> &Responder noraise] + middlewares : Array[(String, async (MocketEvent, async () -> &Responder noraise) -> &Responder noraise)] + static_routes : Map[String, Map[String, async (MocketEvent) -> &Responder noraise]] + dynamic_routes : Map[String, Array[(String, async (MocketEvent) -> &Responder noraise)]] ws_static_routes : Map[String, (WebSocketEvent) -> Unit] ws_dynamic_routes : Array[(String, (WebSocketEvent) -> Unit)] ws_clients : Map[String, Unit] ws_channels : Map[String, Map[String, Unit]] ws_client_port : Map[String, Int] } -fn Mocket::all(Self, String, async (MocketEvent) -> HttpBody noraise) -> Unit -fn Mocket::connect(Self, String, async (MocketEvent) -> HttpBody noraise) -> Unit -fn Mocket::delete(Self, String, async (MocketEvent) -> HttpBody noraise) -> Unit -fn Mocket::get(Self, String, async (MocketEvent) -> HttpBody noraise) -> Unit +fn Mocket::all(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +fn Mocket::connect(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +fn Mocket::delete(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +fn Mocket::get(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit fn Mocket::group(Self, String, (Self) -> Unit) -> Unit -fn Mocket::head(Self, String, async (MocketEvent) -> HttpBody noraise) -> Unit -fn Mocket::on(Self, String, String, async (MocketEvent) -> HttpBody noraise) -> Unit -fn Mocket::options(Self, String, async (MocketEvent) -> HttpBody noraise) -> Unit -fn Mocket::patch(Self, String, async (MocketEvent) -> HttpBody noraise) -> Unit -fn Mocket::post(Self, String, async (MocketEvent) -> HttpBody noraise) -> Unit -fn Mocket::put(Self, String, async (MocketEvent) -> HttpBody noraise) -> Unit +fn Mocket::head(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +fn Mocket::on(Self, String, String, async (MocketEvent) -> &Responder noraise) -> Unit +fn Mocket::options(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +fn Mocket::patch(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +fn Mocket::post(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +fn Mocket::put(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit fn Mocket::serve(Self, port~ : Int) -> Unit -fn Mocket::trace(Self, String, async (MocketEvent) -> HttpBody noraise) -> Unit -fn Mocket::use_middleware(Self, async (MocketEvent, async () -> HttpBody noraise) -> HttpBody noraise, base_path? : String) -> Unit +fn Mocket::static_assets(Self, String, &ServeStaticProvider) -> Unit +fn Mocket::trace(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +fn Mocket::use_middleware(Self, async (MocketEvent, async () -> &Responder noraise) -> &Responder noraise, base_path? : String) -> Unit fn Mocket::ws(Self, String, (WebSocketEvent) -> Unit) -> Unit pub(all) struct MocketEvent { @@ -196,9 +181,92 @@ impl Eq for SameSiteOption impl Show for SameSiteOption impl ToJson for SameSiteOption +pub(all) struct StaticAssetMeta { + asset_type : String? + etag : String? + mtime : Int64? + path : String? + size : Int64? + encoding : String? +} + +pub(all) enum StatusCode { + Continue + SwitchingProtocols + Processing + EarlyHints + OK + Created + Accepted + NonAuthoritativeInfo + NoContent + ResetContent + PartialContent + MultiStatus + AlreadyReported + IMUsed + MultipleChoices + MovedPermanently + Found + SeeOther + NotModified + UseProxy + TemporaryRedirect + PermanentRedirect + BadRequest + Unauthorized + PaymentRequired + Forbidden + NotFound + MethodNotAllowed + NotAcceptable + ProxyAuthRequired + RequestTimeout + Conflict + Gone + LengthRequired + PreconditionFailed + RequestEntityTooLarge + RequestUriTooLong + UnsupportedMediaType + RequestedRangeNotSatisfiable + ExpectationFailed + Teapot + MisdirectedRequest + UnprocessableEntity + Locked + FailedDependency + TooEarly + UpgradeRequired + PreconditionRequired + TooManyRequests + RequestHeaderFieldsTooLarge + UnavailableForLegalReasons + InternalServerError + NotImplemented + BadGateway + ServiceUnavailable + GatewayTimeout + HttpVersionNotSupported + VariantAlsoNegotiates + InsufficientStorage + LoopDetected + NotExtended + NetworkAuthenticationRequired +} +impl Show for StatusCode +impl ToJson for StatusCode +impl StatusCoder for StatusCode + +pub enum WebSocketAggregatedMessage { + Text(String) + Binary(Bytes) + Ping +} + pub enum WebSocketEvent { Open(WebSocketPeer) - Message(WebSocketPeer, HttpBody) + Message(WebSocketPeer, WebSocketAggregatedMessage) Close(WebSocketPeer) } @@ -206,18 +274,52 @@ pub(all) struct WebSocketPeer { connection_id : String mut subscribed_channels : Array[String] } +fn WebSocketPeer::binary(Self, Bytes) -> Unit +fn WebSocketPeer::pong(Self) -> Unit fn WebSocketPeer::publish(String, String) -> Unit -fn WebSocketPeer::send(Self, String) -> Unit fn WebSocketPeer::subscribe(Self, String) -> Unit +fn WebSocketPeer::text(Self, String) -> Unit fn WebSocketPeer::to_string(Self) -> String fn WebSocketPeer::unsubscribe(Self, String) -> Unit // Type aliases -pub type HttpHandler = async (MocketEvent) -> HttpBody noraise +pub type HttpHandler = async (MocketEvent) -> &Responder noraise -pub type Middleware = async (MocketEvent, async () -> HttpBody noraise) -> HttpBody noraise +pub type Middleware = async (MocketEvent, async () -> &Responder noraise) -> &Responder noraise pub type WebSocketHandler = (WebSocketEvent) -> Unit // Traits +pub(open) trait BodyReader { + from_request(HttpRequest) -> Self raise +} +impl BodyReader for String +impl BodyReader for FixedArray[Byte] +impl BodyReader for Bytes +impl BodyReader for Array[Byte] +impl BodyReader for Json + +pub(open) trait Responder { + options(Self, HttpResponse) -> Unit + output(Self, @buffer.Buffer) -> Unit +} +impl Responder for String +impl Responder for Bytes +impl Responder for Json +impl Responder for &ToJson +impl Responder for StringView + +pub(open) trait ServeStaticProvider { + get_meta(Self, String) -> StaticAssetMeta? + get_contents(Self, String) -> &Responder + get_type(Self, String) -> String? + get_encodings(Self) -> Map[String, String] + get_index_names(Self) -> Array[String] + get_fallthrough(Self) -> Bool +} + +pub trait StatusCoder { + to_int(Self) -> Int +} +impl StatusCoder for Int diff --git a/src/request.mbt b/src/request.mbt new file mode 100644 index 0000000..384b6b7 --- /dev/null +++ b/src/request.mbt @@ -0,0 +1,65 @@ +///| +pub(all) struct HttpRequest { + http_method : String + url : String + headers : Map[StringView, StringView] + mut raw_body : Bytes +} + +///| +pub(open) trait BodyReader { + from_request(HttpRequest) -> Self raise +} + +///| +pub fn[T : BodyReader] HttpRequest::body(self : HttpRequest) -> T raise { + T::from_request(self) +} + +///| +pub impl BodyReader for String with from_request(req : HttpRequest) -> String raise { + @encoding/utf8.decode(req.raw_body) +} + +///| +pub impl BodyReader for Json with from_request(req : HttpRequest) -> Json raise { + @json.parse(@encoding/utf8.decode(req.raw_body)) +} + +///| +pub impl BodyReader for Bytes with from_request(req : HttpRequest) -> Bytes raise { + req.raw_body +} + +///| +pub impl BodyReader for FixedArray[Byte] with from_request(req : HttpRequest) -> FixedArray[ + Byte, +] raise { + req.raw_body.to_fixedarray() +} + +///| +pub impl BodyReader for Array[Byte] with from_request(req : HttpRequest) -> Array[ + Byte, +] raise { + req.raw_body.to_array() +} + +///| +test "read_body" { + let req = HttpRequest::{ + http_method: "POST", + url: "/", + headers: Map::new(), + raw_body: b"{\"Hello\":\"World!\"}", + } + let text : String = req.body() + let json : Json = req.body() + inspect( + text, + content=( + #|{"Hello":"World!"} + ), + ) + @json.inspect(json, content={ "Hello": "World!" }) +} diff --git a/src/responder.mbt b/src/responder.mbt index 4a8bc02..e9aa945 100644 --- a/src/responder.mbt +++ b/src/responder.mbt @@ -4,9 +4,20 @@ pub(open) trait Responder { output(Self, buf : @buffer.Buffer) -> Unit } +///| +pub impl Responder for &ToJson with options(_, res) -> Unit { + res.status_code = OK + res.headers["Content-Type"] = "application/json; charset=utf-8" +} + +///| +pub impl Responder for &ToJson with output(self, buf) -> Unit { + buf.write_bytes(@encoding/utf8.encode(self.to_json().stringify())) +} + ///| pub impl Responder for HttpRequest with options(self, res) -> Unit { - res.status_code = 200 + res.status_code = OK res.headers.merge_in_place(self.headers) } @@ -28,18 +39,18 @@ pub impl Responder for HttpResponse with output(self, buf) -> Unit { ///| pub impl Responder for Json with options(_, res) -> Unit { - res.status_code = 200 + res.status_code = OK res.headers["Content-Type"] = "application/json; charset=utf-8" } ///| pub impl Responder for Json with output(self, buf) -> Unit { - buf.write_string(self.stringify()) + buf.write_bytes(@encoding/utf8.encode(self.to_json().stringify())) } ///| pub impl Responder for Bytes with options(_, res) -> Unit { - res.status_code = 200 + res.status_code = OK res.headers["Content-Type"] = "application/octet-stream" } @@ -50,46 +61,38 @@ pub impl Responder for Bytes with output(self, buf) -> Unit { ///| pub impl Responder for String with options(_, res) -> Unit { - res.status_code = 200 + res.status_code = OK res.headers["Content-Type"] = "text/plain; charset=utf-8" } ///| pub impl Responder for String with output(self, buf) -> Unit { - buf.write_string(self) + buf.write_bytes(@encoding/utf8.encode(self)) } ///| pub impl Responder for StringView with options(_, res) -> Unit { - res.status_code = 200 + res.status_code = OK res.headers["Content-Type"] = "text/plain; charset=utf-8" } ///| pub impl Responder for StringView with output(self, buf) -> Unit { - buf.write_string(self.to_string()) + buf.write_bytes(@encoding/utf8.encode(self)) } ///| -struct Html(StringView) - -///| -type Empty +struct Html(String) ///| pub impl Responder for Html with options(_, res) -> Unit { - res.status_code = 200 + res.status_code = OK res.headers["Content-Type"] = "text/html; charset=utf-8" } ///| pub impl Responder for Html with output(self, buf) -> Unit { - buf.write_string(self.0.to_string()) -} - -///| -pub fn json(json : Json) -> &Responder { - json + buf.write_bytes(@encoding/utf8.encode(self.0)) } ///| @@ -101,8 +104,3 @@ pub fn html(html : &Show) -> &Responder { pub fn text(text : &Show) -> &Responder { text.to_string() } - -///| -pub fn bytes(bytes : Bytes) -> &Responder { - bytes -} diff --git a/src/response.mbt b/src/response.mbt index 2390e6d..cc8ce64 100644 --- a/src/response.mbt +++ b/src/response.mbt @@ -1,6 +1,6 @@ ///| pub(all) struct HttpResponse { - mut status_code : Int + mut status_code : &StatusCoder headers : Map[StringView, StringView] cookies : Map[String, CookieItem] mut raw_body : Bytes @@ -8,7 +8,7 @@ pub(all) struct HttpResponse { ///| pub fn HttpResponse::new( - status_code : Int, + status_code : &StatusCoder, headers? : Map[StringView, StringView], cookies? : Map[String, CookieItem], raw_body? : Bytes, @@ -21,21 +21,6 @@ pub fn HttpResponse::new( } } -///| -pub fn HttpResponse::ok() -> HttpResponse { - HttpResponse::new(200) -} - -///| -pub fn HttpResponse::not_found() -> HttpResponse { - HttpResponse::new(404) -} - -///| -pub fn HttpResponse::not_modified() -> HttpResponse { - HttpResponse::new(304) -} - ///| pub fn HttpResponse::body( self : HttpResponse, @@ -47,6 +32,11 @@ pub fn HttpResponse::body( self } +///| +pub fn HttpResponse::json(self : HttpResponse, body : Json) -> HttpResponse { + self.body(body) +} + ///| pub fn HttpResponse::to_responder(self : HttpResponse) -> &Responder { self diff --git a/src/static.mbt b/src/static.mbt index 1403e9a..99f3cba 100644 --- a/src/static.mbt +++ b/src/static.mbt @@ -28,16 +28,16 @@ pub(open) trait ServeStaticProvider { pub fn Mocket::static_assets( self : Mocket, path : String, - options : &ServeStaticProvider, + provider : &ServeStaticProvider, ) -> Unit { self.use_middleware(async fn(event, next) noraise { - if not(event.req.url.has_prefix(path)) { + if not(match_path(path, event.req.url) is None) { return next() } // Method check if event.req.http_method != "GET" && event.req.http_method != "HEAD" { - if options.get_fallthrough() { + if provider.get_fallthrough() { return next() } event.res.headers.set("Allow", "GET, HEAD") @@ -53,7 +53,7 @@ pub fn Mocket::static_assets( .get("Accept-Encoding") .map(fn(v) { v.to_string() }) .unwrap_or("") - let encodings = options.get_encodings() + let encodings = provider.get_encodings() let matched_encodings = [] if accept_encoding != "" { // split requires `chars` label @@ -72,7 +72,7 @@ pub fn Mocket::static_assets( // Search paths let mut id = original_id let mut meta : StaticAssetMeta? = None - let index_names = options.get_index_names() + let index_names = provider.get_index_names() if index_names.length() == 0 { ignore(index_names.push("/index.html")) } @@ -89,7 +89,7 @@ pub fn Mocket::static_assets( } for encoding in try_encodings { let try_id = id + suffix + encoding - match options.get_meta(try_id) { + match provider.get_meta(try_id) { Some(m) => { meta = Some(m) id = try_id @@ -102,10 +102,10 @@ pub fn Mocket::static_assets( } match meta { None => { - if options.get_fallthrough() { + if provider.get_fallthrough() { return next() } - return HttpResponse::not_found() + return HttpResponse::new(NotFound) } Some(meta) => { // Handle caching @@ -123,7 +123,7 @@ pub fn Mocket::static_assets( event.res.headers.set("ETag", etag) } if event.req.headers.get("If-None-Match") == Some(etag) { - return HttpResponse::not_modified() + return HttpResponse::new(NotModified) } } None => () @@ -137,8 +137,10 @@ pub fn Mocket::static_assets( // Simple extension extraction let parts = id.split(".").collect() if parts.length() > 1 { - // TODO: get extension - () + match provider.get_type(parts[parts.length() - 1].to_string()) { + Some(t) => event.res.headers.set("Content-Type", t) + None => () + } } } } @@ -162,10 +164,10 @@ pub fn Mocket::static_assets( None => () } if event.req.http_method == "HEAD" { - return HttpResponse::ok() + return HttpResponse::new(OK) } - let contents = options.get_contents(id) - event.res.status_code = 200 + let contents = provider.get_contents(id) + event.res.status_code = OK contents } } diff --git a/src/static_file/pkg.generated.mbti b/src/static_file/pkg.generated.mbti new file mode 100644 index 0000000..8e38e2d --- /dev/null +++ b/src/static_file/pkg.generated.mbti @@ -0,0 +1,22 @@ +// Generated using `moon info`, DON'T EDIT IT +package "oboard/mocket/static_file" + +import( + "oboard/mocket" +) + +// Values +fn new(String) -> StaticFileProvider + +// Errors + +// Types and methods +pub struct StaticFileProvider { + path : String +} +impl @mocket.ServeStaticProvider for StaticFileProvider + +// Type aliases + +// Traits + diff --git a/src/static_file/static_file.mbt b/src/static_file/static_file.mbt index 759e0b4..3da5ad6 100644 --- a/src/static_file/static_file.mbt +++ b/src/static_file/static_file.mbt @@ -1,67 +1,63 @@ -// // ///| - -// ///| -// using @mocket {trait ServeStaticProvider, trait Responder} - -// ///| -// pub struct StaticFileProvider { -// path : String -// } - -// ///| -// pub fn StaticFileProvider::new(path : String) -> StaticFileProvider { -// { path, } -// } - -// ///| -// pub impl ServeStaticProvider for StaticFileProvider with get_meta( -// self, -// id : String, -// ) -> StaticAssetMeta? { -// None -// } - // ///| -// pub impl ServeStaticProvider for StaticFileProvider with get_contents( -// self, -// id : String, -// ) -> &Responder { -// let res : &Responder = @mocket.bytes( -// @fs.read_file_to_bytes(self.path + "/" + id), -// ) catch { -// _ => @mocket.HttpResponse::not_found() -// } -// res -// } -// // Custom MIME type resolver function - -// // ///| -// // impl ServeStaticProvider for StaticFileProvider with get_type( -// // self, -// // ext : String, -// // ) -> String? { - -// // } -// // // Encodings map - -// // ///| -// // impl ServeStaticProvider for StaticFileProvider with get_encodings(self) -> Map[ -// // String, -// // String, -// // ] { - -// // } -// // // Index names - -// // ///| -// // impl ServeStaticProvider for StaticFileProvider with get_index_names(self) -> Array[ -// // String, -// // ] { - -// // } -// // // Fallthrough - -// // ///| -// // impl ServeStaticProvider for StaticFileProvider with get_fallthrough(self) -> Bool { -// // } +///| +using @mocket {trait ServeStaticProvider, trait Responder} + +///| +pub struct StaticFileProvider { + path : String +} + +///| +pub fn new(path : String) -> StaticFileProvider { + { path, } +} + +///| +pub impl ServeStaticProvider for StaticFileProvider with get_meta( + self, + id : String, +) -> @mocket.StaticAssetMeta? { + None +} + +///| +pub impl ServeStaticProvider for StaticFileProvider with get_contents( + self, + id : String, +) -> &Responder { + let res : &Responder = @mocket.HttpResponse::new(@mocket.OK).body( + @fs.read_file_to_bytes(self.path + "/" + id), + ) catch { + _ => @mocket.HttpResponse::new(@mocket.NotFound) + } + res +} + +///| +pub impl ServeStaticProvider for StaticFileProvider with get_type( + self, + ext : String, +) -> String? { + None +} + +///| +pub impl ServeStaticProvider for StaticFileProvider with get_encodings(self) -> Map[ + String, + String, +] { + {} +} + +///| +pub impl ServeStaticProvider for StaticFileProvider with get_index_names(self) -> Array[ + String, +] { + [] +} + +///| +pub impl ServeStaticProvider for StaticFileProvider with get_fallthrough(self) -> Bool { + false +} diff --git a/src/status_code.mbt b/src/status_code.mbt new file mode 100644 index 0000000..606788e --- /dev/null +++ b/src/status_code.mbt @@ -0,0 +1,212 @@ +///| +pub trait StatusCoder { + to_int(self : Self) -> Int +} + +///| +pub impl StatusCoder for Int with to_int(self) -> Int { + self +} + +///| +/// HTTP status codes as registered with IANA. +/// See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml +pub(all) enum StatusCode { + /// RFC 9110, 15.2.1 + Continue + /// RFC 9110, 15.2.2 + SwitchingProtocols + /// RFC 2518, 10.1 + Processing + /// RFC 8297 + EarlyHints + /// RFC 9110, 15.3.1 + OK + /// RFC 9110, 15.3.2 + Created + /// RFC 9110, 15.3.3 + Accepted + /// RFC 9110, 15.3.4 + NonAuthoritativeInfo + /// RFC 9110, 15.3.5 + NoContent + /// RFC 9110, 15.3.6 + ResetContent + /// RFC 9110, 15.3.7 + PartialContent + /// RFC 4918, 11.1 + MultiStatus + /// RFC 5842, 7.1 + AlreadyReported + /// RFC 3229, 10.4.1 + IMUsed + /// RFC 9110, 15.4.1 + MultipleChoices + /// RFC 9110, 15.4.2 + MovedPermanently + /// RFC 9110, 15.4.3 + Found + /// RFC 9110, 15.4.4 + SeeOther + /// RFC 9110, 15.4.5 + NotModified + /// RFC 9110, 15.4.6 + UseProxy + /// RFC 9110, 15.4.8 + TemporaryRedirect + /// RFC 9110, 15.4.9 + PermanentRedirect + /// RFC 9110, 15.5.1 + BadRequest + /// RFC 9110, 15.5.2 + Unauthorized + /// RFC 9110, 15.5.3 + PaymentRequired + /// RFC 9110, 15.5.4 + Forbidden + /// RFC 9110, 15.5.5 + NotFound + /// RFC 9110, 15.5.6 + MethodNotAllowed + /// RFC 9110, 15.5.7 + NotAcceptable + /// RFC 9110, 15.5.8 + ProxyAuthRequired + /// RFC 9110, 15.5.9 + RequestTimeout + /// RFC 9110, 15.5.10 + Conflict + /// RFC 9110, 15.5.11 + Gone + /// RFC 9110, 15.5.12 + LengthRequired + /// RFC 9110, 15.5.13 + PreconditionFailed + /// RFC 9110, 15.5.14 + RequestEntityTooLarge + /// RFC 9110, 15.5.15 + RequestUriTooLong + /// RFC 9110, 15.5.16 + UnsupportedMediaType + /// RFC 9110, 15.5.17 + RequestedRangeNotSatisfiable + /// RFC 9110, 15.5.18 + ExpectationFailed + /// RFC 9110, 15.5.19 (Unused) + Teapot + /// RFC 9110, 15.5.20 + MisdirectedRequest + /// RFC 9110, 15.5.21 + UnprocessableEntity + /// RFC 4918, 11.3 + Locked + /// RFC 4918, 11.4 + FailedDependency + /// RFC 8470, 5.2. + TooEarly + /// RFC 9110, 15.5.22 + UpgradeRequired + /// RFC 6585, 3 + PreconditionRequired + /// RFC 6585, 4 + TooManyRequests + /// RFC 6585, 5 + RequestHeaderFieldsTooLarge + /// RFC 7725, 3 + UnavailableForLegalReasons + /// RFC 9110, 15.6.1 + InternalServerError + /// RFC 9110, 15.6.2 + NotImplemented + /// RFC 9110, 15.6.3 + BadGateway + /// RFC 9110, 15.6.4 + ServiceUnavailable + /// RFC 9110, 15.6.5 + GatewayTimeout + /// RFC 9110, 15.6.6 + HttpVersionNotSupported + /// RFC 2295, 8.1 + VariantAlsoNegotiates + /// RFC 4918, 11.5 + InsufficientStorage + /// RFC 5842, 7.2 + LoopDetected + /// RFC 2774, 7 + NotExtended + /// RFC 6585, 6 + NetworkAuthenticationRequired +} derive(Show, ToJson) + +///| +pub impl StatusCoder for StatusCode with to_int(self) -> Int { + match self { + Continue => 100 + SwitchingProtocols => 101 + Processing => 102 + EarlyHints => 103 + OK => 200 + Created => 201 + Accepted => 202 + NonAuthoritativeInfo => 203 + NoContent => 204 + ResetContent => 205 + PartialContent => 206 + MultiStatus => 207 + AlreadyReported => 208 + IMUsed => 226 + MultipleChoices => 300 + MovedPermanently => 301 + Found => 302 + SeeOther => 303 + NotModified => 304 + UseProxy => 305 + TemporaryRedirect => 307 + PermanentRedirect => 308 + BadRequest => 400 + Unauthorized => 401 + PaymentRequired => 402 + Forbidden => 403 + NotFound => 404 + MethodNotAllowed => 405 + NotAcceptable => 406 + ProxyAuthRequired => 407 + RequestTimeout => 408 + Conflict => 409 + Gone => 410 + LengthRequired => 411 + PreconditionFailed => 412 + RequestEntityTooLarge => 413 + RequestUriTooLong => 414 + UnsupportedMediaType => 415 + RequestedRangeNotSatisfiable => 416 + ExpectationFailed => 417 + Teapot => 418 + MisdirectedRequest => 421 + UnprocessableEntity => 422 + Locked => 423 + FailedDependency => 424 + TooEarly => 425 + UpgradeRequired => 426 + PreconditionRequired => 428 + TooManyRequests => 429 + RequestHeaderFieldsTooLarge => 431 + UnavailableForLegalReasons => 451 + InternalServerError => 500 + NotImplemented => 501 + BadGateway => 502 + ServiceUnavailable => 503 + GatewayTimeout => 504 + HttpVersionNotSupported => 505 + VariantAlsoNegotiates => 506 + InsufficientStorage => 507 + LoopDetected => 508 + NotExtended => 510 + NetworkAuthenticationRequired => 511 + } +} + +///| +test { + inspect(StatusCode::OK.to_int(), content="200") +} diff --git a/src/websocket.mbt b/src/websocket.mbt index a3d7475..bed0b43 100644 --- a/src/websocket.mbt +++ b/src/websocket.mbt @@ -5,11 +5,23 @@ pub(all) struct WebSocketPeer { } ///| -pub fn WebSocketPeer::send(self : WebSocketPeer, message : String) -> Unit { +pub fn WebSocketPeer::text(self : WebSocketPeer, message : String) -> Unit { // 真正发送到后端连接 ws_send(self.connection_id, message) } +///| +pub fn WebSocketPeer::binary(self : WebSocketPeer, message : Bytes) -> Unit { + // 真正发送到后端连接 + ws_send_bytes(self.connection_id, message) +} + +///| +pub fn WebSocketPeer::pong(self : WebSocketPeer) -> Unit { + // 真正发送到后端连接 + ws_pong(self.connection_id) +} + ///| pub fn WebSocketPeer::subscribe(self : WebSocketPeer, channel : String) -> Unit { if not(self.subscribed_channels.contains(channel)) { @@ -52,10 +64,17 @@ pub fn WebSocketPeer::to_string(self : WebSocketPeer) -> String { ///| pub enum WebSocketEvent { Open(WebSocketPeer) - Message(WebSocketPeer, Bytes) + Message(WebSocketPeer, WebSocketAggregatedMessage) Close(WebSocketPeer) } +///| +pub enum WebSocketAggregatedMessage { + Text(String) + Binary(Bytes) + Ping +} + ///| // WebSocket 路由的事件处理器类型(不返回 HttpBody) pub type WebSocketHandler = (WebSocketEvent) -> Unit diff --git a/test_ws.js b/test_ws.js index 36989ad..c5f58cb 100644 --- a/test_ws.js +++ b/test_ws.js @@ -3,6 +3,11 @@ ws.addEventListener("open", () => { console.log("connected"); ws.send("hello mocket"); console.log("sent"); + + // binary test + const buffer = Buffer.from([0x01, 0x02, 0x03, 0x04]); + ws.send(buffer); + console.log("sent binary"); }); ws.addEventListener("message", (ev) => { console.log("msg:", ev.data); @@ -10,3 +15,4 @@ ws.addEventListener("message", (ev) => { }); ws.addEventListener("close", () => console.log("closed")); ws.addEventListener("error", (err) => console.log("error", err)); +ws.addEventListener("pong", () => console.log("received pong")); From a0dda9283cfe50d33339efe99c44f061db39bc63 Mon Sep 17 00:00:00 2001 From: oboard Date: Tue, 25 Nov 2025 18:34:56 +0800 Subject: [PATCH 3/5] refactor(mocket): centralize not found handling and simplify responder - Add handle_not_found function for consistent 404 responses - Remove redundant status_code assignments in Responder impls - Simplify static file provider implementation - Clean up unused FFI functions --- src/js/pkg.generated.mbti | 194 -------------------------------- src/mocket.js.mbt | 6 +- src/mocket.native.mbt | 23 ++-- src/not_found.mbt | 4 + src/pkg.generated.mbti | 34 ++---- src/responder.mbt | 7 -- src/static.mbt | 17 ++- src/static_file/static_file.mbt | 20 ++-- 8 files changed, 49 insertions(+), 256 deletions(-) create mode 100644 src/not_found.mbt diff --git a/src/js/pkg.generated.mbti b/src/js/pkg.generated.mbti index 5506f59..499bd25 100644 --- a/src/js/pkg.generated.mbti +++ b/src/js/pkg.generated.mbti @@ -1,207 +1,13 @@ // Generated using `moon info`, DON'T EDIT IT package "oboard/mocket/js" -import( - "moonbitlang/core/json" -) - // Values -async fn[T] async_all(Array[async () -> T]) -> Array[T] - -let async_iterator : Symbol - -fn async_run(async () -> Unit noraise) -> Unit - -fn async_test(async () -> Unit) -> Unit - -let globalThis : Value - -let iterator : Symbol - -fn require(String, keys? : Array[String]) -> Value - -fn[T, E : Error] spawn_detach(async () -> T raise E) -> Unit - -async fn[T, E : Error] suspend(((T) -> Unit, (E) -> Unit) -> Unit) -> T raise E // Errors -pub suberror Error_ Value -fn Error_::cause(Self) -> Value? -fn[T] Error_::wrap(() -> Value, map_ok? : (Value) -> T) -> T raise Self -impl Show for Error_ // Types and methods -type Nullable[_] -fn[T] Nullable::from_option(T?) -> Self[T] -fn[T] Nullable::get_exn(Self[T]) -> T -fn[T] Nullable::is_null(Self[T]) -> Bool -fn[T] Nullable::null() -> Self[T] -fn[T] Nullable::to_option(Self[T]) -> T? -fn[T] Nullable::unwrap(Self[T]) -> T - -pub struct Object(Value) -fn[K, V] Object::extend_iter(Self, Iter[(K, V)]) -> Unit -fn[K, V] Object::extend_iter2(Self, Iter2[K, V]) -> Unit -fn Object::extend_object(Self, Self) -> Self -fn[K, V] Object::from_iter(Iter[(K, V)]) -> Self -fn[K, V] Object::from_iter2(Iter2[K, V]) -> Self -fn Object::from_value(Value) -> Optional[Self] -fn Object::from_value_unchecked(Value) -> Self -#deprecated -fn Object::inner(Self) -> Value -fn Object::new() -> Self -fn[K, V] Object::op_get(Self, K) -> V -fn[K, V] Object::op_set(Self, K, V) -> Unit -fn Object::to_value(Self) -> Value - -type Optional[_] -fn[T] Optional::from_option(T?) -> Self[T] -fn[T] Optional::get_exn(Self[T]) -> T -fn[T] Optional::is_undefined(Self[T]) -> Bool -fn[T] Optional::to_option(Self[T]) -> T? -fn[T] Optional::undefined() -> Self[T] -fn[T] Optional::unwrap(Self[T]) -> T - -#external -pub type Promise -fn Promise::all(Array[Self]) -> Self -fn[T] Promise::unsafe_new(async () -> T) -> Self -async fn Promise::wait(Self) -> Value - -type Symbol -fn Symbol::make() -> Self -fn Symbol::make_with_number(Double) -> Self -fn Symbol::make_with_string(String) -> Self -fn Symbol::make_with_string_js(String) -> Self - -type Union2[_, _] -fn[A : Cast, B] Union2::from0(A) -> Self[A, B] -fn[A, B : Cast] Union2::from1(B) -> Self[A, B] -fn[A : Cast, B] Union2::to0(Self[A, B]) -> A? -fn[A, B : Cast] Union2::to1(Self[A, B]) -> B? - -type Union3[_, _, _] -fn[A : Cast, B, C] Union3::from0(A) -> Self[A, B, C] -fn[A, B : Cast, C] Union3::from1(B) -> Self[A, B, C] -fn[A, B, C : Cast] Union3::from2(C) -> Self[A, B, C] -fn[A : Cast, B, C] Union3::to0(Self[A, B, C]) -> A? -fn[A, B : Cast, C] Union3::to1(Self[A, B, C]) -> B? -fn[A, B, C : Cast] Union3::to2(Self[A, B, C]) -> C? - -type Union4[_, _, _, _] -fn[A : Cast, B, C, D] Union4::from0(A) -> Self[A, B, C, D] -fn[A, B : Cast, C, D] Union4::from1(B) -> Self[A, B, C, D] -fn[A, B, C : Cast, D] Union4::from2(C) -> Self[A, B, C, D] -fn[A, B, C, D : Cast] Union4::from3(D) -> Self[A, B, C, D] -fn[A : Cast, B, C, D] Union4::to0(Self[A, B, C, D]) -> A? -fn[A, B : Cast, C, D] Union4::to1(Self[A, B, C, D]) -> B? -fn[A, B, C : Cast, D] Union4::to2(Self[A, B, C, D]) -> C? -fn[A, B, C, D : Cast] Union4::to3(Self[A, B, C, D]) -> D? - -type Union5[_, _, _, _, _] -fn[A : Cast, B, C, D, E] Union5::from0(A) -> Self[A, B, C, D, E] -fn[A, B : Cast, C, D, E] Union5::from1(B) -> Self[A, B, C, D, E] -fn[A, B, C : Cast, D, E] Union5::from2(C) -> Self[A, B, C, D, E] -fn[A, B, C, D : Cast, E] Union5::from3(D) -> Self[A, B, C, D, E] -fn[A, B, C, D, E : Cast] Union5::from4(E) -> Self[A, B, C, D, E] -fn[A : Cast, B, C, D, E] Union5::to0(Self[A, B, C, D, E]) -> A? -fn[A, B : Cast, C, D, E] Union5::to1(Self[A, B, C, D, E]) -> B? -fn[A, B, C : Cast, D, E] Union5::to2(Self[A, B, C, D, E]) -> C? -fn[A, B, C, D : Cast, E] Union5::to3(Self[A, B, C, D, E]) -> D? -fn[A, B, C, D, E : Cast] Union5::to4(Self[A, B, C, D, E]) -> E? - -type Union6[_, _, _, _, _, _] -fn[A : Cast, B, C, D, E, F] Union6::from0(A) -> Self[A, B, C, D, E, F] -fn[A, B : Cast, C, D, E, F] Union6::from1(B) -> Self[A, B, C, D, E, F] -fn[A, B, C : Cast, D, E, F] Union6::from2(C) -> Self[A, B, C, D, E, F] -fn[A, B, C, D : Cast, E, F] Union6::from3(D) -> Self[A, B, C, D, E, F] -fn[A, B, C, D, E : Cast, F] Union6::from4(E) -> Self[A, B, C, D, E, F] -fn[A, B, C, D, E, F : Cast] Union6::from5(F) -> Self[A, B, C, D, E, F] -fn[A : Cast, B, C, D, E, F] Union6::to0(Self[A, B, C, D, E, F]) -> A? -fn[A, B : Cast, C, D, E, F] Union6::to1(Self[A, B, C, D, E, F]) -> B? -fn[A, B, C : Cast, D, E, F] Union6::to2(Self[A, B, C, D, E, F]) -> C? -fn[A, B, C, D : Cast, E, F] Union6::to3(Self[A, B, C, D, E, F]) -> D? -fn[A, B, C, D, E : Cast, F] Union6::to4(Self[A, B, C, D, E, F]) -> E? -fn[A, B, C, D, E, F : Cast] Union6::to5(Self[A, B, C, D, E, F]) -> F? - -type Union7[_, _, _, _, _, _, _] -fn[A : Cast, B, C, D, E, F, G] Union7::from0(A) -> Self[A, B, C, D, E, F, G] -fn[A, B : Cast, C, D, E, F, G] Union7::from1(B) -> Self[A, B, C, D, E, F, G] -fn[A, B, C : Cast, D, E, F, G] Union7::from2(C) -> Self[A, B, C, D, E, F, G] -fn[A, B, C, D : Cast, E, F, G] Union7::from3(D) -> Self[A, B, C, D, E, F, G] -fn[A, B, C, D, E : Cast, F, G] Union7::from4(E) -> Self[A, B, C, D, E, F, G] -fn[A, B, C, D, E, F : Cast, G] Union7::from5(F) -> Self[A, B, C, D, E, F, G] -fn[A, B, C, D, E, F, G : Cast] Union7::from6(G) -> Self[A, B, C, D, E, F, G] -fn[A : Cast, B, C, D, E, F, G] Union7::to0(Self[A, B, C, D, E, F, G]) -> A? -fn[A, B : Cast, C, D, E, F, G] Union7::to1(Self[A, B, C, D, E, F, G]) -> B? -fn[A, B, C : Cast, D, E, F, G] Union7::to2(Self[A, B, C, D, E, F, G]) -> C? -fn[A, B, C, D : Cast, E, F, G] Union7::to3(Self[A, B, C, D, E, F, G]) -> D? -fn[A, B, C, D, E : Cast, F, G] Union7::to4(Self[A, B, C, D, E, F, G]) -> E? -fn[A, B, C, D, E, F : Cast, G] Union7::to5(Self[A, B, C, D, E, F, G]) -> F? -fn[A, B, C, D, E, F, G : Cast] Union7::to6(Self[A, B, C, D, E, F, G]) -> G? - -type Union8[_, _, _, _, _, _, _, _] -fn[A : Cast, B, C, D, E, F, G, H] Union8::from0(A) -> Self[A, B, C, D, E, F, G, H] -fn[A, B : Cast, C, D, E, F, G, H] Union8::from1(B) -> Self[A, B, C, D, E, F, G, H] -fn[A, B, C : Cast, D, E, F, G, H] Union8::from2(C) -> Self[A, B, C, D, E, F, G, H] -fn[A, B, C, D : Cast, E, F, G, H] Union8::from3(D) -> Self[A, B, C, D, E, F, G, H] -fn[A, B, C, D, E : Cast, F, G, H] Union8::from4(E) -> Self[A, B, C, D, E, F, G, H] -fn[A, B, C, D, E, F : Cast, G, H] Union8::from5(F) -> Self[A, B, C, D, E, F, G, H] -fn[A, B, C, D, E, F, G : Cast, H] Union8::from6(G) -> Self[A, B, C, D, E, F, G, H] -fn[A, B, C, D, E, F, G, H : Cast] Union8::from7(H) -> Self[A, B, C, D, E, F, G, H] -fn[A : Cast, B, C, D, E, F, G, H] Union8::to0(Self[A, B, C, D, E, F, G, H]) -> A? -fn[A, B : Cast, C, D, E, F, G, H] Union8::to1(Self[A, B, C, D, E, F, G, H]) -> B? -fn[A, B, C : Cast, D, E, F, G, H] Union8::to2(Self[A, B, C, D, E, F, G, H]) -> C? -fn[A, B, C, D : Cast, E, F, G, H] Union8::to3(Self[A, B, C, D, E, F, G, H]) -> D? -fn[A, B, C, D, E : Cast, F, G, H] Union8::to4(Self[A, B, C, D, E, F, G, H]) -> E? -fn[A, B, C, D, E, F : Cast, G, H] Union8::to5(Self[A, B, C, D, E, F, G, H]) -> F? -fn[A, B, C, D, E, F, G : Cast, H] Union8::to6(Self[A, B, C, D, E, F, G, H]) -> G? -fn[A, B, C, D, E, F, G, H : Cast] Union8::to7(Self[A, B, C, D, E, F, G, H]) -> H? - -#external -pub type Value -fn[Arg, Result] Value::apply(Self, Array[Arg]) -> Result -fn[Arg, Result] Value::apply_with_index(Self, Int, Array[Arg]) -> Result -fn[Arg, Result] Value::apply_with_string(Self, String, Array[Arg]) -> Result -fn[Arg, Result] Value::apply_with_symbol(Self, Symbol, Array[Arg]) -> Result -fn[T] Value::cast(Self) -> T -fn[T] Value::cast_from(T) -> Self -fn Value::extends(Self, Self) -> Self -fn Value::from_json(Json) -> Self raise -fn Value::from_json_string(String) -> Self raise -fn[T] Value::get_with_index(Self, Int) -> T -fn[T] Value::get_with_string(Self, String) -> T -fn[T] Value::get_with_symbol(Self, Symbol) -> T -fn Value::is_bool(Self) -> Bool -fn Value::is_null(Self) -> Bool -fn Value::is_number(Self) -> Bool -fn Value::is_object(Self) -> Bool -fn Value::is_string(Self) -> Bool -fn Value::is_symbol(Self) -> Bool -fn Value::is_undefined(Self) -> Bool -fn[Arg, Result] Value::new(Self, Array[Arg]) -> Result -fn[Arg, Result] Value::new_with_index(Self, Int, Array[Arg]) -> Result -fn[Arg, Result] Value::new_with_string(Self, String, Array[Arg]) -> Result -fn[Arg, Result] Value::new_with_symbol(Self, Symbol, Array[Arg]) -> Result -fn[T] Value::set_with_index(Self, Int, T) -> Unit -fn[T] Value::set_with_string(Self, String, T) -> Unit -fn[T] Value::set_with_symbol(Self, Symbol, T) -> Unit -fn Value::to_json(Self) -> Json raise -fn Value::to_json_string(Self) -> String raise -fn Value::to_string(Self) -> String -impl Show for Value -impl @json.FromJson for Value // Type aliases // Traits -pub(open) trait Cast { - into(Value) -> Self? - from(Self) -> Value -} -impl Cast for Bool -impl Cast for Int -impl Cast for Double -impl Cast for String -impl[A : Cast] Cast for Array[A] diff --git a/src/mocket.js.mbt b/src/mocket.js.mbt index 8ffbade..8d54761 100644 --- a/src/mocket.js.mbt +++ b/src/mocket.js.mbt @@ -273,11 +273,7 @@ pub fn serve_ffi(mocket : Mocket, port~ : Int) -> Unit { let (params, handler) = match mocket.find_route(req.req_method(), req.url()) { Some((h, p)) => (p, h) - _ => { - res.write_head(404, @js.Object::new().to_value()) - res.end(@js.Value::cast_from("Not Found")) - return - } + _ => ({}, handle_not_found) } let event = { req: { diff --git a/src/mocket.native.mbt b/src/mocket.native.mbt index e9fc8cc..27ab2da 100644 --- a/src/mocket.native.mbt +++ b/src/mocket.native.mbt @@ -57,12 +57,12 @@ extern "c" fn HttpRequestInternal::req_body_len( self : HttpRequestInternal, ) -> Int = "req_body_len" -///| -#owned(self, body) -extern "c" fn HttpResponseInternal::end( - self : HttpResponseInternal, - body : @native.CStr, -) -> Unit = "res_end" +// ///| +// #owned(self, body) +// extern "c" fn HttpResponseInternal::end( +// self : HttpResponseInternal, +// body : @native.CStr, +// ) -> Unit = "res_end" ///| #owned(self, body) @@ -184,11 +184,7 @@ fn handle_request_native( }) let (params, handler) = match mocket.find_route(http_method, path) { Some((h, p)) => (p, h) - _ => { - res.status(404) - res.end(to_cstr("Not Found")) - return - } + _ => ({}, handle_not_found) } let event = { req: { http_method, url, raw_body: "", headers: req_headers }, @@ -215,6 +211,9 @@ fn handle_request_native( to_cstr("Set-Cookie"), to_cstr(cookie.to_string()), )) + let buf = @buffer.new() + responder.output(buf) + event.res.raw_body = buf.to_bytes() res.end_bytes(event.res.raw_body) }) } @@ -291,7 +290,7 @@ extern "c" fn ws_send_bytes_len_native( extern "c" fn ws_pong_native(id : @native.CStr) -> Unit = "ws_pong" ///| -extern "c" fn ws_msg_body() -> Bytes = "ws_msg_body" +// extern "c" fn ws_msg_body() -> Bytes = "ws_msg_body" ///| extern "c" fn ws_msg_body_len() -> Int = "ws_msg_body_len" diff --git a/src/not_found.mbt b/src/not_found.mbt new file mode 100644 index 0000000..d1c6ef9 --- /dev/null +++ b/src/not_found.mbt @@ -0,0 +1,4 @@ +///| +pub async fn handle_not_found(_ : MocketEvent) -> &Responder noraise { + HttpResponse::new(404).body("Not Found") +} diff --git a/src/pkg.generated.mbti b/src/pkg.generated.mbti index f6fdf89..0ae022b 100644 --- a/src/pkg.generated.mbti +++ b/src/pkg.generated.mbti @@ -2,39 +2,23 @@ package "oboard/mocket" import( + "illusory0x0/native" "moonbitlang/core/buffer" - "oboard/mocket/js" ) // Values -fn __get_port_by_connection_js(String) -> Int - -fn __get_port_by_connection_js_export((String) -> Int) -> Unit - -fn __ws_emit_js_export((String, Int, String, Bytes) -> Unit) -> Unit - -fn __ws_emit_js_port(String, Int, String, Bytes) -> Unit - -fn __ws_get_members_js(String) -> Array[String] - -fn __ws_state_js_export((String, String) -> Unit, (String, String) -> Unit, (String) -> Array[String]) -> Unit - -fn __ws_subscribe_js(String, String) -> Unit - -fn __ws_unsubscribe_js(String, String) -> Unit +fn __ws_emit(@native.CStr, @native.CStr, @native.CStr) -> Unit fn async_run(async () -> Unit noraise) -> Unit fn cookie_to_string(Array[CookieItem]) -> String -fn create_server((HttpRequestInternal, HttpResponseInternal, () -> Unit) -> Unit, Int) -> Unit - -fn encode_multipart(Map[String, MultipartFormValue], String) -> String - async fn execute_middlewares(Array[(String, async (MocketEvent, async () -> &Responder noraise) -> &Responder noraise)], MocketEvent, async (MocketEvent) -> &Responder noraise) -> &Responder noraise fn form_encode(Map[String, String]) -> String +async fn handle_not_found(MocketEvent) -> &Responder noraise + fn html(&Show) -> &Responder fn new(base_path? : String) -> Mocket @@ -108,8 +92,8 @@ impl Responder for HttpRequest #external pub type HttpRequestInternal -fn HttpRequestInternal::req_method(Self) -> String -fn HttpRequestInternal::url(Self) -> String +fn HttpRequestInternal::on_complete(Self, FuncRef[() -> Unit]) -> Unit +fn HttpRequestInternal::on_headers(Self, FuncRef[(@native.CStr) -> Unit]) -> Unit pub(all) struct HttpResponse { mut status_code : &StatusCoder @@ -127,8 +111,9 @@ impl Responder for HttpResponse #external pub type HttpResponseInternal -fn HttpResponseInternal::end(Self, @js.Value) -> Unit -fn HttpResponseInternal::url(Self) -> String + +#external +pub type HttpServerInternal #alias(T) pub(all) struct Mocket { @@ -189,6 +174,7 @@ pub(all) struct StaticAssetMeta { size : Int64? encoding : String? } +fn StaticAssetMeta::new(asset_type? : String, etag? : String, mtime? : Int64, path? : String, size? : Int64, encoding? : String) -> Self pub(all) enum StatusCode { Continue diff --git a/src/responder.mbt b/src/responder.mbt index e9aa945..1bf9c19 100644 --- a/src/responder.mbt +++ b/src/responder.mbt @@ -6,7 +6,6 @@ pub(open) trait Responder { ///| pub impl Responder for &ToJson with options(_, res) -> Unit { - res.status_code = OK res.headers["Content-Type"] = "application/json; charset=utf-8" } @@ -17,7 +16,6 @@ pub impl Responder for &ToJson with output(self, buf) -> Unit { ///| pub impl Responder for HttpRequest with options(self, res) -> Unit { - res.status_code = OK res.headers.merge_in_place(self.headers) } @@ -39,7 +37,6 @@ pub impl Responder for HttpResponse with output(self, buf) -> Unit { ///| pub impl Responder for Json with options(_, res) -> Unit { - res.status_code = OK res.headers["Content-Type"] = "application/json; charset=utf-8" } @@ -50,7 +47,6 @@ pub impl Responder for Json with output(self, buf) -> Unit { ///| pub impl Responder for Bytes with options(_, res) -> Unit { - res.status_code = OK res.headers["Content-Type"] = "application/octet-stream" } @@ -61,7 +57,6 @@ pub impl Responder for Bytes with output(self, buf) -> Unit { ///| pub impl Responder for String with options(_, res) -> Unit { - res.status_code = OK res.headers["Content-Type"] = "text/plain; charset=utf-8" } @@ -72,7 +67,6 @@ pub impl Responder for String with output(self, buf) -> Unit { ///| pub impl Responder for StringView with options(_, res) -> Unit { - res.status_code = OK res.headers["Content-Type"] = "text/plain; charset=utf-8" } @@ -86,7 +80,6 @@ struct Html(String) ///| pub impl Responder for Html with options(_, res) -> Unit { - res.status_code = OK res.headers["Content-Type"] = "text/html; charset=utf-8" } diff --git a/src/static.mbt b/src/static.mbt index 99f3cba..255ebea 100644 --- a/src/static.mbt +++ b/src/static.mbt @@ -8,6 +8,18 @@ pub(all) struct StaticAssetMeta { encoding : String? } +///| +pub fn StaticAssetMeta::new( + asset_type? : String, + etag? : String, + mtime? : Int64, + path? : String, + size? : Int64, + encoding? : String, +) -> Self { + { asset_type, etag, mtime, path, size, encoding } +} + ///| pub(open) trait ServeStaticProvider { // This function should resolve asset meta @@ -49,10 +61,7 @@ pub fn Mocket::static_assets( // Parse Accept-Encoding // Headers are Map[StringView, StringView] - let accept_encoding = event.req.headers - .get("Accept-Encoding") - .map(fn(v) { v.to_string() }) - .unwrap_or("") + let accept_encoding = event.req.headers.get("Accept-Encoding").unwrap_or("") let encodings = provider.get_encodings() let matched_encodings = [] if accept_encoding != "" { diff --git a/src/static_file/static_file.mbt b/src/static_file/static_file.mbt index 3da5ad6..b0f4cab 100644 --- a/src/static_file/static_file.mbt +++ b/src/static_file/static_file.mbt @@ -18,7 +18,7 @@ pub impl ServeStaticProvider for StaticFileProvider with get_meta( self, id : String, ) -> @mocket.StaticAssetMeta? { - None + Some(@mocket.StaticAssetMeta::new(path=self.path + "/" + id)) } ///| @@ -29,21 +29,18 @@ pub impl ServeStaticProvider for StaticFileProvider with get_contents( let res : &Responder = @mocket.HttpResponse::new(@mocket.OK).body( @fs.read_file_to_bytes(self.path + "/" + id), ) catch { - _ => @mocket.HttpResponse::new(@mocket.NotFound) + _ => @mocket.HttpResponse::new(@mocket.NotFound).body("Not Found") } res } ///| -pub impl ServeStaticProvider for StaticFileProvider with get_type( - self, - ext : String, -) -> String? { +pub impl ServeStaticProvider for StaticFileProvider with get_type(_, _ : String) -> String? { None } ///| -pub impl ServeStaticProvider for StaticFileProvider with get_encodings(self) -> Map[ +pub impl ServeStaticProvider for StaticFileProvider with get_encodings(_) -> Map[ String, String, ] { @@ -51,13 +48,16 @@ pub impl ServeStaticProvider for StaticFileProvider with get_encodings(self) -> } ///| -pub impl ServeStaticProvider for StaticFileProvider with get_index_names(self) -> Array[ +pub impl ServeStaticProvider for StaticFileProvider with get_index_names(_) -> Array[ String, ] { - [] + [ + "index.html", "index.htm", "index.txt", "index.md", "index.json", "index.xml", + "index.xhtml", "default.html", "default.htm", "home.html", "home.htm", + ] } ///| -pub impl ServeStaticProvider for StaticFileProvider with get_fallthrough(self) -> Bool { +pub impl ServeStaticProvider for StaticFileProvider with get_fallthrough(_) -> Bool { false } From 56581912a791928aa2ccb5f8c16492a8b80162a8 Mon Sep 17 00:00:00 2001 From: oboard Date: Tue, 25 Nov 2025 19:00:20 +0800 Subject: [PATCH 4/5] refactor(http): replace status code integers with enum values - Replace raw status code integers with StatusCode enum for better type safety - Update HttpResponse to use StatusCode instead of StatusCoder trait - Add Custom variant to StatusCode enum for custom status codes - Improve JSON handling in responder example with BodyReader implementation --- moon.mod.json | 2 +- src/examples/responder/main.mbt | 30 +++++++++++++++++++++++++++--- src/not_found.mbt | 2 +- src/request.mbt | 2 +- src/response.mbt | 8 ++++---- src/static.mbt | 2 +- src/status_code.mbt | 18 +++++++----------- 7 files changed, 42 insertions(+), 22 deletions(-) diff --git a/moon.mod.json b/moon.mod.json index c45f048..f5c45cd 100644 --- a/moon.mod.json +++ b/moon.mod.json @@ -1,6 +1,6 @@ { "name": "oboard/mocket", - "version": "0.5.8", + "version": "0.6.0", "deps": { "illusory0x0/native": "0.2.1", "moonbitlang/x": "0.4.34", diff --git a/src/examples/responder/main.mbt b/src/examples/responder/main.mbt index dcc7b69..d79109b 100644 --- a/src/examples/responder/main.mbt +++ b/src/examples/responder/main.mbt @@ -1,8 +1,16 @@ +///| +using @mocket {type HttpResponse} + ///| struct Person { name : String age : Int -} derive(ToJson) +} derive(ToJson, FromJson) + +///| +impl @mocket.BodyReader for Person with from_request(req) -> Person raise { + @json.from_json(req.body()) +} ///| fn main { @@ -13,13 +21,29 @@ fn main { ..get("/", _event => "⚡️ Tadaa!") // Object Response - ..get("/", _event => { name: "oboard", age: 21 }.to_json()) + ..get("/json", _event => { name: "oboard", age: 21 }.to_json()) + + // JSON Request + // curl --location 'localhost:4000/json' \ + // --header 'Content-Type: application/json' \ + // --data '{ + // "name": "oboard", + // "age": 21 + // }' + ..post("/json", event => try { + let person : Person = event.req.body() + HttpResponse::new(OK).body( + "Hello, \{person.name}. You are \{person.age} years old.", + ) + } catch { + _ => HttpResponse::new(BadRequest).body("Invalid JSON") + }) // Echo Server ..post("/echo", e => e.req) // 404 Page - ..get("/404", _ => @mocket.HttpResponse::new(@mocket.NotFound).body( + ..get("/404", _ => HttpResponse::new(NotFound).body( @mocket.html( ( #| diff --git a/src/not_found.mbt b/src/not_found.mbt index d1c6ef9..7c82ea8 100644 --- a/src/not_found.mbt +++ b/src/not_found.mbt @@ -1,4 +1,4 @@ ///| pub async fn handle_not_found(_ : MocketEvent) -> &Responder noraise { - HttpResponse::new(404).body("Not Found") + HttpResponse::new(NotFound).body("Not Found") } diff --git a/src/request.mbt b/src/request.mbt index 384b6b7..68949f9 100644 --- a/src/request.mbt +++ b/src/request.mbt @@ -8,7 +8,7 @@ pub(all) struct HttpRequest { ///| pub(open) trait BodyReader { - from_request(HttpRequest) -> Self raise + from_request(req: HttpRequest) -> Self raise } ///| diff --git a/src/response.mbt b/src/response.mbt index cc8ce64..a8ba5bc 100644 --- a/src/response.mbt +++ b/src/response.mbt @@ -1,6 +1,6 @@ ///| pub(all) struct HttpResponse { - mut status_code : &StatusCoder + mut status_code : StatusCode headers : Map[StringView, StringView] cookies : Map[String, CookieItem] mut raw_body : Bytes @@ -8,7 +8,7 @@ pub(all) struct HttpResponse { ///| pub fn HttpResponse::new( - status_code : &StatusCoder, + status_code : StatusCode, headers? : Map[StringView, StringView], cookies? : Map[String, CookieItem], raw_body? : Bytes, @@ -33,8 +33,8 @@ pub fn HttpResponse::body( } ///| -pub fn HttpResponse::json(self : HttpResponse, body : Json) -> HttpResponse { - self.body(body) +pub fn HttpResponse::json(self : HttpResponse, obj : &ToJson) -> HttpResponse { + self.body(obj.to_json()) } ///| diff --git a/src/static.mbt b/src/static.mbt index 255ebea..c3d44df 100644 --- a/src/static.mbt +++ b/src/static.mbt @@ -53,7 +53,7 @@ pub fn Mocket::static_assets( return next() } event.res.headers.set("Allow", "GET, HEAD") - return HttpResponse::new(405) + return HttpResponse::new(MethodNotAllowed) } let original_id = event.req.url[path.length():].to_string() catch { _ => return next() diff --git a/src/status_code.mbt b/src/status_code.mbt index 606788e..e7a7a34 100644 --- a/src/status_code.mbt +++ b/src/status_code.mbt @@ -1,13 +1,3 @@ -///| -pub trait StatusCoder { - to_int(self : Self) -> Int -} - -///| -pub impl StatusCoder for Int with to_int(self) -> Int { - self -} - ///| /// HTTP status codes as registered with IANA. /// See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml @@ -136,10 +126,15 @@ pub(all) enum StatusCode { NotExtended /// RFC 6585, 6 NetworkAuthenticationRequired + Custom(Int) } derive(Show, ToJson) +pub fn StatusCode::from_int(i : Int) -> StatusCode { + Custom(i) +} + ///| -pub impl StatusCoder for StatusCode with to_int(self) -> Int { +pub fn StatusCode::to_int(self: StatusCode) -> Int { match self { Continue => 100 SwitchingProtocols => 101 @@ -203,6 +198,7 @@ pub impl StatusCoder for StatusCode with to_int(self) -> Int { LoopDetected => 508 NotExtended => 510 NetworkAuthenticationRequired => 511 + Custom(i) => i } } From cf89a88731a56c678e3954269f1762ad5a8712f7 Mon Sep 17 00:00:00 2001 From: oboard Date: Tue, 25 Nov 2025 21:11:41 +0800 Subject: [PATCH 5/5] refactor: use direct status codes instead of @mocket namespace Replace @mocket.OK and @mocket.NotFound with direct status code references (OK, NotFound) for cleaner code and better readability --- src/cors/cors.mbt | 2 +- src/examples/responder/pkg.generated.mbti | 5 +++++ src/examples/route/main.mbt | 2 +- src/pkg.generated.mbti | 15 ++++++--------- src/request.mbt | 2 +- src/static_file/static_file.mbt | 4 ++-- src/status_code.mbt | 3 ++- 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/cors/cors.mbt b/src/cors/cors.mbt index e8d09ec..eea1c56 100644 --- a/src/cors/cors.mbt +++ b/src/cors/cors.mbt @@ -66,7 +66,7 @@ pub fn handle_cors( max_age~, ) // 对于预检请求,直接返回空响应,不调用next() - @mocket.HttpResponse::new(@mocket.OK).to_responder() + @mocket.HttpResponse::new(OK).to_responder() } else { append_cors_headers( event, diff --git a/src/examples/responder/pkg.generated.mbti b/src/examples/responder/pkg.generated.mbti index 683eaeb..0f51072 100644 --- a/src/examples/responder/pkg.generated.mbti +++ b/src/examples/responder/pkg.generated.mbti @@ -1,6 +1,10 @@ // Generated using `moon info`, DON'T EDIT IT package "oboard/mocket/examples/responder" +import( + "moonbitlang/core/json" +) + // Values // Errors @@ -8,6 +12,7 @@ package "oboard/mocket/examples/responder" // Types and methods type Person impl ToJson for Person +impl @json.FromJson for Person // Type aliases diff --git a/src/examples/route/main.mbt b/src/examples/route/main.mbt index ddf6eae..a784756 100644 --- a/src/examples/route/main.mbt +++ b/src/examples/route/main.mbt @@ -57,7 +57,7 @@ fn main { ..post("/echo", e => e.req) // 404 Page - ..get("/404", _ => @mocket.HttpResponse::new(@mocket.NotFound).body( + ..get("/404", _ => @mocket.HttpResponse::new(NotFound).body( @mocket.html( ( #| diff --git a/src/pkg.generated.mbti b/src/pkg.generated.mbti index 0ae022b..79c7ca1 100644 --- a/src/pkg.generated.mbti +++ b/src/pkg.generated.mbti @@ -96,15 +96,15 @@ fn HttpRequestInternal::on_complete(Self, FuncRef[() -> Unit]) -> Unit fn HttpRequestInternal::on_headers(Self, FuncRef[(@native.CStr) -> Unit]) -> Unit pub(all) struct HttpResponse { - mut status_code : &StatusCoder + mut status_code : StatusCode headers : Map[StringView, StringView] cookies : Map[String, CookieItem] mut raw_body : Bytes } fn HttpResponse::body(Self, &Responder) -> Self fn HttpResponse::delete_cookie(Self, String) -> Unit -fn HttpResponse::json(Self, Json) -> Self -fn HttpResponse::new(&StatusCoder, headers? : Map[StringView, StringView], cookies? : Map[String, CookieItem], raw_body? : Bytes) -> Self +fn HttpResponse::json(Self, &ToJson) -> Self +fn HttpResponse::new(StatusCode, headers? : Map[StringView, StringView], cookies? : Map[String, CookieItem], raw_body? : Bytes) -> Self fn HttpResponse::set_cookie(Self, String, String, max_age? : Int, path? : String, domain? : String, secure? : Bool, http_only? : Bool, same_site? : SameSiteOption) -> Unit fn HttpResponse::to_responder(Self) -> &Responder impl Responder for HttpResponse @@ -239,10 +239,12 @@ pub(all) enum StatusCode { LoopDetected NotExtended NetworkAuthenticationRequired + Custom(Int) } +fn StatusCode::from_int(Int) -> Self +fn StatusCode::to_int(Self) -> Int impl Show for StatusCode impl ToJson for StatusCode -impl StatusCoder for StatusCode pub enum WebSocketAggregatedMessage { Text(String) @@ -304,8 +306,3 @@ pub(open) trait ServeStaticProvider { get_fallthrough(Self) -> Bool } -pub trait StatusCoder { - to_int(Self) -> Int -} -impl StatusCoder for Int - diff --git a/src/request.mbt b/src/request.mbt index 68949f9..213767a 100644 --- a/src/request.mbt +++ b/src/request.mbt @@ -8,7 +8,7 @@ pub(all) struct HttpRequest { ///| pub(open) trait BodyReader { - from_request(req: HttpRequest) -> Self raise + from_request(req : HttpRequest) -> Self raise } ///| diff --git a/src/static_file/static_file.mbt b/src/static_file/static_file.mbt index b0f4cab..7c85c5e 100644 --- a/src/static_file/static_file.mbt +++ b/src/static_file/static_file.mbt @@ -26,10 +26,10 @@ pub impl ServeStaticProvider for StaticFileProvider with get_contents( self, id : String, ) -> &Responder { - let res : &Responder = @mocket.HttpResponse::new(@mocket.OK).body( + let res : &Responder = @mocket.HttpResponse::new(OK).body( @fs.read_file_to_bytes(self.path + "/" + id), ) catch { - _ => @mocket.HttpResponse::new(@mocket.NotFound).body("Not Found") + _ => @mocket.HttpResponse::new(NotFound).body("Not Found") } res } diff --git a/src/status_code.mbt b/src/status_code.mbt index e7a7a34..835ef25 100644 --- a/src/status_code.mbt +++ b/src/status_code.mbt @@ -129,12 +129,13 @@ pub(all) enum StatusCode { Custom(Int) } derive(Show, ToJson) +///| pub fn StatusCode::from_int(i : Int) -> StatusCode { Custom(i) } ///| -pub fn StatusCode::to_int(self: StatusCode) -> Int { +pub fn StatusCode::to_int(self : StatusCode) -> Int { match self { Continue => 100 SwitchingProtocols => 101