+1
gleam.toml
+1
gleam.toml
+3
manifest.toml
+3
manifest.toml
···
5
{ name = "compresso", version = "0.1.0", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_stdlib", "gleam_yielder", "logging"], otp_app = "compresso", source = "hex", outer_checksum = "8BE29A1EDA42F70826ED148EAE40C46BB3FC18E78FE472663DB01DD4A38172D4" },
6
{ name = "ewe", version = "2.0.2", build_tools = ["gleam"], requirements = ["compresso", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "logging"], otp_app = "ewe", source = "hex", outer_checksum = "55FABC310F9C0EC67021F6CA973ED59A37481E39E6A850813D5B6F4CEA241A68" },
7
{ name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" },
8
{ name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" },
9
{ name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
10
{ name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" },
11
{ name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
12
{ name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" },
13
{ name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" },
14
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
15
{ name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" },
16
{ name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" },
···
22
23
[requirements]
24
ewe = { version = ">= 2.0.1 and < 3.0.0" }
25
gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" }
26
gleam_http = { version = ">= 4.3.0 and < 5.0.0" }
27
gleam_json = { version = ">= 3.0.2 and < 4.0.0" }
···
5
{ name = "compresso", version = "0.1.0", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_stdlib", "gleam_yielder", "logging"], otp_app = "compresso", source = "hex", outer_checksum = "8BE29A1EDA42F70826ED148EAE40C46BB3FC18E78FE472663DB01DD4A38172D4" },
6
{ name = "ewe", version = "2.0.2", build_tools = ["gleam"], requirements = ["compresso", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "logging"], otp_app = "ewe", source = "hex", outer_checksum = "55FABC310F9C0EC67021F6CA973ED59A37481E39E6A850813D5B6F4CEA241A68" },
7
{ name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" },
8
+
{ name = "formal", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "formal", source = "hex", outer_checksum = "686D0C4C9CB36397DBAC2EC8C6ED9BD0F81B2DF2E88F75A7DB75F56768DDD8FC" },
9
{ name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" },
10
{ name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
11
{ name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" },
12
{ name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
13
{ name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" },
14
{ name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" },
15
+
{ name = "gleam_time", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "DCDDC040CE97DA3D2A925CDBBA08D8A78681139745754A83998641C8A3F6587E" },
16
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
17
{ name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" },
18
{ name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" },
···
24
25
[requirements]
26
ewe = { version = ">= 2.0.1 and < 3.0.0" }
27
+
formal = { version = ">= 3.0.0 and < 4.0.0" }
28
gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" }
29
gleam_http = { version = ">= 4.3.0 and < 5.0.0" }
30
gleam_json = { version = ">= 3.0.2 and < 4.0.0" }
+34
-85
src/clover.gleam
+34
-85
src/clover.gleam
···
1
import clover/counter
2
import clover/data
3
import ewe
4
-
import gleam/bytes_tree
5
import gleam/erlang/application
6
import gleam/erlang/process
7
import gleam/http/request
8
import gleam/http/response
9
import gleam/int
10
-
import gleam/json
11
import gleam/option
12
import logging
13
import lustre
14
-
import lustre/attribute as attr
15
-
import lustre/element
16
-
import lustre/element/html
17
-
import lustre/server_component
18
19
pub fn main() {
20
logging.configure()
···
45
use req <- log_req(req)
46
47
case request.path_segments(req) {
48
-
[] -> serve_html()
49
["lustre", "runtime.mjs"] -> serve_runtime()
50
["ws"] -> serve_counter(req, counter)
51
_ -> data.not_found()
52
}
53
}
54
55
-
fn serve_html() -> response.Response(ewe.ResponseBody) {
56
-
let html =
57
-
html.html([attr.lang("en")], [
58
-
html.head([], [
59
-
html.meta([attr.charset("utf-8")]),
60
-
html.meta([
61
-
attr.name("viewport"),
62
-
attr.content("width=device-width, initial-scale=1"),
63
-
]),
64
-
html.title([], "Clover"),
65
-
html.script([attr.type_("module"), attr.src("/lustre/runtime.mjs")], ""),
66
-
]),
67
-
html.body(
68
-
[attr.styles([#("max-width", "32rem"), #("margin", "3rem auto")])],
69
-
[server_component.element([server_component.route("/ws")], [])],
70
-
),
71
-
])
72
-
|> element.to_document_string_tree()
73
-
|> bytes_tree.from_string_tree()
74
-
75
-
response.new(200)
76
-
|> data.html_response(html)
77
}
78
79
fn serve_runtime() -> response.Response(ewe.ResponseBody) {
···
96
req: request.Request(ewe.Connection),
97
counter: lustre.Runtime(counter.Msg),
98
) -> response.Response(ewe.ResponseBody) {
99
-
let on_init = fn(a, b) { init_counter_socket(a, b, counter) }
100
101
req
102
|> ewe.upgrade_websocket(
103
on_init:,
104
-
handler: loop_counter_socket,
105
-
on_close: close_counter_socket,
106
)
107
}
108
-
109
-
type CounterSocket {
110
-
CounterSocket(
111
-
component: lustre.Runtime(counter.Msg),
112
-
self: process.Subject(server_component.ClientMessage(counter.Msg)),
113
-
)
114
-
}
115
-
116
-
fn init_counter_socket(
117
-
_: ewe.WebsocketConnection,
118
-
_: process.Selector(_),
119
-
counter counter: lustre.Runtime(counter.Msg),
120
-
) -> #(CounterSocket, process.Selector(_)) {
121
-
let self = process.new_subject()
122
-
let selector =
123
-
process.new_selector()
124
-
|> process.select(self)
125
-
126
-
server_component.register_subject(self)
127
-
|> lustre.send(to: counter)
128
-
129
-
#(CounterSocket(component: counter, self:), selector)
130
-
}
131
-
132
-
fn loop_counter_socket(
133
-
conn: ewe.WebsocketConnection,
134
-
value: CounterSocket,
135
-
msg: ewe.WebsocketMessage(server_component.ClientMessage(counter.Msg)),
136
-
) -> ewe.WebsocketNext(
137
-
CounterSocket,
138
-
server_component.ClientMessage(counter.Msg),
139
-
) {
140
-
case msg {
141
-
ewe.Text(json) -> {
142
-
case json.parse(json, server_component.runtime_message_decoder()) {
143
-
Ok(runtime_msg) -> lustre.send(value.component, runtime_msg)
144
-
Error(_) -> Nil
145
-
}
146
-
ewe.websocket_continue(value)
147
-
}
148
-
ewe.Binary(_) -> ewe.websocket_continue(value)
149
-
ewe.User(client_msg) -> {
150
-
let json = server_component.client_message_to_json(client_msg)
151
-
let assert Ok(_) = ewe.send_text_frame(conn, json.to_string(json))
152
-
ewe.websocket_continue(value)
153
-
}
154
-
}
155
-
}
156
-
157
-
fn close_counter_socket(_: ewe.WebsocketConnection, value: CounterSocket) -> Nil {
158
-
server_component.deregister_subject(value.self)
159
-
|> lustre.send(to: value.component)
160
-
}
···
1
import clover/counter
2
import clover/data
3
+
import clover/pages/index
4
+
import clover/pages/room
5
import ewe
6
+
import formal/form
7
import gleam/erlang/application
8
import gleam/erlang/process
9
import gleam/http/request
10
import gleam/http/response
11
import gleam/int
12
+
import gleam/list
13
import gleam/option
14
+
import gleam/string
15
+
import gleam/uri
16
import logging
17
import lustre
18
19
pub fn main() {
20
logging.configure()
···
45
use req <- log_req(req)
46
47
case request.path_segments(req) {
48
+
[] -> data.page(index.view())
49
+
["new"] -> {
50
+
case list.key_find(req.headers, "content-type") {
51
+
Ok("application/x-www-form-urlencoded")
52
+
| Ok("application/x-www-form-urlencoded; " <> _) -> {
53
+
use body <- data.require_string_body(req)
54
+
case uri.parse_query(body) {
55
+
Error(_) -> data.bad_request("Invalid form encoding")
56
+
Ok(pairs) -> {
57
+
let pairs = sort_keys(pairs)
58
+
let form = index.room_form() |> form.add_values(pairs)
59
+
case form.run(form) {
60
+
Ok(data) -> data.redirect("/room/" <> data.id)
61
+
Error(_) -> data.redirect("/")
62
+
}
63
+
}
64
+
}
65
+
}
66
+
_ -> data.redirect("/")
67
+
}
68
+
}
69
+
["room", id] -> data.page(room.view(id))
70
+
["room", id, "html"] -> data.page(room.view_html(id))
71
["lustre", "runtime.mjs"] -> serve_runtime()
72
["ws"] -> serve_counter(req, counter)
73
_ -> data.not_found()
74
}
75
}
76
77
+
fn sort_keys(pairs: List(#(String, t))) -> List(#(String, t)) {
78
+
list.sort(pairs, fn(a, b) { string.compare(a.0, b.0) })
79
}
80
81
fn serve_runtime() -> response.Response(ewe.ResponseBody) {
···
98
req: request.Request(ewe.Connection),
99
counter: lustre.Runtime(counter.Msg),
100
) -> response.Response(ewe.ResponseBody) {
101
+
let on_init = fn(a, b) { counter.init_socket(a, b, counter) }
102
103
req
104
|> ewe.upgrade_websocket(
105
on_init:,
106
+
handler: counter.loop_socket,
107
+
on_close: counter.close_socket,
108
)
109
}
+56
src/clover/counter.gleam
+56
src/clover/counter.gleam
···
1
import gleam/int
2
import lustre
3
import lustre/attribute as attr
4
import lustre/element
5
import lustre/element/html
6
import lustre/event
7
8
pub fn component() -> lustre.App(_, Model, Msg) {
9
lustre.simple(init, update, view)
···
44
fn view_button(string: String, msg: Msg) -> element.Element(Msg) {
45
html.button([event.on_click(msg)], [html.text(string)])
46
}
···
1
+
import ewe
2
+
import gleam/erlang/process
3
import gleam/int
4
+
import gleam/json
5
import lustre
6
import lustre/attribute as attr
7
import lustre/element
8
import lustre/element/html
9
import lustre/event
10
+
import lustre/server_component
11
12
pub fn component() -> lustre.App(_, Model, Msg) {
13
lustre.simple(init, update, view)
···
48
fn view_button(string: String, msg: Msg) -> element.Element(Msg) {
49
html.button([event.on_click(msg)], [html.text(string)])
50
}
51
+
52
+
// Server Component stuff
53
+
54
+
pub type Socket {
55
+
Socket(
56
+
component: lustre.Runtime(Msg),
57
+
self: process.Subject(server_component.ClientMessage(Msg)),
58
+
)
59
+
}
60
+
61
+
pub fn init_socket(
62
+
_: ewe.WebsocketConnection,
63
+
_: process.Selector(_),
64
+
counter counter: lustre.Runtime(Msg),
65
+
) -> #(Socket, process.Selector(_)) {
66
+
let self = process.new_subject()
67
+
let selector =
68
+
process.new_selector()
69
+
|> process.select(self)
70
+
71
+
server_component.register_subject(self)
72
+
|> lustre.send(to: counter)
73
+
74
+
#(Socket(component: counter, self:), selector)
75
+
}
76
+
77
+
pub fn loop_socket(
78
+
conn: ewe.WebsocketConnection,
79
+
value: Socket,
80
+
msg: ewe.WebsocketMessage(server_component.ClientMessage(Msg)),
81
+
) -> ewe.WebsocketNext(Socket, server_component.ClientMessage(Msg)) {
82
+
case msg {
83
+
ewe.Text(json) -> {
84
+
case json.parse(json, server_component.runtime_message_decoder()) {
85
+
Ok(runtime_msg) -> lustre.send(value.component, runtime_msg)
86
+
Error(_) -> Nil
87
+
}
88
+
ewe.websocket_continue(value)
89
+
}
90
+
ewe.Binary(_) -> ewe.websocket_continue(value)
91
+
ewe.User(client_msg) -> {
92
+
let json = server_component.client_message_to_json(client_msg)
93
+
let assert Ok(_) = ewe.send_text_frame(conn, json.to_string(json))
94
+
ewe.websocket_continue(value)
95
+
}
96
+
}
97
+
}
98
+
99
+
pub fn close_socket(_: ewe.WebsocketConnection, value: Socket) -> Nil {
100
+
server_component.deregister_subject(value.self)
101
+
|> lustre.send(to: value.component)
102
+
}
+55
-2
src/clover/data.gleam
+55
-2
src/clover/data.gleam
···
1
import ewe
2
import gleam/bytes_tree
3
import gleam/http/response
4
5
-
pub fn text_response(
6
resp: response.Response(_),
7
string: String,
8
) -> response.Response(ewe.ResponseBody) {
···
11
|> response.set_body(ewe.TextData(string))
12
}
13
14
-
pub fn html_response(
15
resp: response.Response(_),
16
bytes: bytes_tree.BytesTree,
17
) -> response.Response(ewe.ResponseBody) {
···
24
response.new(404)
25
|> response.set_body(ewe.Empty)
26
}
···
1
import ewe
2
+
import gleam/bit_array
3
import gleam/bytes_tree
4
+
import gleam/http/request
5
import gleam/http/response
6
+
import lustre/attribute as attr
7
+
import lustre/element
8
+
import lustre/element/html
9
10
+
pub fn text(
11
resp: response.Response(_),
12
string: String,
13
) -> response.Response(ewe.ResponseBody) {
···
16
|> response.set_body(ewe.TextData(string))
17
}
18
19
+
pub fn html(
20
resp: response.Response(_),
21
bytes: bytes_tree.BytesTree,
22
) -> response.Response(ewe.ResponseBody) {
···
29
response.new(404)
30
|> response.set_body(ewe.Empty)
31
}
32
+
33
+
pub fn redirect(to url: String) -> response.Response(ewe.ResponseBody) {
34
+
response.new(303)
35
+
|> response.set_header("location", url)
36
+
|> response.set_header("content-type", "text/plain; charset=utf-8")
37
+
|> response.set_body(ewe.TextData("You are being redirected: " <> url))
38
+
}
39
+
40
+
pub fn page(body: element.Element(a)) -> response.Response(ewe.ResponseBody) {
41
+
let r =
42
+
html.html([attr.lang("en")], [
43
+
html.head([], [
44
+
html.meta([attr.charset("utf-8")]),
45
+
html.meta([
46
+
attr.name("viewport"),
47
+
attr.content("width=device-width, initial-scale=1"),
48
+
]),
49
+
html.title([], "Clover"),
50
+
html.script([attr.type_("module"), attr.src("/lustre/runtime.mjs")], ""),
51
+
]),
52
+
body,
53
+
])
54
+
|> element.to_document_string_tree()
55
+
|> bytes_tree.from_string_tree()
56
+
57
+
response.new(200)
58
+
|> html(r)
59
+
}
60
+
61
+
pub fn bad_request(detail: String) -> response.Response(ewe.ResponseBody) {
62
+
let body = case detail {
63
+
"" -> "Bad request"
64
+
_ -> "Bad request: " <> detail
65
+
}
66
+
response.new(400)
67
+
|> response.set_header("content-type", "text/plain")
68
+
|> response.set_body(ewe.TextData(body))
69
+
}
70
+
71
+
pub fn require_string_body(
72
+
req: request.Request(ewe.Connection),
73
+
next: fn(String) -> response.Response(ewe.ResponseBody),
74
+
) -> response.Response(ewe.ResponseBody) {
75
+
case bit_array.to_string(req.body.buffer.data) {
76
+
Ok(body) -> next(body)
77
+
Error(_) -> bad_request("Invalid UTF-8")
78
+
}
79
+
}
+48
src/clover/pages/index.gleam
+48
src/clover/pages/index.gleam
···
···
1
+
import formal/form
2
+
import gleam/list
3
+
import lustre/attribute as attr
4
+
import lustre/element/html
5
+
6
+
pub type RoomInput {
7
+
RoomInput(id: String)
8
+
}
9
+
10
+
pub fn room_form() -> form.Form(RoomInput) {
11
+
form.new({
12
+
use id <- form.field("id", form.parse_string)
13
+
form.success(RoomInput(id:))
14
+
})
15
+
}
16
+
17
+
pub fn view() {
18
+
let form = room_form()
19
+
html.body([attr.styles([#("max-width", "32rem"), #("margin", "3rem auto")])], [
20
+
html.h1([], [html.text("Clover")]),
21
+
html.form([attr.method("POST"), attr.action("/new")], [
22
+
field_input(form:, label: "Room ID", type_: "text", name: "id"),
23
+
html.div([], [html.input([attr.type_("submit")])]),
24
+
]),
25
+
])
26
+
}
27
+
28
+
fn field_input(
29
+
form form: form.Form(RoomInput),
30
+
label label: String,
31
+
type_ type_: String,
32
+
name name: String,
33
+
) {
34
+
let errors = form.field_error_messages(form, name)
35
+
html.label([], [
36
+
html.text(label),
37
+
html.input([
38
+
attr.type_(type_),
39
+
attr.name(name),
40
+
attr.value(form.field_value(form, name)),
41
+
case errors {
42
+
[] -> attr.none()
43
+
_ -> attr.aria_invalid("true")
44
+
},
45
+
]),
46
+
..list.map(errors, fn(msg) { html.small([], [html.text(msg)]) })
47
+
])
48
+
}
+17
src/clover/pages/room.gleam
+17
src/clover/pages/room.gleam
···
···
1
+
import lustre/attribute as attr
2
+
import lustre/element/html
3
+
import lustre/server_component
4
+
5
+
pub fn view(id: String) {
6
+
html.body([attr.styles([#("max-width", "32rem"), #("margin", "3rem auto")])], [
7
+
html.a([attr.href("/")], [html.text("< Home")]),
8
+
html.h1([], [html.text("Room ID: "), html.text(id)]),
9
+
server_component.element([server_component.route("/ws")], []),
10
+
])
11
+
}
12
+
13
+
pub fn view_html(_id: String) {
14
+
html.body([attr.styles([#("max-width", "32rem"), #("margin", "3rem auto")])], [
15
+
server_component.element([server_component.route("/ws")], []),
16
+
])
17
+
}