🧚 A practical web framework for Gleam

Upgrading to new Mist

+181 -32
+1 -1
README.md
··· 4 4 5 5 ## TODO 6 6 7 - - Sort out the CI configuration for this monorepo. 8 7 - Log when HTTP requests are received. 9 8 - Read database file location from environment. 10 9 - Store answer from form in database. 10 + - TODO: And also write tests! 11 11 - Send you to the next question. 12 12 - Eventual "thank you!" page. 13 13 - HTML layouts.
+1 -1
action/gleam.toml
··· 4 4 5 5 [dependencies] 6 6 gleam_stdlib = "~> 0.29" 7 - mist = "~> 0.12" 7 + mist = "~> 0.13" 8 8 sqlight = "~> 0.6" 9 9 ids = "~> 0.7" 10 10 framework = { path = "../framework" }
+10 -10
action/manifest.toml
··· 3 3 4 4 packages = [ 5 5 { name = "esqlite", version = "0.8.6", build_tools = ["rebar3"], requirements = [], otp_app = "esqlite", source = "hex", outer_checksum = "607E45F4DA42601D8F530979417F57A4CD629AB49085891849302057E68EA188" }, 6 - { name = "framework", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], source = "local", path = "../framework" }, 7 - { name = "gleam_erlang", version = "0.19.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "720D1E0A0CEBBD51C4AA88501D1D4FBFEF4AA7B3332C994691ED944767A52582" }, 8 - { name = "gleam_http", version = "3.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "D034F5CE0639CD142CBA210B7D5D14236C284B0C5772A043D2E22128594573AE" }, 9 - { name = "gleam_otp", version = "0.5.3", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang"], otp_app = "gleam_otp", source = "hex", outer_checksum = "6E705B69464237353E0380AC8143BDB29A3F0BF6168755D5F2D6E55A34A8B077" }, 6 + { name = "framework", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "mist"], source = "local", path = "/Users/louis/src/gleam/action/framework" }, 7 + { name = "gleam_erlang", version = "0.20.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "F216A80C8FDFF774447B494D5E08AE4E9A911E971727B9A78FEBF5F300914584" }, 8 + { name = "gleam_http", version = "3.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "B6EB76D304E0E66267485983E6B7BC28F3BFA6795BB2BF90FC411F6903AF6A1A" }, 9 + { name = "gleam_otp", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang"], otp_app = "gleam_otp", source = "hex", outer_checksum = "E31B158857E3D2AF946FE6E90E0CB21699AF226F4630E93FBEAC5DB4515F8920" }, 10 10 { name = "gleam_stdlib", version = "0.30.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "03710B3DA047A3683117591707FCA19D32B980229DD8CE8B0603EB5B5144F6C3" }, 11 11 { name = "gleeunit", version = "0.10.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "ECEA2DE4BE6528D36AFE74F42A21CDF99966EC36D7F25DEB34D47DD0F7977BAF" }, 12 - { name = "glisten", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "52B530FF25370590843998D1B6C4EC6169DB1300D5E4407A5CDA1575374B7AEC" }, 13 - { name = "htmb", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../htmb" }, 14 - { name = "ids", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib", "gleam_otp"], otp_app = "ids", source = "hex", outer_checksum = "7A378014D40E848326FBEE8AC0C9B35EB9C8094DC4414D89F9A5AA99397A6042" }, 15 - { name = "mist", version = "0.12.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_erlang", "gleam_stdlib", "glisten", "gleam_otp"], otp_app = "mist", source = "hex", outer_checksum = "6FD3D24B0E79CA90AE07A695C2ADA2049E16A384BEF960B122E841DE88B6CA67" }, 16 - { name = "sqlight", version = "0.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "esqlite"], otp_app = "sqlight", source = "hex", outer_checksum = "082244304DE85652A5BCF31CFE891BB8E96017B0A4BEA43623D32CC0983BF699" }, 12 + { name = "glisten", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang", "gleam_otp"], otp_app = "glisten", source = "hex", outer_checksum = "6DDE276F8A2E3C79E5A580DEA05C7D87FCDE3A37FF69F607770D92686E193531" }, 13 + { name = "htmb", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "/Users/louis/src/gleam/action/htmb" }, 14 + { name = "ids", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_otp", "gleam_erlang"], otp_app = "ids", source = "hex", outer_checksum = "7A378014D40E848326FBEE8AC0C9B35EB9C8094DC4414D89F9A5AA99397A6042" }, 15 + { name = "mist", version = "0.13.0", build_tools = ["gleam"], requirements = ["glisten", "gleam_erlang", "gleam_stdlib", "gleam_http"], otp_app = "mist", source = "hex", outer_checksum = "9A374CA245D682E2C08A5224B4420DDA252EF553AE5FD0ED7BAD33F86ACF7C98" }, 16 + { name = "sqlight", version = "0.7.0", build_tools = ["gleam"], requirements = ["esqlite", "gleam_stdlib"], otp_app = "sqlight", source = "hex", outer_checksum = "BDBAA35B58E11B6DE20DC869EAA247F447268B8F398E0677F25444C9F7AE54EA" }, 17 17 ] 18 18 19 19 [requirements] ··· 22 22 gleeunit = { version = "~> 0.10" } 23 23 htmb = { path = "../htmb" } 24 24 ids = { version = "~> 0.7" } 25 - mist = { version = "~> 0.12" } 25 + mist = { version = "~> 0.13" } 26 26 sqlight = { version = "~> 0.6" }
+7 -5
action/src/action.gleam
··· 1 1 import gleam/erlang/process 2 - import gleam/http/request.{Request} 3 - import gleam/http/response 4 2 import gleam/io 5 3 import framework 6 4 import mist ··· 9 7 import action/router 10 8 11 9 pub fn main() { 12 - let assert Ok(_) = mist.run_service(8000, app, max_body_limit: 4_000_000) 10 + let assert Ok(_) = 11 + app 12 + |> framework.mist_service 13 + |> mist.new 14 + |> mist.port(8000) 15 + |> mist.start_http 13 16 io.println("Started listening on http://localhost:8000 ✨") 14 17 process.sleep_forever() 15 18 } 16 19 17 - pub fn app(request: Request(BitString)) { 20 + pub fn app(request: framework.Request) { 18 21 use db <- database.with_connection("db.sqlite3") 19 22 20 23 let context = Context(db: db) 21 24 router.handle_request(request, context) 22 - |> response.map(framework.body_to_bit_builder) 23 25 }
+13 -8
action/src/action/web.gleam
··· 1 1 import sqlight 2 2 import framework.{Response} 3 3 import htmb.{h, text} 4 + import gleam/bool 4 5 5 6 pub type Context { 6 7 Context(db: sqlight.Connection) 7 8 } 8 9 9 10 pub fn default_responses(response: Response) -> Response { 10 - case response.status, response.body { 11 - 404, framework.Empty -> { 11 + use <- bool.guard(response.body != framework.Empty, return: response) 12 + 13 + case response.status { 14 + 404 -> 12 15 h("h1", [], [text("There's nothing here")]) 13 16 |> htmb.render_page(doctype: "html") 14 17 |> framework.html_body(response, _) 15 - } 16 18 17 - 405, framework.Empty -> { 19 + 405 -> 18 20 h("h1", [], [text("There's nothing here")]) 19 21 |> htmb.render_page(doctype: "html") 20 22 |> framework.html_body(response, _) 21 - } 22 23 23 - 400, framework.Empty -> { 24 + 400 -> 24 25 h("h1", [], [text("Invalid request")]) 25 26 |> htmb.render_page(doctype: "html") 26 27 |> framework.html_body(response, _) 27 - } 28 28 29 - _, _ -> response 29 + 413 -> 30 + h("h1", [], [text("Request entity too large")]) 31 + |> htmb.render_page(doctype: "html") 32 + |> framework.html_body(response, _) 33 + 34 + _ -> response 30 35 } 31 36 }
+1
framework/gleam.toml
··· 12 12 [dependencies] 13 13 gleam_stdlib = "~> 0.29" 14 14 gleam_http = "~> 3.2" 15 + mist = "~> 0.13" 15 16 16 17 [dev-dependencies] 17 18 gleeunit = "~> 0.10"
+7 -2
framework/manifest.toml
··· 2 2 # You typically do not need to edit this file 3 3 4 4 packages = [ 5 - { name = "gleam_http", version = "3.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "D034F5CE0639CD142CBA210B7D5D14236C284B0C5772A043D2E22128594573AE" }, 6 - { name = "gleam_stdlib", version = "0.29.2", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "B296BF9B8AA384A6B64CD49F333016A9DCA6AC73A95400D17F2271E072EFF986" }, 5 + { name = "gleam_erlang", version = "0.20.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "F216A80C8FDFF774447B494D5E08AE4E9A911E971727B9A78FEBF5F300914584" }, 6 + { name = "gleam_http", version = "3.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "B6EB76D304E0E66267485983E6B7BC28F3BFA6795BB2BF90FC411F6903AF6A1A" }, 7 + { name = "gleam_otp", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang"], otp_app = "gleam_otp", source = "hex", outer_checksum = "E31B158857E3D2AF946FE6E90E0CB21699AF226F4630E93FBEAC5DB4515F8920" }, 8 + { name = "gleam_stdlib", version = "0.30.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "03710B3DA047A3683117591707FCA19D32B980229DD8CE8B0603EB5B5144F6C3" }, 7 9 { name = "gleeunit", version = "0.10.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "ECEA2DE4BE6528D36AFE74F42A21CDF99966EC36D7F25DEB34D47DD0F7977BAF" }, 10 + { name = "glisten", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "6DDE276F8A2E3C79E5A580DEA05C7D87FCDE3A37FF69F607770D92686E193531" }, 11 + { name = "mist", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_http", "glisten", "gleam_stdlib", "gleam_erlang"], otp_app = "mist", source = "hex", outer_checksum = "9A374CA245D682E2C08A5224B4420DDA252EF553AE5FD0ED7BAD33F86ACF7C98" }, 8 12 ] 9 13 10 14 [requirements] 11 15 gleam_http = { version = "~> 3.2" } 12 16 gleam_stdlib = { version = "~> 0.29" } 13 17 gleeunit = { version = "~> 0.10" } 18 + mist = { version = "~> 0.13" }
+141 -5
framework/src/framework.gleam
··· 9 9 import gleam/result 10 10 import gleam/string 11 11 import gleam/uri 12 + import mist 13 + 14 + // 15 + // Running the server 16 + // 17 + 18 + pub fn mist_service( 19 + service: fn(Request) -> Response, 20 + ) -> fn(HttpRequest(mist.Connection)) -> HttpResponse(mist.ResponseData) { 21 + fn(request: HttpRequest(_)) { 22 + let connection = 23 + Connection( 24 + reader: mist_body_reader(request), 25 + max_body_size: 8_000_000, 26 + max_files_size: 32_000_000, 27 + read_chunk_size: 1_000_000, 28 + ) 29 + request 30 + |> request.set_body(connection) 31 + |> service 32 + |> mist_response 33 + } 34 + } 35 + 36 + fn mist_body_reader(request: HttpRequest(mist.Connection)) -> Reader { 37 + case mist.stream(request) { 38 + Error(_) -> fn(_) { Ok(ReadingFinished) } 39 + Ok(stream) -> fn(size) { wrap_mist_chunk(stream(size)) } 40 + } 41 + } 42 + 43 + fn wrap_mist_chunk( 44 + chunk: Result(mist.Chunk, mist.ReadError), 45 + ) -> Result(Read, Nil) { 46 + chunk 47 + |> result.nil_error 48 + |> result.map(fn(chunk) { 49 + case chunk { 50 + mist.Done -> ReadingFinished 51 + mist.Chunk(data, consume) -> 52 + Chunk(data, fn(size) { wrap_mist_chunk(consume(size)) }) 53 + } 54 + }) 55 + } 56 + 57 + fn mist_response(response: Response) -> HttpResponse(mist.ResponseData) { 58 + let body = case response.body { 59 + Empty -> mist.Bytes(bit_builder.new()) 60 + Text(text) -> mist.Bytes(bit_builder.from_string_builder(text)) 61 + } 62 + response 63 + |> response.set_body(body) 64 + } 12 65 13 66 // 14 67 // Responses ··· 61 114 62 115 // TODO: test 63 116 // TODO: document 117 + pub fn entity_too_large() -> Response { 118 + HttpResponse(413, [], Empty) 119 + } 120 + 121 + // TODO: test 122 + // TODO: document 64 123 pub fn body_to_string_builder(body: Body) -> StringBuilder { 65 124 case body { 66 125 Empty -> string_builder.new() ··· 81 140 // Requests 82 141 // 83 142 143 + pub opaque type Connection { 144 + Connection( 145 + reader: Reader, 146 + // TODO: document these. Cannot be here as this is opaque. 147 + max_body_size: Int, 148 + max_files_size: Int, 149 + read_chunk_size: Int, 150 + ) 151 + } 152 + 153 + type Reader = 154 + fn(Int) -> Result(Read, Nil) 155 + 156 + type Read { 157 + Chunk(BitString, next: Reader) 158 + ReadingFinished 159 + } 160 + 161 + // TODO: test 162 + // TODO: document 163 + pub fn set_max_body_size(request: Request, size: Int) -> Request { 164 + Connection(..request.body, max_body_size: size) 165 + |> request.set_body(request, _) 166 + } 167 + 168 + // TODO: test 169 + // TODO: document 170 + pub fn set_max_files_size(request: Request, size: Int) -> Request { 171 + Connection(..request.body, max_files_size: size) 172 + |> request.set_body(request, _) 173 + } 174 + 175 + // TODO: test 176 + // TODO: document 177 + pub fn set_read_chunk_size(request: Request, size: Int) -> Request { 178 + Connection(..request.body, read_chunk_size: size) 179 + |> request.set_body(request, _) 180 + } 181 + 84 182 pub type Request = 85 - HttpRequest(BitString) 183 + HttpRequest(Connection) 86 184 87 185 // TODO: test 88 186 // TODO: document ··· 121 219 { 122 220 use query <- result.try(request.get_query(request)) 123 221 use pair <- result.try(list.key_pop(query, "_method")) 124 - use method <- result.try(http.parse_method(pair.0)) 222 + use method <- result.map(http.parse_method(pair.0)) 125 223 126 - Ok(case method { 224 + case method { 127 225 http.Put | http.Patch | http.Delete -> request.set_method(request, method) 128 226 _ -> request 129 - }) 227 + } 130 228 } 131 229 |> result.unwrap(request) 132 230 } ··· 137 235 request: Request, 138 236 next: fn(String) -> Response, 139 237 ) -> Response { 140 - require(bit_string.to_string(request.body), next) 238 + case read_entire_body(request) { 239 + Ok(body) -> require(bit_string.to_string(body), next) 240 + Error(_) -> entity_too_large() 241 + } 242 + } 243 + 244 + // TODO: test 245 + // TODO: public? 246 + // TODO: document 247 + // TODO: note you probably want a `require_` function 248 + // TODO: note it'll hang if you call it twice 249 + fn read_entire_body(request: Request) -> Result(BitString, Nil) { 250 + let connection = request.body 251 + read_body_loop( 252 + connection.reader, 253 + connection.read_chunk_size, 254 + connection.max_body_size, 255 + <<>>, 256 + ) 257 + } 258 + 259 + fn read_body_loop( 260 + reader: Reader, 261 + read_chunk_size: Int, 262 + max_body_size: Int, 263 + accumulator: BitString, 264 + ) -> Result(BitString, Nil) { 265 + use chunk <- result.try(reader(read_chunk_size)) 266 + case chunk { 267 + ReadingFinished -> Ok(accumulator) 268 + Chunk(chunk, next) -> { 269 + let accumulator = bit_string.append(accumulator, chunk) 270 + case bit_string.byte_size(accumulator) > max_body_size { 271 + True -> Error(Nil) 272 + False -> 273 + read_body_loop(next, read_chunk_size, max_body_size, accumulator) 274 + } 275 + } 276 + } 141 277 } 142 278 143 279 // TODO: replace with a function that also supports multipart forms