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/body_reader.mbt b/src/content_type.mbt
similarity index 51%
rename from src/body_reader.mbt
rename to src/content_type.mbt
index e689c78..0c3479b 100644
--- a/src/body_reader.mbt
+++ b/src/content_type.mbt
@@ -1,51 +1,3 @@
-///|
-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)
-}
-
///|
priv struct ContentType {
media_type : StringView
diff --git a/src/cors/cors.mbt b/src/cors/cors.mbt
index 584321e..eea1c56 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::new(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/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/responder/main.mbt b/src/examples/responder/main.mbt
new file mode 100644
index 0000000..d79109b
--- /dev/null
+++ b/src/examples/responder/main.mbt
@@ -0,0 +1,65 @@
+///|
+using @mocket {type HttpResponse}
+
+///|
+struct Person {
+ name : String
+ age : Int
+} derive(ToJson, FromJson)
+
+///|
+impl @mocket.BodyReader for Person with from_request(req) -> Person raise {
+ @json.from_json(req.body())
+}
+
+///|
+fn main {
+ let app = @mocket.new()
+
+ // Text Response
+ app
+ ..get("/", _event => "⚡️ Tadaa!")
+
+ // Object Response
+ ..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", _ => HttpResponse::new(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..0f51072
--- /dev/null
+++ b/src/examples/responder/pkg.generated.mbti
@@ -0,0 +1,20 @@
+// Generated using `moon info`, DON'T EDIT IT
+package "oboard/mocket/examples/responder"
+
+import(
+ "moonbitlang/core/json"
+)
+
+// Values
+
+// Errors
+
+// Types and methods
+type Person
+impl ToJson for Person
+impl @json.FromJson for Person
+
+// Type aliases
+
+// Traits
+
diff --git a/src/examples/route/main.mbt b/src/examples/route/main.mbt
index ee9d666..a784756 100644
--- a/src/examples/route/main.mbt
+++ b/src/examples/route/main.mbt
@@ -1,20 +1,17 @@
///|
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
- ..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,52 +20,45 @@ fn main {
)
next()
})
- group.get("/hello", _ => Text("Hello world!"))
- group.get("/json", _ => Json({
- "name": "John Doe",
- "age": 30,
- "city": "New York",
- }))
+ group.get("/hello", _ => "Hello world!")
+ group.get("/json", _ => (
+ { "name": "John Doe", "age": 30, "city": "New York" } : Json))
})
// JSON Response
- ..get("/json", _event => 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 {
- Json({ "name": "John Doe", "age": 30, "city": "New York" })
+ ({ "name": "John Doe", "age": 30, "city": "New York" } : Json)
})
// 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(
+ ..get("/404", _ => @mocket.HttpResponse::new(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 ec8eb2d..8cebc17 100644
--- a/src/examples/websocket/websocket_echo.mbt
+++ b/src/examples/websocket/websocket_echo.mbt
@@ -1,16 +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()
- _ => ""
+ 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()
+ }
}
- println("WS message: " + msg)
- peer.send(msg)
- }
Close(peer) => println("WS close: " + peer.to_string())
})
println("WebSocket echo server listening on ws://localhost:8080/ws")
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..32da44e 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 {
@@ -47,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)]
@@ -58,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: {},
@@ -77,14 +39,6 @@ pub fn new(
}
}
-///|
-pub(all) struct HttpRequest {
- http_method : String
- url : String
- headers : Map[StringView, StringView]
- mut body : HttpBody
-}
-
///|
///|
@@ -151,14 +105,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,
@@ -167,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 => {
@@ -286,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/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/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..8d54761 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)).toString('utf8');
- #| // 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, () => {});
#|}
///|
@@ -246,20 +273,16 @@ 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: {
http_method: req.req_method(),
url: req.url(),
- body: Empty,
headers: string_headers,
+ raw_body: "",
},
- res: { status_code: 200, headers: {}, cookies: {} },
+ res: HttpResponse::new(OK),
params,
}
async_run(() => {
@@ -271,23 +294,15 @@ 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 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(),
@@ -302,17 +317,10 @@ 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()
+ responder.output(buf)
+ event.res.raw_body = buf.to_bytes()
+ res.end(@js.Value::cast_from(event.res.raw_body))
})
},
port,
@@ -355,7 +363,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 +390,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, Text(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))
_ => ()
}
@@ -391,7 +404,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 (_) {} }; }"
///|
@@ -468,6 +481,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); }"
@@ -478,7 +497,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/mocket.native.mbt b/src/mocket.native.mbt
index 41b91f8..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,56 +184,37 @@ 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, 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()),
+ ))
+ let buf = @buffer.new()
+ responder.output(buf)
+ event.res.raw_body = buf.to_bytes()
+ res.end_bytes(event.res.raw_body)
})
}
@@ -245,7 +226,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 +234,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 +277,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/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/not_found.mbt b/src/not_found.mbt
new file mode 100644
index 0000000..7c82ea8
--- /dev/null
+++ b/src/not_found.mbt
@@ -0,0 +1,4 @@
+///|
+pub async fn handle_not_found(_ : MocketEvent) -> &Responder noraise {
+ HttpResponse::new(NotFound).body("Not Found")
+}
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..79c7ca1 100644
--- a/src/pkg.generated.mbti
+++ b/src/pkg.generated.mbti
@@ -3,7 +3,7 @@ package "oboard/mocket"
import(
"illusory0x0/native"
- "moonbitlang/core/builtin"
+ "moonbitlang/core/buffer"
)
// Values
@@ -13,17 +13,15 @@ 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
+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
-fn new(base_path? : String, logger? : Logger) -> Mocket
+async fn handle_not_found(MocketEvent) -> &Responder noraise
-fn new_debug_logger() -> Logger
+fn html(&Show) -> &Responder
-fn new_logger(enabled? : Bool, level? : LogLevel) -> Logger
-
-fn new_production_logger() -> Logger
+fn new(base_path? : String) -> Mocket
fn parse_cookie(StringView) -> Map[String, CookieItem]
@@ -37,25 +35,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,24 +77,18 @@ 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
@@ -104,12 +96,18 @@ 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 : Int
+ 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, &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
#external
pub type HttpResponseInternal
@@ -117,62 +115,34 @@ 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
-
#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 +166,95 @@ 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?
+}
+fn StaticAssetMeta::new(asset_type? : String, etag? : String, mtime? : Int64, path? : String, size? : Int64, encoding? : String) -> Self
+
+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
+ Custom(Int)
+}
+fn StatusCode::from_int(Int) -> Self
+fn StatusCode::to_int(Self) -> Int
+impl Show for StatusCode
+impl ToJson 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 +262,47 @@ 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
+}
diff --git a/src/request.mbt b/src/request.mbt
new file mode 100644
index 0000000..213767a
--- /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(req : 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
new file mode 100644
index 0000000..1bf9c19
--- /dev/null
+++ b/src/responder.mbt
@@ -0,0 +1,99 @@
+///|
+pub(open) trait Responder {
+ options(Self, res : HttpResponse) -> Unit
+ output(Self, buf : @buffer.Buffer) -> Unit
+}
+
+///|
+pub impl Responder for &ToJson with options(_, res) -> Unit {
+ 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.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.headers["Content-Type"] = "application/json; charset=utf-8"
+}
+
+///|
+pub impl Responder for Json with output(self, buf) -> Unit {
+ buf.write_bytes(@encoding/utf8.encode(self.to_json().stringify()))
+}
+
+///|
+pub impl Responder for Bytes with options(_, res) -> Unit {
+ 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.headers["Content-Type"] = "text/plain; charset=utf-8"
+}
+
+///|
+pub impl Responder for String with output(self, buf) -> Unit {
+ buf.write_bytes(@encoding/utf8.encode(self))
+}
+
+///|
+pub impl Responder for StringView with options(_, res) -> Unit {
+ res.headers["Content-Type"] = "text/plain; charset=utf-8"
+}
+
+///|
+pub impl Responder for StringView with output(self, buf) -> Unit {
+ buf.write_bytes(@encoding/utf8.encode(self))
+}
+
+///|
+struct Html(String)
+
+///|
+pub impl Responder for Html with options(_, res) -> Unit {
+ res.headers["Content-Type"] = "text/html; charset=utf-8"
+}
+
+///|
+pub impl Responder for Html with output(self, buf) -> Unit {
+ buf.write_bytes(@encoding/utf8.encode(self.0))
+}
+
+///|
+pub fn html(html : &Show) -> &Responder {
+ Html(html.to_string())
+}
+
+///|
+pub fn text(text : &Show) -> &Responder {
+ text.to_string()
+}
diff --git a/src/response.mbt b/src/response.mbt
new file mode 100644
index 0000000..a8ba5bc
--- /dev/null
+++ b/src/response.mbt
@@ -0,0 +1,43 @@
+///|
+pub(all) struct HttpResponse {
+ mut status_code : StatusCode
+ headers : Map[StringView, StringView]
+ cookies : Map[String, CookieItem]
+ mut raw_body : Bytes
+}
+
+///|
+pub fn HttpResponse::new(
+ status_code : StatusCode,
+ 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::body(
+ self : HttpResponse,
+ body : &Responder,
+) -> HttpResponse {
+ let buf = @buffer.new()
+ body.output(buf)
+ self.raw_body = buf.to_bytes()
+ self
+}
+
+///|
+pub fn HttpResponse::json(self : HttpResponse, obj : &ToJson) -> HttpResponse {
+ self.body(obj.to_json())
+}
+
+///|
+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..c3d44df
--- /dev/null
+++ b/src/static.mbt
@@ -0,0 +1,184 @@
+///|
+pub(all) struct StaticAssetMeta {
+ asset_type : String?
+ etag : String?
+ mtime : Int64?
+ path : String?
+ size : Int64?
+ 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
+ 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,
+ provider : &ServeStaticProvider,
+) -> Unit {
+ self.use_middleware(async fn(event, next) noraise {
+ 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 provider.get_fallthrough() {
+ return next()
+ }
+ event.res.headers.set("Allow", "GET, HEAD")
+ return HttpResponse::new(MethodNotAllowed)
+ }
+ 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").unwrap_or("")
+ let encodings = provider.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 = provider.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 provider.get_meta(try_id) {
+ Some(m) => {
+ meta = Some(m)
+ id = try_id
+ found = true
+ break
+ }
+ None => ()
+ }
+ }
+ }
+ match meta {
+ None => {
+ if provider.get_fallthrough() {
+ return next()
+ }
+ return HttpResponse::new(NotFound)
+ }
+ 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::new(NotModified)
+ }
+ }
+ 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 {
+ match provider.get_type(parts[parts.length() - 1].to_string()) {
+ Some(t) => event.res.headers.set("Content-Type", t)
+ None => ()
+ }
+ }
+ }
+ }
+ }
+
+ // 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::new(OK)
+ }
+ let contents = provider.get_contents(id)
+ event.res.status_code = OK
+ 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/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
new file mode 100644
index 0000000..7c85c5e
--- /dev/null
+++ b/src/static_file/static_file.mbt
@@ -0,0 +1,63 @@
+// ///|
+
+///|
+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? {
+ Some(@mocket.StaticAssetMeta::new(path=self.path + "/" + id))
+}
+
+///|
+pub impl ServeStaticProvider for StaticFileProvider with get_contents(
+ self,
+ id : String,
+) -> &Responder {
+ let res : &Responder = @mocket.HttpResponse::new(OK).body(
+ @fs.read_file_to_bytes(self.path + "/" + id),
+ ) catch {
+ _ => @mocket.HttpResponse::new(NotFound).body("Not Found")
+ }
+ res
+}
+
+///|
+pub impl ServeStaticProvider for StaticFileProvider with get_type(_, _ : String) -> String? {
+ None
+}
+
+///|
+pub impl ServeStaticProvider for StaticFileProvider with get_encodings(_) -> Map[
+ String,
+ String,
+] {
+ {}
+}
+
+///|
+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(_) -> Bool {
+ false
+}
diff --git a/src/status_code.mbt b/src/status_code.mbt
new file mode 100644
index 0000000..835ef25
--- /dev/null
+++ b/src/status_code.mbt
@@ -0,0 +1,209 @@
+///|
+/// 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
+ Custom(Int)
+} derive(Show, ToJson)
+
+///|
+pub fn StatusCode::from_int(i : Int) -> StatusCode {
+ Custom(i)
+}
+
+///|
+pub fn StatusCode::to_int(self : StatusCode) -> 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
+ Custom(i) => i
+ }
+}
+
+///|
+test {
+ inspect(StatusCode::OK.to_int(), content="200")
+}
diff --git a/src/websocket.mbt b/src/websocket.mbt
index c7616eb..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, HttpBody)
+ 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"));