// Copyright 2026 Leo Cheng
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// The WebSocket surface, driven in-process. A `WebSocketHandler` answers the
/// opening handshake three ways - `Accept` (negotiating a subprotocol with
/// `select_subprotocol`), `Reject` with a bare close, or `DenyHttp` with a full
/// HTTP response - then echoes frames as `WsSend`. `TestClient::websocket`
/// captures the exchange as a `WsTestSession`; `ws_run` and `ws_run_app` are the
/// raw cores under it.
///
/// moon run src/examples/07-websocket
#coverage.skip
fn main {
let opt = fn(o : String?) -> String {
match o {
Some(v) => v
None => "-"
}
}
let opt_int = fn(o : Int32?) -> String {
match o {
Some(v) => v.to_string()
None => "-"
}
}
let c = @moonasgi.TestClient::new(fn(_r) { @moonasgi.Response::text("") })
// 1. Echo with subprotocol negotiation: pick the first offered value the
// server also supports.
let echo = @moonasgi.WebSocketHandler::new(
on_connect=fn(s) {
@ws.Accept(subprotocol=s.select_subprotocol(["chat.v2", "chat.v1"]), headers=[
("x-ws", "ok"),
])
},
on_receive=fn(m) {
match m {
@ws.Text(t) => [@ws.SendText("echo:" + t)]
@ws.Binary(b) => [@ws.SendBinary(b)]
}
},
)
let s = c.websocket(
path="/ws",
handler=echo,
subprotocols=["chat.v1", "chat.v3"],
send=[@ws.Text("emm"), @ws.Text("hhh")],
)
println(
"echo: accepted=\{s.accepted} subprotocol=\{opt(s.subprotocol)} x-ws=\{opt(s.header("x-ws"))}",
)
println("echo texts: " + s.texts().join(", "))
// 2. Reject the handshake with a bare close code.
let reject = @moonasgi.WebSocketHandler::new(on_connect=fn(_s) {
@ws.Reject(code=4403, reason="forbidden")
})
let sr = c.websocket(path="/ws", handler=reject, send=[@ws.Text("ignored")])
println(
"reject: accepted=\{sr.accepted} closed=\{sr.closed} code=\{opt_int(sr.close_code)}",
)
// 3. Deny the handshake with a full HTTP response (websocket.http.response).
let deny = @moonasgi.WebSocketHandler::new(on_connect=fn(_s) {
@ws.DenyHttp(status=401, headers=[("x-why", "auth")], body=b"denied")
})
let sd = c.websocket(path="/ws", handler=deny)
match sd.denial {
Some(resp) => println("deny: status=\{resp.status} body=" + resp.text())
None => ()
}
// 4. The built-in echo handler over the raw ws_run core.
let raw = @moonasgi.ws_run(
@moonasgi.WebSocketHandler::echo(subprotocol="chat.v1"),
@moonasgi.Scope::WebSocket(
@moonasgi.WebSocketScope::new(path="/ws", subprotocols=["chat.v1"]),
),
[
@spec.WebSocketConnect,
@spec.WebSocketReceive(text=Some("qwq"), bytes=None),
@spec.WebSocketDisconnect(code=1000, reason=None),
],
)
let ws_ok = @moonasgi.validate_ws_response(raw) is None
println("ws_run -> \{raw.length()} outbound events, well-formed: \{ws_ok}")
// 5. ws_run_app / websocket_app: the escape hatch sees the whole scope + stream.
let counted = c.websocket_app(
path="/ws",
app=fn(scope, inbound) {
[
@spec.WebSocketAccept(subprotocol=scope.subprotocols.get(0), headers=[
("x-frames", inbound.length().to_string()),
]),
]
},
subprotocols=["v1"],
send=[@ws.Text("a"), @ws.Text("b")],
disconnect=None,
)
println(
"websocket_app: subprotocol=\{opt(counted.subprotocol)} x-frames=\{opt(counted.header("x-frames"))}",
)
}