🧚 A practical web framework for Gleam

Static file serving

+333 -29
+5 -4
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", "mist"], source = "local", path = "../framework" }, 6 + { name = "framework", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "mist", "simplifile"], source = "local", path = "../framework" }, 7 7 { name = "gleam_erlang", version = "0.20.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "F216A80C8FDFF774447B494D5E08AE4E9A911E971727B9A78FEBF5F300914584" }, 8 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" }, 9 + { name = "gleam_otp", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], 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.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang", "gleam_otp"], otp_app = "glisten", source = "hex", outer_checksum = "6DDE276F8A2E3C79E5A580DEA05C7D87FCDE3A37FF69F607770D92686E193531" }, 12 + { name = "glisten", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib", "gleam_otp"], otp_app = "glisten", source = "hex", outer_checksum = "6DDE276F8A2E3C79E5A580DEA05C7D87FCDE3A37FF69F607770D92686E193531" }, 13 13 { name = "htmb", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../htmb" }, 14 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" }, 15 + { name = "mist", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "gleam_erlang", "glisten"], otp_app = "mist", source = "hex", outer_checksum = "9A374CA245D682E2C08A5224B4420DDA252EF553AE5FD0ED7BAD33F86ACF7C98" }, 16 + { name = "simplifile", version = "0.1.8", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "9CED66E65AF32C98AA336A65365A498DCF018D2A3D96A05D861C4005DCDE4D2D" }, 16 17 { name = "sqlight", version = "0.7.0", build_tools = ["gleam"], requirements = ["esqlite", "gleam_stdlib"], otp_app = "sqlight", source = "hex", outer_checksum = "BDBAA35B58E11B6DE20DC869EAA247F447268B8F398E0677F25444C9F7AE54EA" }, 17 18 ] 18 19
+10
action/priv/static/styles.css
··· 1 + * { 2 + box-sizing: border-box; 3 + } 4 + 5 + body { 6 + margin: 0; 7 + padding: 0; 8 + font-family: sans-serif; 9 + background-color: hotpink; 10 + }
+2 -2
action/src/action.gleam
··· 8 8 9 9 pub fn main() { 10 10 let assert Ok(_) = 11 - app 11 + handle_request 12 12 |> framework.mist_service 13 13 |> mist.new 14 14 |> mist.port(8000) ··· 17 17 process.sleep_forever() 18 18 } 19 19 20 - pub fn app(request: framework.Request) { 20 + pub fn handle_request(request: framework.Request) { 21 21 use db <- database.with_connection("db.sqlite3") 22 22 23 23 let context = Context(db: db)
+1
action/src/action/web.gleam
··· 9 9 10 10 pub fn middleware(req: Request, service: fn(Request) -> Response) -> Response { 11 11 let req = framework.method_override(req) 12 + use <- framework.serve_static(req, under: "/static", from: "priv/static") 12 13 use <- framework.log_requests(req) 13 14 use <- serve_default_responses 14 15 use <- framework.rescue_crashes
+1
framework/gleam.toml
··· 13 13 gleam_stdlib = "~> 0.29" 14 14 gleam_http = "~> 3.2" 15 15 mist = "~> 0.13" 16 + simplifile = "~> 0.1" 16 17 17 18 [dev-dependencies] 18 19 gleeunit = "~> 0.10"
+5 -3
framework/manifest.toml
··· 4 4 packages = [ 5 5 { name = "gleam_erlang", version = "0.20.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "F216A80C8FDFF774447B494D5E08AE4E9A911E971727B9A78FEBF5F300914584" }, 6 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" }, 7 + { name = "gleam_otp", version = "0.6.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "E31B158857E3D2AF946FE6E90E0CB21699AF226F4630E93FBEAC5DB4515F8920" }, 8 8 { name = "gleam_stdlib", version = "0.30.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "03710B3DA047A3683117591707FCA19D32B980229DD8CE8B0603EB5B5144F6C3" }, 9 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" }, 10 + { name = "glisten", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang", "gleam_otp"], otp_app = "glisten", source = "hex", outer_checksum = "6DDE276F8A2E3C79E5A580DEA05C7D87FCDE3A37FF69F607770D92686E193531" }, 11 + { name = "mist", version = "0.13.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "glisten", "gleam_stdlib", "gleam_http"], otp_app = "mist", source = "hex", outer_checksum = "9A374CA245D682E2C08A5224B4420DDA252EF553AE5FD0ED7BAD33F86ACF7C98" }, 12 + { name = "simplifile", version = "0.1.8", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "9CED66E65AF32C98AA336A65365A498DCF018D2A3D96A05D861C4005DCDE4D2D" }, 12 13 ] 13 14 14 15 [requirements] ··· 16 17 gleam_stdlib = { version = "~> 0.29" } 17 18 gleeunit = { version = "~> 0.10" } 18 19 mist = { version = "~> 0.13" } 20 + simplifile = { version = "~> 0.1" }
+309 -20
framework/src/framework.gleam
··· 8 8 // - [ ] Body writing 9 9 // - [x] Html 10 10 // - [x] Json 11 - // - [ ] Static files 11 + // - [x] Static files 12 12 // - [ ] Cookies 13 13 // - [ ] Signed cookies 14 14 // - [ ] Secret keys ··· 18 18 // - [ ] Websockets 19 19 // - [ ] CSRF 20 20 // - [ ] Project generators 21 - // - [ ] Exception recovery 21 + // - [x] Exception recovery 22 22 23 23 import gleam/string_builder.{StringBuilder} 24 24 import gleam/bit_builder.{BitBuilder} 25 25 import gleam/bit_string 26 26 import gleam/erlang 27 + import gleam/dynamic.{Dynamic} 27 28 import gleam/bool 28 29 import gleam/http.{Method} 29 30 import gleam/http/request.{Request as HttpRequest} ··· 34 35 import gleam/uri 35 36 import gleam/io 36 37 import gleam/int 38 + import simplifile 37 39 import mist 40 + import mist/file as mist_file 38 41 39 42 // 40 43 // Running the server ··· 88 91 let body = case response.body { 89 92 Empty -> mist.Bytes(bit_builder.new()) 90 93 Text(text) -> mist.Bytes(bit_builder.from_string_builder(text)) 94 + File(path, content_type) -> { 95 + let path = <<path:utf8>> 96 + case mist_file.open(path) { 97 + Error(_) -> { 98 + // TODO: log error 99 + mist.Bytes(bit_builder.new()) 100 + } 101 + Ok(descriptor) -> { 102 + mist.File(descriptor, content_type, 0, mist_file.size(path)) 103 + } 104 + } 105 + } 91 106 } 92 107 response 93 108 |> response.set_body(body) ··· 99 114 100 115 pub type ResponseBody { 101 116 Empty 117 + // TODO: remove content type 118 + File(path: String, content_type: String) 102 119 Text(StringBuilder) 103 120 } 104 121 ··· 152 169 // TODO: document 153 170 pub fn internal_server_error() -> Response { 154 171 HttpResponse(500, [], Empty) 155 - } 156 - 157 - // TODO: test 158 - // TODO: document 159 - pub fn body_to_string_builder(body: ResponseBody) -> StringBuilder { 160 - case body { 161 - Empty -> string_builder.new() 162 - Text(text) -> text 163 - } 164 - } 165 - 166 - // TODO: test 167 - // TODO: document 168 - pub fn body_to_bit_builder(body: ResponseBody) -> BitBuilder { 169 - case body { 170 - Empty -> bit_builder.new() 171 - Text(text) -> bit_builder.from_string_builder(text) 172 - } 173 172 } 174 173 175 174 // ··· 337 336 } 338 337 339 338 // 339 + // MIME types 340 + // 341 + 342 + // TODO: test 343 + // TODO: move to another package 344 + pub fn mime_type_to_extensions(mime_type: String) -> List(String) { 345 + case mime_type { 346 + "application/atom+xml" -> ["atom"] 347 + "application/epub+zip" -> ["epub"] 348 + "application/gzip" -> ["gz"] 349 + "application/java-archive" -> ["jar"] 350 + "application/javascript" -> ["js"] 351 + "application/json" -> ["json"] 352 + "application/json-patch+json" -> ["json-patch"] 353 + "application/ld+json" -> ["jsonld"] 354 + "application/manifest+json" -> ["webmanifest"] 355 + "application/msword" -> ["doc"] 356 + "application/octet-stream" -> ["bin"] 357 + "application/ogg" -> ["ogx"] 358 + "application/pdf" -> ["pdf"] 359 + "application/postscript" -> ["ps", "eps", "ai"] 360 + "application/rss+xml" -> ["rss"] 361 + "application/rtf" -> ["rtf"] 362 + "application/vnd.amazon.ebook" -> ["azw"] 363 + "application/vnd.api+json" -> ["json-api"] 364 + "application/vnd.apple.installer+xml" -> ["mpkg"] 365 + "application/vnd.etsi.asic-e+zip" -> ["asice", "sce"] 366 + "application/vnd.etsi.asic-s+zip" -> ["asics", "scs"] 367 + "application/vnd.mozilla.xul+xml" -> ["xul"] 368 + "application/vnd.ms-excel" -> ["xls"] 369 + "application/vnd.ms-fontobject" -> ["eot"] 370 + "application/vnd.ms-powerpoint" -> ["ppt"] 371 + "application/vnd.oasis.opendocument.presentation" -> ["odp"] 372 + "application/vnd.oasis.opendocument.spreadsheet" -> ["ods"] 373 + "application/vnd.oasis.opendocument.text" -> ["odt"] 374 + "application/vnd.openxmlformats-officedocument.presentationml.presentation" -> [ 375 + "pptx", 376 + ] 377 + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" -> [ 378 + "xlsx", 379 + ] 380 + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" -> [ 381 + "docx", 382 + ] 383 + "application/vnd.rar" -> ["rar"] 384 + "application/vnd.visio" -> ["vsd"] 385 + "application/wasm" -> ["wasm"] 386 + "application/x-7z-compressed" -> ["7z"] 387 + "application/x-abiword" -> ["abw"] 388 + "application/x-bzip" -> ["bz"] 389 + "application/x-bzip2" -> ["bz2"] 390 + "application/x-cdf" -> ["cda"] 391 + "application/x-csh" -> ["csh"] 392 + "application/x-freearc" -> ["arc"] 393 + "application/x-httpd-php" -> ["php"] 394 + "application/x-msaccess" -> ["mdb"] 395 + "application/x-sh" -> ["sh"] 396 + "application/x-shockwave-flash" -> ["swf"] 397 + "application/x-tar" -> ["tar"] 398 + "application/xhtml+xml" -> ["xhtml"] 399 + "application/xml" -> ["xml"] 400 + "application/zip" -> ["zip"] 401 + "audio/3gpp" -> ["3gp"] 402 + "audio/3gpp2" -> ["3g2"] 403 + "audio/aac" -> ["aac"] 404 + "audio/midi" -> ["mid", "midi"] 405 + "audio/mpeg" -> ["mp3"] 406 + "audio/ogg" -> ["oga"] 407 + "audio/opus" -> ["opus"] 408 + "audio/wav" -> ["wav"] 409 + "audio/webm" -> ["weba"] 410 + "font/otf" -> ["otf"] 411 + "font/ttf" -> ["ttf"] 412 + "font/woff" -> ["woff"] 413 + "font/woff2" -> ["woff2"] 414 + "image/avif" -> ["avif"] 415 + "image/bmp" -> ["bmp"] 416 + "image/gif" -> ["gif"] 417 + "image/heic" -> ["heic"] 418 + "image/heif" -> ["heif"] 419 + "image/jpeg" -> ["jpg", "jpeg"] 420 + "image/jxl" -> ["jxl"] 421 + "image/png" -> ["png"] 422 + "image/svg+xml" -> ["svg", "svgz"] 423 + "image/tiff" -> ["tiff", "tif"] 424 + "image/vnd.adobe.photoshop" -> ["psd"] 425 + "image/vnd.microsoft.icon" -> ["ico"] 426 + "image/webp" -> ["webp"] 427 + "text/calendar" -> ["ics"] 428 + "text/css" -> ["css"] 429 + "text/csv" -> ["csv"] 430 + "text/html" -> ["html", "htm"] 431 + "text/javascript" -> ["js", "mjs"] 432 + "text/markdown" -> ["md", "markdown"] 433 + "text/plain" -> ["txt", "text"] 434 + "text/xml" -> ["xml"] 435 + "video/3gpp" -> ["3gp"] 436 + "video/3gpp2" -> ["3g2"] 437 + "video/mp2t" -> ["ts"] 438 + "video/mp4" -> ["mp4"] 439 + "video/mpeg" -> ["mpeg", "mpg"] 440 + "video/ogg" -> ["ogv"] 441 + "video/quicktime" -> ["mov"] 442 + "video/webm" -> ["webm"] 443 + "video/x-ms-wmv" -> ["wmv"] 444 + "video/x-msvideo" -> ["avi"] 445 + _ -> [] 446 + } 447 + } 448 + 449 + // TODO: test 450 + // TODO: move to another package 451 + fn extension_to_mime_type(extension: String) -> String { 452 + case extension { 453 + "7z" -> "application/x-7z-compressed" 454 + "aac" -> "audio/aac" 455 + "abw" -> "application/x-abiword" 456 + "ai" -> "application/postscript" 457 + "arc" -> "application/x-freearc" 458 + "asice" -> "application/vnd.etsi.asic-e+zip" 459 + "asics" -> "application/vnd.etsi.asic-s+zip" 460 + "atom" -> "application/atom+xml" 461 + "avi" -> "video/x-msvideo" 462 + "avif" -> "image/avif" 463 + "azw" -> "application/vnd.amazon.ebook" 464 + "bin" -> "application/octet-stream" 465 + "bmp" -> "image/bmp" 466 + "bz" -> "application/x-bzip" 467 + "bz2" -> "application/x-bzip2" 468 + "cda" -> "application/x-cdf" 469 + "csh" -> "application/x-csh" 470 + "css" -> "text/css" 471 + "csv" -> "text/csv" 472 + "doc" -> "application/msword" 473 + "docx" -> 474 + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" 475 + "eot" -> "application/vnd.ms-fontobject" 476 + "eps" -> "application/postscript" 477 + "epub" -> "application/epub+zip" 478 + "gif" -> "image/gif" 479 + "gz" -> "application/gzip" 480 + "heic" -> "image/heic" 481 + "heif" -> "image/heif" 482 + "htm" -> "text/html" 483 + "html" -> "text/html" 484 + "ico" -> "image/vnd.microsoft.icon" 485 + "ics" -> "text/calendar" 486 + "jar" -> "application/java-archive" 487 + "jpeg" -> "image/jpeg" 488 + "jpg" -> "image/jpeg" 489 + "js" -> "application/javascript" 490 + "json" -> "application/json" 491 + "json-api" -> "application/vnd.api+json" 492 + "json-patch" -> "application/json-patch+json" 493 + "jsonld" -> "application/ld+json" 494 + "jxl" -> "image/jxl" 495 + "markdown" -> "text/markdown" 496 + "md" -> "text/markdown" 497 + "mdb" -> "application/x-msaccess" 498 + "mid" -> "audio/midi" 499 + "midi" -> "audio/midi" 500 + "mjs" -> "text/javascript" 501 + "mov" -> "video/quicktime" 502 + "mp3" -> "audio/mpeg" 503 + "mp4" -> "video/mp4" 504 + "mpeg" -> "video/mpeg" 505 + "mpg" -> "video/mpeg" 506 + "mpkg" -> "application/vnd.apple.installer+xml" 507 + "odp" -> "application/vnd.oasis.opendocument.presentation" 508 + "ods" -> "application/vnd.oasis.opendocument.spreadsheet" 509 + "odt" -> "application/vnd.oasis.opendocument.text" 510 + "oga" -> "audio/ogg" 511 + "ogv" -> "video/ogg" 512 + "ogx" -> "application/ogg" 513 + "opus" -> "audio/opus" 514 + "otf" -> "font/otf" 515 + "pdf" -> "application/pdf" 516 + "php" -> "application/x-httpd-php" 517 + "png" -> "image/png" 518 + "ppt" -> "application/vnd.ms-powerpoint" 519 + "pptx" -> 520 + "application/vnd.openxmlformats-officedocument.presentationml.presentation" 521 + "ps" -> "application/postscript" 522 + "psd" -> "image/vnd.adobe.photoshop" 523 + "rar" -> "application/vnd.rar" 524 + "rss" -> "application/rss+xml" 525 + "rtf" -> "application/rtf" 526 + "sce" -> "application/vnd.etsi.asic-e+zip" 527 + "scs" -> "application/vnd.etsi.asic-s+zip" 528 + "sh" -> "application/x-sh" 529 + "svg" -> "image/svg+xml" 530 + "svgz" -> "image/svg+xml" 531 + "swf" -> "application/x-shockwave-flash" 532 + "tar" -> "application/x-tar" 533 + "text" -> "text/plain" 534 + "tif" -> "image/tiff" 535 + "tiff" -> "image/tiff" 536 + "ts" -> "video/mp2t" 537 + "ttf" -> "font/ttf" 538 + "txt" -> "text/plain" 539 + "vsd" -> "application/vnd.visio" 540 + "wasm" -> "application/wasm" 541 + "wav" -> "audio/wav" 542 + "weba" -> "audio/webm" 543 + "webm" -> "video/webm" 544 + "webmanifest" -> "application/manifest+json" 545 + "webp" -> "image/webp" 546 + "wmv" -> "video/x-ms-wmv" 547 + "woff" -> "font/woff" 548 + "woff2" -> "font/woff2" 549 + "xhtml" -> "application/xhtml+xml" 550 + "xls" -> "application/vnd.ms-excel" 551 + "xlsx" -> 552 + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" 553 + "xml" -> "application/xml" 554 + "xul" -> "application/vnd.mozilla.xul+xml" 555 + "zip" -> "application/zip" 556 + _ -> "application/octet-stream" 557 + } 558 + } 559 + 560 + // 340 561 // Middleware 341 562 // 342 563 ··· 370 591 response 371 592 } 372 593 594 + // TODO: test 595 + // TODO: document 596 + // TODO: remove requirement for preceeding slash on prefix 597 + pub fn serve_static( 598 + req: Request, 599 + under prefix: String, 600 + from directory: String, 601 + next service: fn() -> Response, 602 + ) -> Response { 603 + case req.method, string.starts_with(req.path, prefix) { 604 + http.Get, True -> { 605 + let path = 606 + req.path 607 + |> string.drop_left(string.length(prefix)) 608 + |> string.replace(each: "..", with: "") 609 + |> string.replace(each: "//", with: "/") 610 + |> string.append(directory, _) 611 + 612 + let mime_type = 613 + req.path 614 + |> string.split(on: ".") 615 + |> list.last 616 + |> result.unwrap("") 617 + |> extension_to_mime_type 618 + 619 + // TODO: better check for file existence. 620 + case file_info(path) { 621 + Error(_) -> service() 622 + Ok(_) -> 623 + response.new(200) 624 + |> response.set_header("content-type", mime_type) 625 + |> response.set_body(File(path, mime_type)) 626 + } 627 + } 628 + _, _ -> service() 629 + } 630 + } 631 + 632 + @external(erlang, "file", "read_file_info") 633 + fn file_info(path: String) -> Result(Dynamic, Dynamic) 634 + 373 635 // 374 636 // Testing 375 637 // 376 638 377 639 // TODO: test 378 640 // TODO: document 641 + // TODO: chunk the body 379 642 pub fn test_connection(body: BitString) -> Connection { 380 643 make_connection(fn(_size) { 381 644 Ok(Chunk(body, fn(_size) { Ok(ReadingFinished) })) 382 645 }) 383 646 } 647 + 648 + // TODO: test 649 + // TODO: document 650 + pub fn body_to_string_builder(body: ResponseBody) -> StringBuilder { 651 + case body { 652 + Empty -> string_builder.new() 653 + Text(text) -> text 654 + File(path, _) -> { 655 + let assert Ok(contents) = simplifile.read(path) 656 + string_builder.from_string(contents) 657 + } 658 + } 659 + } 660 + 661 + // TODO: test 662 + // TODO: document 663 + pub fn body_to_bit_builder(body: ResponseBody) -> BitBuilder { 664 + case body { 665 + Empty -> bit_builder.new() 666 + Text(text) -> bit_builder.from_string_builder(text) 667 + File(path, _) -> { 668 + let assert Ok(contents) = simplifile.read_bits(path) 669 + bit_builder.from_bit_string(contents) 670 + } 671 + } 672 + }