gleam HTTP server. because it glistens on a web
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at master 261 lines 7.0 kB view raw
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}