gleam HTTP server. because it glistens on a web
1import gleam/bit_array
2import gleam/bool
3import gleam/bytes_tree.{type BytesTree}
4import gleam/erlang
5import gleam/erlang/process.{type Selector}
6import gleam/hackney
7import gleam/http
8import gleam/http/request.{type Request}
9import gleam/http/response.{type Response, Response}
10import gleam/int
11import gleam/list
12import gleam/option
13import gleam/otp/actor
14import gleam/result
15import gleam/set
16import gleam/string
17import gleam/yielder
18import gleeunit/should
19import glisten
20import glisten/internal/handler as glisten_handler
21import glisten/tcp
22import glisten/transport.{Tcp}
23import mist
24import mist/internal/handler
25import mist/internal/http.{type Connection} as mhttp
26
27pub fn with_server(
28 at port: Int,
29 with handler: fn(Request(Connection)) -> Response(mist.ResponseData),
30 given req: Request(String),
31) -> Response(String) {
32 use <- open_server(port, handler)
33
34 let assert Ok(resp) = hackney.send(req)
35
36 resp
37}
38
39pub fn open_server(
40 at port: Int,
41 with handler: fn(Request(Connection)) -> Response(mist.ResponseData),
42 after perform: fn() -> return,
43) -> return {
44 let pid =
45 process.start(
46 fn() {
47 let assert Ok(listener) = tcp.listen(port, [])
48 let assert Ok(socket) = tcp.accept(listener)
49
50 let loop_func =
51 handler.with_func(fn(req) {
52 req
53 |> handler
54 |> convert_body_types
55 })
56
57 let glisten_handler =
58 glisten_handler.Handler(
59 socket: socket,
60 on_init: handler.init,
61 on_close: option.None,
62 loop: convert_loop(loop_func),
63 transport: Tcp,
64 )
65
66 let assert Ok(server) = glisten_handler.start(glisten_handler)
67 let assert Ok(_nil) =
68 tcp.controlling_process(socket, process.subject_owner(server))
69 process.send(server, glisten_handler.Internal(glisten_handler.Ready))
70
71 process.sleep_forever()
72 },
73 False,
74 )
75
76 case erlang.rescue(perform) {
77 Ok(return) -> {
78 process.kill(pid)
79 return
80 }
81 Error(err) -> {
82 process.kill(pid)
83 panic as { "Handler failed with: " <> string.inspect(err) }
84 }
85 }
86}
87
88fn convert_body_types(
89 resp: Response(mist.ResponseData),
90) -> Response(mhttp.ResponseData) {
91 let new_body = case resp.body {
92 mist.Websocket(selector) -> mhttp.Websocket(selector)
93 mist.Bytes(data) -> mhttp.Bytes(data)
94 mist.File(descriptor, offset, length) ->
95 mhttp.File(descriptor, offset, length)
96 mist.Chunked(iter) -> mhttp.Chunked(iter)
97 mist.ServerSentEvents(selector) -> mhttp.ServerSentEvents(selector)
98 }
99 response.set_body(resp, new_body)
100}
101
102fn map_user_selector(
103 selector: Selector(glisten.Message(user_message)),
104) -> Selector(glisten_handler.LoopMessage(user_message)) {
105 process.map_selector(selector, fn(value) {
106 case value {
107 glisten.Packet(msg) -> glisten_handler.Packet(msg)
108 glisten.User(msg) -> glisten_handler.Custom(msg)
109 }
110 })
111}
112
113fn convert_loop(
114 loop: glisten.Loop(user_message, data),
115) -> glisten_handler.Loop(user_message, data) {
116 fn(msg, data, conn: glisten_handler.Connection(user_message)) {
117 let conn = glisten.Connection(conn.socket, conn.transport, conn.sender)
118 case msg {
119 glisten_handler.Packet(msg) -> {
120 case loop(glisten.Packet(msg), data, conn) {
121 actor.Continue(data, selector) ->
122 actor.Continue(data, option.map(selector, map_user_selector))
123 actor.Stop(reason) -> actor.Stop(reason)
124 }
125 }
126 glisten_handler.Custom(msg) -> {
127 case loop(glisten.User(msg), data, conn) {
128 actor.Continue(data, selector) ->
129 actor.Continue(data, option.map(selector, map_user_selector))
130 actor.Stop(reason) -> actor.Stop(reason)
131 }
132 }
133 }
134 }
135}
136
137pub fn chunked_echo_server(chunk_size: Int) {
138 fn(req: request.Request(mhttp.Connection)) {
139 let assert Ok(req) = mhttp.read_body(req)
140 let assert Ok(body) = bit_array.to_string(req.body)
141 let chunks =
142 body
143 |> string.to_graphemes
144 |> yielder.from_list
145 |> yielder.sized_chunk(chunk_size)
146 |> yielder.map(fn(chars) {
147 chars
148 |> string.join("")
149 |> bytes_tree.from_string
150 })
151 response.new(200)
152 |> response.set_body(mist.Chunked(chunks))
153 }
154}
155
156pub fn default_handler(
157 req: request.Request(Connection),
158) -> response.Response(mist.ResponseData) {
159 let too_beeg =
160 response.new(413)
161 |> response.set_header("connection", "close")
162 |> response.set_body(mist.Bytes(bytes_tree.new()))
163 let req = mist.read_body(req, 4_000_000)
164 use <- bool.guard(when: result.is_error(req), return: too_beeg)
165 let assert Ok(req) = req
166 let body =
167 req.query
168 |> option.map(bit_array.from_string)
169 |> option.unwrap(req.body)
170 |> bytes_tree.from_bit_array
171 let length =
172 body
173 |> bytes_tree.byte_size
174 |> int.to_string
175 let headers =
176 list.filter(req.headers, fn(p) {
177 case p {
178 #("transfer-encoding", "chunked") -> False
179 #("content-length", _) -> False
180 _ -> True
181 }
182 })
183 |> list.prepend(#("content-length", length))
184 Response(status: 200, headers: headers, body: mist.Bytes(body))
185}
186
187fn compare_bitstring_body(actual: BitArray, expected: BytesTree) {
188 actual
189 |> bytes_tree.from_bit_array
190 |> should.equal(expected)
191}
192
193fn compare_string_body(actual: String, expected: BytesTree) {
194 actual
195 |> bytes_tree.from_string
196 |> should.equal(expected)
197}
198
199fn compare_headers_and_status(actual: Response(a), expected: Response(b)) {
200 should.equal(actual.status, expected.status)
201
202 let expected_headers = set.from_list(expected.headers)
203 let actual_headers =
204 actual.headers
205 |> set.from_list
206 |> set.filter(fn(pair) {
207 let #(key, _value) = pair
208 key != "date"
209 })
210
211 let missing_headers =
212 set.filter(expected_headers, fn(header) {
213 set.contains(actual_headers, header) == False
214 })
215 let extra_headers =
216 set.filter(actual_headers, fn(header) {
217 set.contains(expected_headers, header) == False
218 })
219
220 should.equal(missing_headers, extra_headers)
221}
222
223pub fn string_response_should_equal(
224 actual: Response(String),
225 expected: Response(BytesTree),
226) {
227 compare_headers_and_status(actual, expected)
228 compare_string_body(actual.body, expected.body)
229}
230
231pub fn bitstring_response_should_equal(
232 actual: Response(BitArray),
233 expected: Response(BytesTree),
234) {
235 compare_headers_and_status(actual, expected)
236 compare_bitstring_body(actual.body, expected.body)
237}
238
239pub fn make_request(path: String, body: body) -> request.Request(body) {
240 request.new()
241 |> request.set_host("localhost:8888")
242 |> request.set_method(http.Post)
243 |> request.set_path(path)
244 |> request.set_body(body)
245 |> request.set_scheme(http.Http)
246}
247
248type IoFormat {
249 User
250}
251
252@external(erlang, "io", "fwrite")
253fn io_fwrite(
254 format format: IoFormat,
255 output_format output_format: String,
256 data data: any,
257) -> Nil
258
259pub fn io_fwrite_user(data: anything) {
260 io_fwrite(User, "~tp\n", [data])
261}