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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ packages = [
{ name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
{ name = "gleam_otp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7987CBEBC8060B88F14575DEF546253F3116EBE2A5DA6FD82F38243FCE97C54B" },
{ name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" },
{ name = "gleam_otp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7987CBEBC8060B88F14575DEF546253F3116EBE2A5DA6FD82F38243FCE97C54B" },
{ name = "gleam_stdlib", version = "0.64.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "EA2E13FC4E65750643E078487D5EF360BEBCA5EBBBA12042FB589C19F53E35C0" },
{ name = "gleam_time", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "DCDDC040CE97DA3D2A925CDBBA08D8A78681139745754A83998641C8A3F6587E" },
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
{ name = "glearray", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "5E272F7CB278CC05A929C58DEB58F5D5AC6DB5B879A681E71138658D0061C38A" },
Expand Down
19 changes: 19 additions & 0 deletions examples/src/working_with_websockets/app.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import gleam/erlang/process
import mist
import wisp
import wisp/wisp_mist
import working_with_websockets/app/router

pub fn main() {
wisp.configure_logger()
let secret_key_base = wisp.random_string(64)

let assert Ok(_) =
router.handle_request
|> wisp_mist.handler(secret_key_base)
|> mist.new
|> mist.port(8001)
|> mist.start

process.sleep_forever()
}
146 changes: 146 additions & 0 deletions examples/src/working_with_websockets/app/router.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import gleam/int
import gleam/option
import wisp.{type Request, type Response}
import wisp/websocket

pub fn handle_request(request: Request) -> Response {
use <- wisp.log_request(request)

case wisp.path_segments(request) {
[] -> home_page()
["websocket"] -> websocket_handler(request)
_ -> wisp.not_found()
}
}

fn home_page() -> Response {
let html =
"<!DOCTYPE html>
<html>
<head>
<title>WebSocket Echo Example</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
#messages { border: 1px solid #ccc; height: 300px; overflow-y: scroll; padding: 10px; margin: 10px 0; }
input[type=\"text\"] { width: 300px; padding: 5px; }
button { padding: 5px 10px; }
.message { margin: 5px 0; }
.sent { color: blue; }
.received { color: green; }
.status { color: red; font-weight: bold; }
</style>
</head>
<body>
<h1>WebSocket Echo Example</h1>
<p>This example demonstrates WebSocket support in Wisp.</p>

<div>
<button onclick=\"connect()\">Connect</button>
<button onclick=\"disconnect()\">Disconnect</button>
<span id=\"status\">Disconnected</span>
</div>

<div id=\"messages\"></div>

<div>
<input type=\"text\" id=\"messageInput\" placeholder=\"Type your message...\" onkeypress=\"if(event.key==='Enter') sendMessage()\">
<button onclick=\"sendMessage()\">Send</button>
</div>

<script>
let ws = null;
const messages = document.getElementById('messages');
const status = document.getElementById('status');
const messageInput = document.getElementById('messageInput');

function addMessage(content, className) {
const div = document.createElement('div');
div.className = 'message ' + className;
div.textContent = content;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}

function connect() {
if (ws) {
ws.close();
}

ws = new WebSocket('ws://localhost:8001/websocket');

ws.onopen = function(event) {
status.textContent = 'Connected';
addMessage('Connected to WebSocket', 'status');
};

ws.onmessage = function(event) {
addMessage('Received: ' + event.data, 'received');
};

ws.onclose = function(event) {
status.textContent = 'Disconnected';
addMessage('WebSocket connection closed', 'status');
};

ws.onerror = function(error) {
addMessage('WebSocket error: ' + error, 'status');
};
}

function disconnect() {
if (ws) {
ws.close();
}
}

function sendMessage() {
if (ws && ws.readyState === WebSocket.OPEN) {
const message = messageInput.value;
if (message) {
ws.send(message);
addMessage('Sent: ' + message, 'sent');
messageInput.value = '';
}
} else {
addMessage('WebSocket is not connected', 'status');
}
}
</script>
</body>
</html>"

wisp.html_response(html, 200)
}

fn websocket_handler(request: Request) -> Response {
wisp.websocket(
request,
on_init: fn(_connection) { #(0, option.None) },
on_message: fn(state, message, connection) {
case message {
websocket.Text(text) -> {
let count = state + 1
let response = "Echo #" <> int.to_string(count) <> ": " <> text
case websocket.send_text(connection, response) {
Ok(_) -> websocket.Continue(count)
Error(_) -> websocket.StopWithError("Failed to send message")
}
}
websocket.Binary(binary) -> {
case websocket.send_binary(connection, binary) {
Ok(_) -> websocket.Continue(state)
Error(_) -> websocket.StopWithError("Failed to send binary message")
}
}
websocket.Closed -> websocket.Stop
websocket.Shutdown -> websocket.Stop
websocket.Custom(_) -> websocket.Stop
}
},
on_close: fn(state) {
wisp.log_info(
"Connection closed after: " <> int.to_string(state) <> " messages",
)
},
)
}
138 changes: 138 additions & 0 deletions examples/test/working_with_websockets/app_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import gleam/http
import wisp
import wisp/simulate
import working_with_websockets/app/router

pub fn get_home_page_test() {
let request = simulate.browser_request(http.Get, "/")
let response = router.handle_request(request)

assert response.status == 200
assert response.headers == [#("content-type", "text/html; charset=utf-8")]
}

pub fn page_not_found_test() {
let request = simulate.browser_request(http.Get, "/nothing-here")
let response = router.handle_request(request)

assert response.status == 404
}

pub fn websocket_upgrade_test() {
let request = simulate.websocket_request(http.Get, "/websocket")
let response = router.handle_request(request)

let assert wisp.WebSocket(_) = response.body
}

pub fn websocket_text_echo_test() {
let request = simulate.websocket_request(http.Get, "/websocket")
let response = router.handle_request(request)

let assert wisp.WebSocket(upgrade) = response.body
let handler = wisp.recover(upgrade)
let assert Ok(ws) = simulate.create_websocket(handler)

// Send first text message
let assert Ok(ws) = simulate.send_websocket_text(ws, "Hello")
let assert ["Echo #1: Hello"] = simulate.websocket_sent_text_messages(ws)
let assert [] = simulate.websocket_sent_binary_messages(ws)

// Send second text message
let assert Ok(ws) = simulate.send_websocket_text(ws, "World")
let assert ["Echo #1: Hello", "Echo #2: World"] =
simulate.websocket_sent_text_messages(ws)

// Send third text message
let assert Ok(ws) = simulate.send_websocket_text(ws, "!")
let assert ["Echo #1: Hello", "Echo #2: World", "Echo #3: !"] =
simulate.websocket_sent_text_messages(ws)
}

pub fn websocket_binary_echo_test() {
let request = simulate.websocket_request(http.Get, "/websocket")
let response = router.handle_request(request)

let assert wisp.WebSocket(upgrade) = response.body
let handler = wisp.recover(upgrade)
let assert Ok(ws) = simulate.create_websocket(handler)

// Send binary message
let assert Ok(ws) = simulate.send_websocket_binary(ws, <<1, 2, 3>>)
let assert [<<1, 2, 3>>] = simulate.websocket_sent_binary_messages(ws)
let assert [] = simulate.websocket_sent_text_messages(ws)

// Send another binary message
let assert Ok(ws) = simulate.send_websocket_binary(ws, <<4, 5, 6>>)
let assert [<<1, 2, 3>>, <<4, 5, 6>>] =
simulate.websocket_sent_binary_messages(ws)
}

pub fn websocket_mixed_messages_test() {
let request = simulate.websocket_request(http.Get, "/websocket")
let response = router.handle_request(request)

let assert wisp.WebSocket(upgrade) = response.body
let handler = wisp.recover(upgrade)
let assert Ok(ws) = simulate.create_websocket(handler)

// Send text message
let assert Ok(ws) = simulate.send_websocket_text(ws, "Text message")
let assert ["Echo #1: Text message"] =
simulate.websocket_sent_text_messages(ws)

// Send binary message (doesn't increment count)
let assert Ok(ws) = simulate.send_websocket_binary(ws, <<7, 8, 9>>)
let assert [<<7, 8, 9>>] = simulate.websocket_sent_binary_messages(ws)

// Send another text message (count should be 2)
let assert Ok(ws) = simulate.send_websocket_text(ws, "Another text")
let assert ["Echo #1: Text message", "Echo #2: Another text"] =
simulate.websocket_sent_text_messages(ws)
}

pub fn websocket_close_test() {
let request = simulate.websocket_request(http.Get, "/websocket")
let response = router.handle_request(request)

let assert wisp.WebSocket(upgrade) = response.body
let handler = wisp.recover(upgrade)
let assert Ok(ws) = simulate.create_websocket(handler)

// Send some messages
let assert Ok(ws) = simulate.send_websocket_text(ws, "First")
let assert Ok(ws) = simulate.send_websocket_text(ws, "Second")
let assert Ok(ws) = simulate.send_websocket_text(ws, "Third")

let assert ["Echo #1: First", "Echo #2: Second", "Echo #3: Third"] =
simulate.websocket_sent_text_messages(ws)

// Close the websocket - should succeed
let assert Ok(Nil) = simulate.close_websocket(ws)
}

pub fn websocket_closed_ignores_messages_test() {
let request = simulate.websocket_request(http.Get, "/websocket")
let response = router.handle_request(request)

let assert wisp.WebSocket(upgrade) = response.body
let handler = wisp.recover(upgrade)
let assert Ok(ws) = simulate.create_websocket(handler)

// Send a message
let assert Ok(ws) = simulate.send_websocket_text(ws, "Before close")
let assert ["Echo #1: Before close"] =
simulate.websocket_sent_text_messages(ws)

// Close the websocket
let assert Ok(Nil) = simulate.close_websocket(ws)

// Try to send messages after closing
let assert Ok(ws) = simulate.send_websocket_text(ws, "After close")
let assert Ok(ws) = simulate.send_websocket_binary(ws, <<10, 11, 12>>)

// Messages should not be processed
let assert ["Echo #1: Before close"] =
simulate.websocket_sent_text_messages(ws)
let assert [] = simulate.websocket_sent_binary_messages(ws)
}
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ logging = ">= 1.2.0 and < 2.0.0"
directories = ">= 1.0.0 and < 2.0.0"
houdini = ">= 1.0.0 and < 2.0.0"
filepath = ">= 1.1.2 and < 2.0.0"
gleam_otp = ">= 1.1.0 and < 2.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
1 change: 1 addition & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ gleam_crypto = { version = ">= 1.0.0 and < 2.0.0" }
gleam_erlang = { version = ">= 1.0.0 and < 2.0.0" }
gleam_http = { version = ">= 3.5.0 and < 5.0.0" }
gleam_json = { version = ">= 3.0.0 and < 4.0.0" }
gleam_otp = { version = ">= 1.1.0 and < 2.0.0" }
gleam_stdlib = { version = ">= 0.50.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
houdini = { version = ">= 1.0.0 and < 2.0.0" }
Expand Down
Loading