🧚 A practical web framework for Gleam

Compare changes

Choose any two refs to compare.

Changed files
+563 -375
.github
workflows
examples
00-hello-world
01-routing
02-working-with-form-data
03-working-with-json
04-working-with-other-formats
05-using-a-database
06-serving-static-assets
07-logging
08-working-with-cookies
09-configuring-default-responses
10-working-with-files
utilities
src
test
+2 -2
.github/workflows/ci.yml
··· 13 13 - uses: actions/checkout@v4 14 14 - uses: erlef/setup-beam@v1 15 15 with: 16 - otp-version: "26.0" 17 - gleam-version: "1.2.0" 16 + otp-version: "27.0" 17 + gleam-version: "1.4.0" 18 18 rebar3-version: "3" 19 19 # elixir-version: "1.14.2" 20 20 - run: gleam format --check src test
+31 -1
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 - ## Unreleased 3 + ## v1.3.0 - 2024-11-21 4 + 5 + - Updated for `gleam_stdlib` v0.43.0. 6 + 7 + ## v1.2.0 - 2024-10-09 8 + 9 + - The requirement for `gleam_json` has been relaxed to < 3.0.0. 10 + - The requirement for `mist` has been relaxed to < 4.0.0. 11 + - The Gleam version requirement has been corrected to `>= 1.1.0` from the 12 + previously inaccurate `">= 0.32.0`. 13 + 14 + ## v1.1.0 - 2024-08-23 15 + 16 + - Rather than using `/tmp`, the platform-specific temporary directory is 17 + detected used. 18 + 19 + ## v1.0.0 - 2024-08-21 20 + 21 + - The Mist web server related functions have been moved to the `wisp_mist` 22 + module. 23 + - The `wisp` module gains the `set_logger_level` function and `LogLevel` type. 24 + 25 + ## v0.16.0 - 2024-07-13 26 + 27 + - HTML and JSON body functions now include `charset=utf-8` in the content-type 28 + header. 29 + - The `require_content_type` function now handles additional attributes 30 + correctly. 31 + 32 + ## v0.15.0 - 2024-05-12 4 33 5 34 - The `mist` version constraint has been increased to >= 1.2.0. 35 + - The `simplifile` version constraint has been increased to >= 2.0.0. 6 36 - The `escape_html` function in the `wisp` module has been optimised. 7 37 8 38 ## v0.14.0 - 2024-03-28
+1 -1
examples/00-hello-world/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = ">= 1.2.0 and < 2.0.0" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 11 12 12 13 13 [dev-dependencies]
+3 -3
examples/00-hello-world/src/app/router.gleam
··· 1 - import wisp.{type Request, type Response} 2 - import gleam/string_builder 3 1 import app/web 2 + import gleam/string_tree 3 + import wisp.{type Request, type Response} 4 4 5 5 /// The HTTP request handler- your application! 6 6 /// ··· 9 9 use _req <- web.middleware(req) 10 10 11 11 // Later we'll use templates, but for now a string will do. 12 - let body = string_builder.from_string("<h1>Hello, Joe!</h1>") 12 + let body = string_tree.from_string("<h1>Hello, Joe!</h1>") 13 13 14 14 // Return a 200 OK response with the body and a HTML content type. 15 15 wisp.html_response(body, 200)
+3 -2
examples/00-hello-world/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 // This sets the logger to print INFO level logs, and other sensible defaults ··· 14 15 15 16 // Start the Mist web server. 16 17 let assert Ok(_) = 17 - wisp.mist_handler(router.handle_request, secret_key_base) 18 + wisp_mist.handler(router.handle_request, secret_key_base) 18 19 |> mist.new 19 20 |> mist.port(8000) 20 21 |> mist.start_http
+1 -1
examples/00-hello-world/test/app_test.gleam
··· 14 14 |> should.equal(200) 15 15 16 16 response.headers 17 - |> should.equal([#("content-type", "text/html")]) 17 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 18 18 19 19 response 20 20 |> testing.string_body
+1 -1
examples/01-routing/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = ">= 1.2.0 and < 2.0.0" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 11 gleam_http = "~> 3.5" 12 12 13 13 [dev-dependencies]
+7 -7
examples/01-routing/src/app/router.gleam
··· 1 - import wisp.{type Request, type Response} 2 - import gleam/string_builder 3 - import gleam/http.{Get, Post} 4 1 import app/web 2 + import gleam/http.{Get, Post} 3 + import gleam/string_tree 4 + import wisp.{type Request, type Response} 5 5 6 6 pub fn handle_request(req: Request) -> Response { 7 7 use req <- web.middleware(req) ··· 31 31 // used to return a 405: Method Not Allowed response for all other methods. 32 32 use <- wisp.require_method(req, Get) 33 33 34 - let html = string_builder.from_string("Hello, Joe!") 34 + let html = string_tree.from_string("Hello, Joe!") 35 35 wisp.ok() 36 36 |> wisp.html_body(html) 37 37 } ··· 48 48 49 49 fn list_comments() -> Response { 50 50 // In a later example we'll show how to read from a database. 51 - let html = string_builder.from_string("Comments!") 51 + let html = string_tree.from_string("Comments!") 52 52 wisp.ok() 53 53 |> wisp.html_body(html) 54 54 } 55 55 56 56 fn create_comment(_req: Request) -> Response { 57 57 // In a later example we'll show how to parse data from the request body. 58 - let html = string_builder.from_string("Created") 58 + let html = string_tree.from_string("Created") 59 59 wisp.created() 60 60 |> wisp.html_body(html) 61 61 } ··· 66 66 // The `id` path parameter has been passed to this function, so we could use 67 67 // it to look up a comment in a database. 68 68 // For now we'll just include in the response body. 69 - let html = string_builder.from_string("Comment with id " <> id) 69 + let html = string_tree.from_string("Comment with id " <> id) 70 70 wisp.ok() 71 71 |> wisp.html_body(html) 72 72 }
+3 -2
examples/01-routing/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+1 -1
examples/01-routing/test/app_test.gleam
··· 15 15 |> should.equal(200) 16 16 17 17 response.headers 18 - |> should.equal([#("content-type", "text/html")]) 18 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 19 19 20 20 response 21 21 |> testing.string_body
+1 -1
examples/02-working-with-form-data/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = ">= 1.2.0 and < 2.0.0" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 11 gleam_http = "~> 3.5" 12 12 13 13 [dev-dependencies]
+3 -3
examples/02-working-with-form-data/src/app/router.gleam
··· 2 2 import gleam/http.{Get, Post} 3 3 import gleam/list 4 4 import gleam/result 5 - import gleam/string_builder 5 + import gleam/string_tree 6 6 import wisp.{type Request, type Response} 7 7 8 8 pub fn handle_request(req: Request) -> Response { ··· 21 21 // In a larger application a template library or HTML form library might 22 22 // be used here instead of a string literal. 23 23 let html = 24 - string_builder.from_string( 24 + string_tree.from_string( 25 25 "<form method='post'> 26 26 <label>Title: 27 27 <input type='text' name='title'> ··· 60 60 case result { 61 61 Ok(content) -> { 62 62 wisp.ok() 63 - |> wisp.html_body(string_builder.from_string(content)) 63 + |> wisp.html_body(string_tree.from_string(content)) 64 64 } 65 65 Error(_) -> { 66 66 wisp.bad_request()
+3 -2
examples/02-working-with-form-data/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+2 -2
examples/02-working-with-form-data/test/app_test.gleam
··· 15 15 |> should.equal(200) 16 16 17 17 response.headers 18 - |> should.equal([#("content-type", "text/html")]) 18 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 19 19 20 20 response 21 21 |> testing.string_body ··· 55 55 |> should.equal(200) 56 56 57 57 response.headers 58 - |> should.equal([#("content-type", "text/html")]) 58 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 59 59 60 60 response 61 61 |> testing.string_body
+1 -1
examples/03-working-with-json/gleam.toml
··· 8 8 wisp = { path = "../.." } 9 9 gleam_json = "~> 0.6" 10 10 gleam_erlang = "~> 0.23" 11 - mist = ">= 1.2.0 and < 2.0.0" 11 + mist = ">= 2.0.0 and < 3.0.0" 12 12 gleam_http = "~> 3.5" 13 13 14 14 [dev-dependencies]
+3 -2
examples/03-working-with-json/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+1 -1
examples/03-working-with-json/test/app_test.gleam
··· 49 49 |> should.equal(201) 50 50 51 51 response.headers 52 - |> should.equal([#("content-type", "application/json")]) 52 + |> should.equal([#("content-type", "application/json; charset=utf-8")]) 53 53 54 54 response 55 55 |> testing.string_body
+1 -1
examples/04-working-with-other-formats/gleam.toml
··· 8 8 wisp = { path = "../.." } 9 9 gsv = "~> 1.0" 10 10 gleam_erlang = "~> 0.23" 11 - mist = ">= 1.2.0 and < 2.0.0" 11 + mist = ">= 2.0.0 and < 3.0.0" 12 12 gleam_http = "~> 3.5" 13 13 14 14 [dev-dependencies]
+3 -2
examples/04-working-with-other-formats/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+1 -1
examples/05-using-a-database/gleam.toml
··· 9 9 gleam_json = "~> 0.6" 10 10 tiny_database = { path = "../utilities/tiny_database" } 11 11 gleam_erlang = "~> 0.23" 12 - mist = ">= 1.2.0 and < 2.0.0" 12 + mist = ">= 2.0.0 and < 3.0.0" 13 13 gleam_http = "~> 3.5" 14 14 15 15 [dev-dependencies]
+2 -2
examples/05-using-a-database/src/app/web/people.gleam
··· 1 1 import app/web.{type Context} 2 + import gleam/dict 2 3 import gleam/dynamic.{type Dynamic} 3 4 import gleam/http.{Get, Post} 4 5 import gleam/json 5 - import gleam/dict 6 6 import gleam/result.{try} 7 7 import tiny_database 8 8 import wisp.{type Request, type Response} ··· 122 122 // In this example we are not going to be reporting specific errors to the 123 123 // user, so we can discard the error and replace it with Nil. 124 124 result 125 - |> result.nil_error 125 + |> result.replace_error(Nil) 126 126 } 127 127 128 128 /// Save a person to the database and return the id of the newly created record.
+5 -4
examples/05-using-a-database/src/app.gleam
··· 1 + import app/router 2 + import app/web 1 3 import gleam/erlang/process 2 - import tiny_database 3 4 import mist 5 + import tiny_database 4 6 import wisp 5 - import app/router 6 - import app/web 7 + import wisp/wisp_mist 7 8 8 9 pub const data_directory = "tmp/data" 9 10 ··· 24 25 25 26 let assert Ok(_) = 26 27 handler 27 - |> wisp.mist_handler(secret_key_base) 28 + |> wisp_mist.handler(secret_key_base) 28 29 |> mist.new 29 30 |> mist.port(8000) 30 31 |> mist.start_http
+1 -1
examples/05-using-a-database/test/app_test.gleam
··· 40 40 response.status 41 41 |> should.equal(200) 42 42 response.headers 43 - |> should.equal([#("content-type", "application/json")]) 43 + |> should.equal([#("content-type", "application/json; charset=utf-8")]) 44 44 45 45 // Initially there are no people in the database 46 46 response
+1 -1
examples/06-serving-static-assets/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = ">= 1.2.0 and < 2.0.0" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 11 gleam_http = "~> 3.5" 12 12 13 13 [dev-dependencies]
+3 -3
examples/06-serving-static-assets/src/app/router.gleam
··· 1 - import wisp.{type Request, type Response} 2 - import gleam/string_builder 3 1 import app/web.{type Context} 2 + import gleam/string_tree 3 + import wisp.{type Request, type Response} 4 4 5 5 const html = "<!DOCTYPE html> 6 6 <html lang=\"en\"> ··· 18 18 19 19 pub fn handle_request(req: Request, ctx: Context) -> Response { 20 20 use _req <- web.middleware(req, ctx) 21 - wisp.html_response(string_builder.from_string(html), 200) 21 + wisp.html_response(string_tree.from_string(html), 200) 22 22 }
+4 -3
examples/06-serving-static-assets/src/app.gleam
··· 1 + import app/router 2 + import app/web.{Context} 1 3 import gleam/erlang/process 2 4 import mist 3 5 import wisp 4 - import app/router 5 - import app/web.{Context} 6 + import wisp/wisp_mist 6 7 7 8 pub fn main() { 8 9 wisp.configure_logger() ··· 16 17 let handler = router.handle_request(_, ctx) 17 18 18 19 let assert Ok(_) = 19 - wisp.mist_handler(handler, secret_key_base) 20 + wisp_mist.handler(handler, secret_key_base) 20 21 |> mist.new 21 22 |> mist.port(8000) 22 23 |> mist.start_http
+6 -6
examples/06-serving-static-assets/test/app_test.gleam
··· 1 + import app 2 + import app/router 3 + import app/web.{type Context, Context} 1 4 import gleeunit 2 5 import gleeunit/should 3 6 import wisp/testing 4 - import app 5 - import app/router 6 - import app/web.{type Context, Context} 7 7 8 8 pub fn main() { 9 9 gleeunit.main() ··· 26 26 |> should.equal(200) 27 27 28 28 response.headers 29 - |> should.equal([#("content-type", "text/html")]) 29 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 30 30 } 31 31 32 32 pub fn get_stylesheet_test() { ··· 38 38 |> should.equal(200) 39 39 40 40 response.headers 41 - |> should.equal([#("content-type", "text/css")]) 41 + |> should.equal([#("content-type", "text/css; charset=utf-8")]) 42 42 } 43 43 44 44 pub fn get_javascript_test() { ··· 50 50 |> should.equal(200) 51 51 52 52 response.headers 53 - |> should.equal([#("content-type", "text/javascript")]) 53 + |> should.equal([#("content-type", "text/javascript; charset=utf-8")]) 54 54 }
+1 -1
examples/07-logging/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = "~> 0.14" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 11 12 12 [dev-dependencies] 13 13 gleeunit = "~> 1.0"
+3 -2
examples/07-logging/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+1 -1
examples/08-working-with-cookies/gleam.toml
··· 8 8 wisp = { path = "../.." } 9 9 gleam_crypto = "~> 1.0" 10 10 gleam_erlang = "~> 0.23" 11 - mist = ">= 1.2.0 and < 2.0.0" 11 + mist = ">= 2.0.0 and < 3.0.0" 12 12 gleam_http = "~> 3.5" 13 13 14 14 [dev-dependencies]
+3 -3
examples/08-working-with-cookies/src/app/router.gleam
··· 1 1 import app/web 2 2 import gleam/http.{Delete, Get, Post} 3 3 import gleam/list 4 - import gleam/string_builder 4 + import gleam/string_tree 5 5 import wisp.{type Request, type Response} 6 6 7 7 const cookie_name = "id" ··· 25 25 " <button type='submit'>Log out</button>", 26 26 "</form>", 27 27 ] 28 - |> string_builder.from_strings 28 + |> string_tree.from_strings 29 29 |> wisp.html_response(200) 30 30 } 31 31 Error(_) -> { ··· 52 52 <button type='submit'>Log in</button> 53 53 </form> 54 54 " 55 - |> string_builder.from_string 55 + |> string_tree.from_string 56 56 |> wisp.html_response(200) 57 57 } 58 58
+3 -2
examples/08-working-with-cookies/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+1 -1
examples/09-configuring-default-responses/README.md
··· 1 - # Wisp Example: Working with form data 1 + # Wisp Example: Configuring default responses 2 2 3 3 ```sh 4 4 gleam run # Run the server
+1 -1
examples/09-configuring-default-responses/gleam.toml
··· 6 6 [dependencies] 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 - mist = "~> 0.14" 9 + mist = ">= 2.0.0 and < 3.0.0" 10 10 gleam_erlang = "~> 0.23" 11 11 12 12 [dev-dependencies]
+2 -2
examples/09-configuring-default-responses/src/app/router.gleam
··· 1 1 import app/web 2 - import gleam/string_builder 2 + import gleam/string_tree 3 3 import wisp.{type Request, type Response} 4 4 5 5 pub fn handle_request(req: Request) -> Response { ··· 9 9 // This request returns a non-empty body. 10 10 [] -> { 11 11 "<h1>Hello, Joe!</h1>" 12 - |> string_builder.from_string 12 + |> string_tree.from_string 13 13 |> wisp.html_response(200) 14 14 } 15 15
+6 -6
examples/09-configuring-default-responses/src/app/web.gleam
··· 1 - import wisp 2 1 import gleam/bool 3 - import gleam/string_builder 2 + import gleam/string_tree 3 + import wisp 4 4 5 5 pub fn middleware( 6 6 req: wisp.Request, ··· 32 32 case response.status { 33 33 404 | 405 -> 34 34 "<h1>There's nothing here</h1>" 35 - |> string_builder.from_string 35 + |> string_tree.from_string 36 36 |> wisp.html_body(response, _) 37 37 38 38 400 | 422 -> 39 39 "<h1>Bad request</h1>" 40 - |> string_builder.from_string 40 + |> string_tree.from_string 41 41 |> wisp.html_body(response, _) 42 42 43 43 413 -> 44 44 "<h1>Request entity too large</h1>" 45 - |> string_builder.from_string 45 + |> string_tree.from_string 46 46 |> wisp.html_body(response, _) 47 47 48 48 500 -> 49 49 "<h1>Internal server error</h1>" 50 - |> string_builder.from_string 50 + |> string_tree.from_string 51 51 |> wisp.html_body(response, _) 52 52 53 53 // For other status codes redirect to the home page
+3 -2
examples/09-configuring-default-responses/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+7 -7
examples/09-configuring-default-responses/test/app_test.gleam
··· 15 15 |> should.equal(200) 16 16 17 17 response.headers 18 - |> should.equal([#("content-type", "text/html")]) 18 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 19 19 20 20 let assert True = 21 21 response ··· 31 31 |> should.equal(500) 32 32 33 33 response.headers 34 - |> should.equal([#("content-type", "text/html")]) 34 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 35 35 36 36 let assert True = 37 37 response ··· 46 46 |> should.equal(422) 47 47 48 48 response.headers 49 - |> should.equal([#("content-type", "text/html")]) 49 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 50 50 51 51 let assert True = 52 52 response ··· 61 61 |> should.equal(400) 62 62 63 63 response.headers 64 - |> should.equal([#("content-type", "text/html")]) 64 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 65 65 66 66 let assert True = 67 67 response ··· 76 76 |> should.equal(405) 77 77 78 78 response.headers 79 - |> should.equal([#("allow", ""), #("content-type", "text/html")]) 79 + |> should.equal([#("allow", ""), #("content-type", "text/html; charset=utf-8")]) 80 80 81 81 let assert True = 82 82 response ··· 91 91 |> should.equal(404) 92 92 93 93 response.headers 94 - |> should.equal([#("content-type", "text/html")]) 94 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 95 95 96 96 let assert True = 97 97 response ··· 106 106 |> should.equal(413) 107 107 108 108 response.headers 109 - |> should.equal([#("content-type", "text/html")]) 109 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 110 110 111 111 let assert True = 112 112 response
+1 -1
examples/10-working-with-files/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = ">= 1.2.0 and < 2.0.0" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 11 gleam_http = "~> 3.5" 12 12 13 13 [dev-dependencies]
+5 -5
examples/10-working-with-files/src/app/router.gleam
··· 1 1 import app/web 2 + import gleam/bytes_tree 2 3 import gleam/http.{Get, Post} 3 4 import gleam/list 4 5 import gleam/result 5 - import gleam/string_builder 6 - import gleam/bytes_builder 6 + import gleam/string_tree 7 7 import wisp.{type Request, type Response} 8 8 9 9 pub fn handle_request(req: Request) -> Response { ··· 35 35 fn show_home(req: Request) -> Response { 36 36 use <- wisp.require_method(req, Get) 37 37 html 38 - |> string_builder.from_string 38 + |> string_tree.from_string 39 39 |> wisp.html_response(200) 40 40 } 41 41 ··· 45 45 // In this case we have the file contents in memory as a string. 46 46 // This is good if we have just made the file, but if the file already exists 47 47 // on the disc then the approach in the next function is more efficient. 48 - let file_contents = bytes_builder.from_string("Hello, Joe!") 48 + let file_contents = bytes_tree.from_string("Hello, Joe!") 49 49 50 50 wisp.ok() 51 51 |> wisp.set_header("content-type", "text/plain") ··· 107 107 case result { 108 108 Ok(name) -> { 109 109 { "<p>Thank you for your file!" <> name <> "</p>" <> html } 110 - |> string_builder.from_string 110 + |> string_tree.from_string 111 111 |> wisp.html_response(200) 112 112 } 113 113 Error(_) -> {
+3 -2
examples/10-working-with-files/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+1 -1
examples/10-working-with-files/test/app_test.gleam
··· 15 15 |> should.equal(200) 16 16 17 17 response.headers 18 - |> should.equal([#("content-type", "text/html")]) 18 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 19 19 20 20 response 21 21 |> testing.string_body
+2 -2
examples/utilities/tiny_database/gleam.toml
··· 6 6 7 7 [dependencies] 8 8 gleam_stdlib = "~> 0.30" 9 - simplifile = "~> 1.0" 10 - ids = "~> 0.8" 9 + simplifile = "~> 2.0" 11 10 gleam_json = "~> 0.6" 11 + youid = ">= 1.1.0 and < 2.0.0" 12 12 13 13 [dev-dependencies] 14 14 gleeunit = "~> 1.0"
+9 -8
examples/utilities/tiny_database/manifest.toml
··· 2 2 # You typically do not need to edit this file 3 3 4 4 packages = [ 5 - { name = "gleam_erlang", version = "0.23.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "C21CFB816C114784E669FFF4BBF433535EEA9960FA2F216209B8691E87156B96" }, 5 + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, 6 + { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, 7 + { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, 6 8 { name = "gleam_json", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB405BD93A8828BCD870463DE29375E7B2D252D9D124C109E5B618AAC00B86FC" }, 7 - { name = "gleam_otp", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang"], otp_app = "gleam_otp", source = "hex", outer_checksum = "18EF8242A5E54BA92F717C7222F03B3228AEE00D1F286D4C56C3E8C18AA2588E" }, 8 - { name = "gleam_stdlib", version = "0.32.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "ABF00CDCCB66FABBCE351A50060964C4ACE798F95A0D78622C8A7DC838792577" }, 9 - { name = "gleeunit", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D3682ED8C5F9CAE1C928F2506DE91625588CC752495988CBE0F5653A42A6F334" }, 10 - { name = "ids", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib", "gleam_otp"], otp_app = "ids", source = "hex", outer_checksum = "912A21722E07E68117B92863D05B15BE97E5AEF4ECF47C2F567CECCD5A4F5739" }, 11 - { name = "simplifile", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0BD6F0E7DA1A7E11D18B8AD48453225CAFCA4C8CFB4513D217B372D2866C501C" }, 9 + { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, 10 + { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, 11 + { name = "simplifile", version = "2.0.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "95219227A43FCFE62C6E494F413A1D56FF953B68FE420698612E3D89A1EFE029" }, 12 12 { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, 13 + { name = "youid", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_stdlib"], otp_app = "youid", source = "hex", outer_checksum = "15BF3E8173C8741930E23D22071CD55AE203B6E43B9E0C6C9E7D9F116808E418" }, 13 14 ] 14 15 15 16 [requirements] 16 17 gleam_json = { version = "~> 0.6" } 17 18 gleam_stdlib = { version = "~> 0.30" } 18 19 gleeunit = { version = "~> 1.0" } 19 - ids = { version = "~> 0.8" } 20 - simplifile = { version = "~> 1.0" } 20 + simplifile = { version = "~> 2.0" } 21 + youid = { version = ">= 1.1.0 and < 2.0.0" }
+2 -2
examples/utilities/tiny_database/src/tiny_database.gleam
··· 3 3 import gleam/json 4 4 import gleam/list 5 5 import gleam/result 6 - import ids/nanoid 7 6 import simplifile 7 + import youid/uuid 8 8 9 9 pub opaque type Connection { 10 10 Connection(root: String) ··· 44 44 values: Dict(String, String), 45 45 ) -> Result(String, Nil) { 46 46 let assert Ok(_) = simplifile.create_directory_all(connection.root) 47 - let id = nanoid.generate() 47 + let id = uuid.v4_string() 48 48 let values = 49 49 values 50 50 |> dict.to_list
+2 -2
examples/utilities/tiny_database/test/tiny_database_test.gleam
··· 1 - import gleam/map 1 + import gleam/dict 2 2 import gleeunit 3 3 import gleeunit/should 4 4 import tiny_database ··· 10 10 pub fn insert_read_test() { 11 11 let connection = tiny_database.connect("tmp/data") 12 12 13 - let data = map.from_list([#("name", "Alice"), #("profession", "Programmer")]) 13 + let data = dict.from_list([#("name", "Alice"), #("profession", "Programmer")]) 14 14 15 15 let assert Ok(Nil) = tiny_database.truncate(connection) 16 16 let assert Ok([]) = tiny_database.list(connection)
+10 -11
gleam.toml
··· 1 1 name = "wisp" 2 - version = "0.14.0" 3 - gleam = ">= 0.32.0" 2 + version = "1.3.0" 3 + gleam = ">= 1.4.0" 4 4 description = "A practical web framework for Gleam" 5 5 licences = ["Apache-2.0"] 6 6 7 7 repository = { type = "github", user = "gleam-wisp", repo = "wisp" } 8 - links = [ 9 - { title = "Sponsor", href = "https://github.com/sponsors/lpil" }, 10 - ] 8 + links = [{ title = "Sponsor", href = "https://github.com/sponsors/lpil" }] 11 9 12 10 [dependencies] 13 11 exception = ">= 2.0.0 and < 3.0.0" 14 12 gleam_crypto = ">= 1.0.0 and < 2.0.0" 15 13 gleam_erlang = ">= 0.21.0 and < 2.0.0" 16 14 gleam_http = ">= 3.5.0 and < 4.0.0" 17 - gleam_json = ">= 0.6.0 and < 2.0.0" 18 - gleam_stdlib = ">= 0.29.0 and < 2.0.0" 19 - mist = ">= 1.2.0 and < 2.0.0" 20 - simplifile = ">= 1.4.0 and != 1.6.0 and < 2.0.0" 15 + gleam_json = ">= 0.6.0 and < 3.0.0" 16 + gleam_stdlib = ">= 0.43.0 and < 2.0.0" 17 + mist = ">= 1.2.0 and < 4.0.0" 18 + simplifile = ">= 2.0.0 and < 3.0.0" 21 19 marceau = ">= 1.1.0 and < 2.0.0" 22 - logging = ">= 1.0.0 and < 2.0.0" 20 + logging = ">= 1.2.0 and < 2.0.0" 21 + directories = ">= 1.0.0 and < 2.0.0" 23 22 24 23 [dev-dependencies] 25 - gleeunit = "~> 1.0" 24 + gleeunit = ">= 1.0.0 and < 2.0.0"
+24 -20
manifest.toml
··· 3 3 4 4 packages = [ 5 5 { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, 6 + { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, 7 + { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, 6 8 { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, 7 - { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, 8 - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, 9 - { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, 10 - { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, 11 - { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, 12 - { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, 13 - { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, 14 - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, 15 - { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, 9 + { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, 10 + { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, 11 + { name = "gleam_erlang", version = "0.30.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "760618870AE4A497B10C73548E6E44F43B76292A54F0207B3771CBB599C675B4" }, 12 + { name = "gleam_http", version = "3.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "A9EE0722106FCCAB8AD3BF9D0A3EFF92BFE8561D59B83BAE96EB0BE1938D4E0F" }, 13 + { name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" }, 14 + { name = "gleam_otp", version = "0.14.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5A8CE8DBD01C29403390A7BD5C0A63D26F865C83173CF9708E6E827E53159C65" }, 15 + { name = "gleam_stdlib", version = "0.43.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "69EF22E78FDCA9097CBE7DF91C05B2A8B5436826D9F66680D879182C0860A747" }, 16 + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 17 + { name = "glisten", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "912132751031473CB38F454120124FFC96AF6B0EA33D92C9C90DB16327A2A972" }, 16 18 { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, 17 19 { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 18 - { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" }, 19 - { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, 20 - { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, 20 + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 21 + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 22 + { name = "mist", version = "3.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "CDA1A74E768419235E16886463EC4722EFF4AB3F8D820A76EAD45D7C167D7282" }, 23 + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, 21 24 { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, 22 - { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, 23 - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, 25 + { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, 26 + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 24 27 ] 25 28 26 29 [requirements] 30 + directories = { version = ">= 1.0.0 and < 2.0.0" } 27 31 exception = { version = ">= 2.0.0 and < 3.0.0" } 28 32 gleam_crypto = { version = ">= 1.0.0 and < 2.0.0" } 29 33 gleam_erlang = { version = ">= 0.21.0 and < 2.0.0" } 30 34 gleam_http = { version = ">= 3.5.0 and < 4.0.0" } 31 - gleam_json = { version = ">= 0.6.0 and < 2.0.0" } 32 - gleam_stdlib = { version = ">= 0.29.0 and < 2.0.0" } 33 - gleeunit = { version = "~> 1.0" } 34 - logging = { version = ">= 1.0.0 and < 2.0.0" } 35 + gleam_json = { version = ">= 0.6.0 and < 3.0.0" } 36 + gleam_stdlib = { version = ">= 0.43.0 and < 2.0.0" } 37 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 38 + logging = { version = ">= 1.2.0 and < 2.0.0" } 35 39 marceau = { version = ">= 1.1.0 and < 2.0.0" } 36 - mist = { version = ">= 1.2.0 and < 2.0.0" } 37 - simplifile = { version = ">= 1.4.0 and != 1.6.0 and < 2.0.0" } 40 + mist = { version = ">= 1.2.0 and < 4.0.0" } 41 + simplifile = { version = ">= 2.0.0 and < 3.0.0" }
+90
src/wisp/internal.gleam
··· 1 + import directories 2 + import gleam/bit_array 3 + import gleam/crypto 4 + import gleam/string 5 + 6 + // HELPERS 7 + 8 + // 9 + // Requests 10 + // 11 + 12 + /// The connection to the client for a HTTP request. 13 + /// 14 + /// The body of the request can be read from this connection using functions 15 + /// such as `require_multipart_body`. 16 + /// 17 + pub type Connection { 18 + Connection( 19 + reader: Reader, 20 + max_body_size: Int, 21 + max_files_size: Int, 22 + read_chunk_size: Int, 23 + secret_key_base: String, 24 + temporary_directory: String, 25 + ) 26 + } 27 + 28 + pub fn make_connection( 29 + body_reader: Reader, 30 + secret_key_base: String, 31 + ) -> Connection { 32 + // Fallback to current working directory when no valid tmp directory exists 33 + let prefix = case directories.tmp_dir() { 34 + Ok(tmp_dir) -> tmp_dir <> "/gleam-wisp/" 35 + Error(_) -> "./tmp/" 36 + } 37 + let temporary_directory = join_path(prefix, random_slug()) 38 + Connection( 39 + reader: body_reader, 40 + max_body_size: 8_000_000, 41 + max_files_size: 32_000_000, 42 + read_chunk_size: 1_000_000, 43 + temporary_directory: temporary_directory, 44 + secret_key_base: secret_key_base, 45 + ) 46 + } 47 + 48 + type Reader = 49 + fn(Int) -> Result(Read, Nil) 50 + 51 + pub type Read { 52 + Chunk(BitArray, next: Reader) 53 + ReadingFinished 54 + } 55 + 56 + // 57 + // Middleware Helpers 58 + // 59 + 60 + pub fn remove_preceeding_slashes(string: String) -> String { 61 + case string { 62 + "/" <> rest -> remove_preceeding_slashes(rest) 63 + _ -> string 64 + } 65 + } 66 + 67 + // TODO: replace with simplifile function when it exists 68 + pub fn join_path(a: String, b: String) -> String { 69 + let b = remove_preceeding_slashes(b) 70 + case string.ends_with(a, "/") { 71 + True -> a <> b 72 + False -> a <> "/" <> b 73 + } 74 + } 75 + 76 + // 77 + // Cryptography 78 + // 79 + 80 + /// Generate a random string of the given length. 81 + /// 82 + pub fn random_string(length: Int) -> String { 83 + crypto.strong_random_bytes(length) 84 + |> bit_array.base64_url_encode(False) 85 + |> string.slice(0, length) 86 + } 87 + 88 + pub fn random_slug() -> String { 89 + random_string(16) 90 + }
+6 -7
src/wisp/testing.gleam
··· 1 1 import gleam/bit_array 2 - import gleam/bytes_builder 2 + import gleam/bytes_tree 3 3 import gleam/crypto 4 4 import gleam/http 5 5 import gleam/http/request 6 6 import gleam/json.{type Json} 7 7 import gleam/option.{None, Some} 8 8 import gleam/string 9 - import gleam/string_builder 9 + import gleam/string_tree 10 10 import gleam/uri 11 11 import simplifile 12 12 import wisp.{type Request, type Response, Bytes, Empty, File, Text} ··· 227 227 pub fn string_body(response: Response) -> String { 228 228 case response.body { 229 229 Empty -> "" 230 - Text(builder) -> string_builder.to_string(builder) 230 + Text(tree) -> string_tree.to_string(tree) 231 231 Bytes(bytes) -> { 232 - let data = bytes_builder.to_bit_array(bytes) 232 + let data = bytes_tree.to_bit_array(bytes) 233 233 let assert Ok(string) = bit_array.to_string(data) 234 234 string 235 235 } ··· 250 250 pub fn bit_array_body(response: Response) -> BitArray { 251 251 case response.body { 252 252 Empty -> <<>> 253 - Bytes(builder) -> bytes_builder.to_bit_array(builder) 254 - Text(builder) -> 255 - bytes_builder.to_bit_array(bytes_builder.from_string_builder(builder)) 253 + Bytes(tree) -> bytes_tree.to_bit_array(tree) 254 + Text(tree) -> bytes_tree.to_bit_array(bytes_tree.from_string_tree(tree)) 256 255 File(path) -> { 257 256 let assert Ok(contents) = simplifile.read_bits(path) 258 257 contents
+96
src/wisp/wisp_mist.gleam
··· 1 + import exception 2 + import gleam/bytes_tree 3 + import gleam/http/request.{type Request as HttpRequest} 4 + import gleam/http/response.{type Response as HttpResponse} 5 + import gleam/option 6 + import gleam/result 7 + import gleam/string 8 + import mist 9 + import wisp 10 + import wisp/internal 11 + 12 + // 13 + // Running the server 14 + // 15 + 16 + /// Convert a Wisp request handler into a function that can be run with the Mist 17 + /// web server. 18 + /// 19 + /// # Examples 20 + /// 21 + /// ```gleam 22 + /// pub fn main() { 23 + /// let secret_key_base = "..." 24 + /// let assert Ok(_) = 25 + /// handle_request 26 + /// |> wisp_mist.handler(secret_key_base) 27 + /// |> mist.new 28 + /// |> mist.port(8000) 29 + /// |> mist.start_http 30 + /// process.sleep_forever() 31 + /// } 32 + /// ``` 33 + pub fn handler( 34 + handler: fn(wisp.Request) -> wisp.Response, 35 + secret_key_base: String, 36 + ) -> fn(HttpRequest(mist.Connection)) -> HttpResponse(mist.ResponseData) { 37 + fn(request: HttpRequest(_)) { 38 + let connection = 39 + internal.make_connection(mist_body_reader(request), secret_key_base) 40 + let request = request.set_body(request, connection) 41 + 42 + use <- exception.defer(fn() { 43 + let assert Ok(_) = wisp.delete_temporary_files(request) 44 + }) 45 + 46 + let response = 47 + request 48 + |> handler 49 + |> mist_response 50 + 51 + response 52 + } 53 + } 54 + 55 + fn mist_body_reader(request: HttpRequest(mist.Connection)) -> internal.Reader { 56 + case mist.stream(request) { 57 + Error(_) -> fn(_) { Ok(internal.ReadingFinished) } 58 + Ok(stream) -> fn(size) { wrap_mist_chunk(stream(size)) } 59 + } 60 + } 61 + 62 + fn wrap_mist_chunk( 63 + chunk: Result(mist.Chunk, mist.ReadError), 64 + ) -> Result(internal.Read, Nil) { 65 + chunk 66 + |> result.replace_error(Nil) 67 + |> result.map(fn(chunk) { 68 + case chunk { 69 + mist.Done -> internal.ReadingFinished 70 + mist.Chunk(data, consume) -> 71 + internal.Chunk(data, fn(size) { wrap_mist_chunk(consume(size)) }) 72 + } 73 + }) 74 + } 75 + 76 + fn mist_response(response: wisp.Response) -> HttpResponse(mist.ResponseData) { 77 + let body = case response.body { 78 + wisp.Empty -> mist.Bytes(bytes_tree.new()) 79 + wisp.Text(text) -> mist.Bytes(bytes_tree.from_string_tree(text)) 80 + wisp.Bytes(bytes) -> mist.Bytes(bytes) 81 + wisp.File(path) -> mist_send_file(path) 82 + } 83 + response 84 + |> response.set_body(body) 85 + } 86 + 87 + fn mist_send_file(path: String) -> mist.ResponseData { 88 + case mist.send_file(path, offset: 0, limit: option.None) { 89 + Ok(body) -> body 90 + Error(error) -> { 91 + wisp.log_error(string.inspect(error)) 92 + // TODO: return 500 93 + mist.Bytes(bytes_tree.new()) 94 + } 95 + } 96 + }
+130 -203
src/wisp.gleam
··· 1 1 import exception 2 2 import gleam/bit_array 3 3 import gleam/bool 4 - import gleam/bytes_builder.{type BytesBuilder} 4 + import gleam/bytes_tree.{type BytesTree} 5 5 import gleam/crypto 6 6 import gleam/dict.{type Dict} 7 7 import gleam/dynamic.{type Dynamic} ··· 19 19 import gleam/option.{type Option} 20 20 import gleam/result 21 21 import gleam/string 22 - import gleam/string_builder.{type StringBuilder} 22 + import gleam/string_tree.{type StringTree} 23 23 import gleam/uri 24 24 import logging 25 25 import marceau 26 - import mist 27 26 import simplifile 28 - 29 - // 30 - // Running the server 31 - // 32 - 33 - /// Convert a Wisp request handler into a function that can be run with the Mist 34 - /// web server. 35 - /// 36 - /// # Examples 37 - /// 38 - /// ```gleam 39 - /// pub fn main() { 40 - /// let secret_key_base = "..." 41 - /// let assert Ok(_) = 42 - /// handle_request 43 - /// |> wisp.mist_handler(secret_key_base) 44 - /// |> mist.new 45 - /// |> mist.port(8000) 46 - /// |> mist.start_http 47 - /// process.sleep_forever() 48 - /// } 49 - /// ``` 50 - pub fn mist_handler( 51 - handler: fn(Request) -> Response, 52 - secret_key_base: String, 53 - ) -> fn(HttpRequest(mist.Connection)) -> HttpResponse(mist.ResponseData) { 54 - fn(request: HttpRequest(_)) { 55 - let connection = make_connection(mist_body_reader(request), secret_key_base) 56 - let request = request.set_body(request, connection) 57 - 58 - use <- exception.defer(fn() { 59 - let assert Ok(_) = delete_temporary_files(request) 60 - }) 61 - 62 - let response = 63 - request 64 - |> handler 65 - |> mist_response 66 - 67 - response 68 - } 69 - } 70 - 71 - fn mist_body_reader(request: HttpRequest(mist.Connection)) -> Reader { 72 - case mist.stream(request) { 73 - Error(_) -> fn(_) { Ok(ReadingFinished) } 74 - Ok(stream) -> fn(size) { wrap_mist_chunk(stream(size)) } 75 - } 76 - } 77 - 78 - fn wrap_mist_chunk( 79 - chunk: Result(mist.Chunk, mist.ReadError), 80 - ) -> Result(Read, Nil) { 81 - chunk 82 - |> result.nil_error 83 - |> result.map(fn(chunk) { 84 - case chunk { 85 - mist.Done -> ReadingFinished 86 - mist.Chunk(data, consume) -> 87 - Chunk(data, fn(size) { wrap_mist_chunk(consume(size)) }) 88 - } 89 - }) 90 - } 91 - 92 - fn mist_response(response: Response) -> HttpResponse(mist.ResponseData) { 93 - let body = case response.body { 94 - Empty -> mist.Bytes(bytes_builder.new()) 95 - Text(text) -> mist.Bytes(bytes_builder.from_string_builder(text)) 96 - Bytes(bytes) -> mist.Bytes(bytes) 97 - File(path) -> mist_send_file(path) 98 - } 99 - response 100 - |> response.set_body(body) 101 - } 102 - 103 - fn mist_send_file(path: String) -> mist.ResponseData { 104 - case mist.send_file(path, offset: 0, limit: option.None) { 105 - Ok(body) -> body 106 - Error(error) -> { 107 - log_error(string.inspect(error)) 108 - // TODO: return 500 109 - mist.Bytes(bytes_builder.new()) 110 - } 111 - } 112 - } 27 + import wisp/internal 113 28 114 29 // 115 30 // Responses ··· 120 35 pub type Body { 121 36 /// A body of unicode text. 122 37 /// 123 - /// The body is represented using a `StringBuilder`. If you have a `String` 124 - /// you can use the `string_builder.from_string` function to convert it. 38 + /// The body is represented using a `StringTree`. If you have a `String` 39 + /// you can use the `string_tree.from_string` function to convert it. 125 40 /// 126 - Text(StringBuilder) 41 + Text(StringTree) 127 42 /// A body of binary data. 128 43 /// 129 - /// The body is represented using a `StringBuilder`. If you have a `String` 130 - /// you can use the `string_builder.from_string` function to convert it. 44 + /// The body is represented using a `BytesTree`. If you have a `BitArray` 45 + /// you can use the `bytes_tree.from_bit_array` function to convert it. 131 46 /// 132 - Bytes(BytesBuilder) 47 + Bytes(BytesTree) 133 48 /// A body of the contents of a file. 134 49 /// 135 50 /// This will be sent efficiently using the `send_file` function of the ··· 231 146 /// # Examples 232 147 /// 233 148 /// ```gleam 149 + /// let content = bytes_tree.from_string("Hello, Joe!") 234 150 /// response(200) 235 - /// |> file_download_from_memory(named: "myfile.txt", containing: "Hello, Joe!") 151 + /// |> file_download_from_memory(named: "myfile.txt", containing: content) 236 152 /// // -> Response( 237 153 /// // 200, 238 154 /// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")], ··· 243 159 pub fn file_download_from_memory( 244 160 response: Response, 245 161 named name: String, 246 - containing data: BytesBuilder, 162 + containing data: BytesTree, 247 163 ) -> Response { 248 164 let name = uri.percent_encode(name) 249 165 response ··· 262 178 /// # Examples 263 179 /// 264 180 /// ```gleam 265 - /// let body = string_builder.from_string("<h1>Hello, Joe!</h1>") 181 + /// let body = string_tree.from_string("<h1>Hello, Joe!</h1>") 266 182 /// html_response(body, 200) 267 183 /// // -> Response(200, [#("content-type", "text/html")], Text(body)) 268 184 /// ``` 269 185 /// 270 - pub fn html_response(html: StringBuilder, status: Int) -> Response { 271 - HttpResponse(status, [#("content-type", "text/html")], Text(html)) 186 + pub fn html_response(html: StringTree, status: Int) -> Response { 187 + HttpResponse( 188 + status, 189 + [#("content-type", "text/html; charset=utf-8")], 190 + Text(html), 191 + ) 272 192 } 273 193 274 194 /// Create a JSON response. ··· 279 199 /// # Examples 280 200 /// 281 201 /// ```gleam 282 - /// let body = string_builder.from_string("{\"name\": \"Joe\"}") 202 + /// let body = string_tree.from_string("{\"name\": \"Joe\"}") 283 203 /// json_response(body, 200) 284 204 /// // -> Response(200, [#("content-type", "application/json")], Text(body)) 285 205 /// ``` 286 206 /// 287 - pub fn json_response(json: StringBuilder, status: Int) -> Response { 288 - HttpResponse(status, [#("content-type", "application/json")], Text(json)) 207 + pub fn json_response(json: StringTree, status: Int) -> Response { 208 + HttpResponse( 209 + status, 210 + [#("content-type", "application/json; charset=utf-8")], 211 + Text(json), 212 + ) 289 213 } 290 214 291 215 /// Set the body of a response to a given HTML document, and set the ··· 296 220 /// # Examples 297 221 /// 298 222 /// ```gleam 299 - /// let body = string_builder.from_string("<h1>Hello, Joe!</h1>") 223 + /// let body = string_tree.from_string("<h1>Hello, Joe!</h1>") 300 224 /// response(201) 301 225 /// |> html_body(body) 302 - /// // -> Response(201, [#("content-type", "text/html")], Text(body)) 226 + /// // -> Response(201, [#("content-type", "text/html; charset=utf-8")], Text(body)) 303 227 /// ``` 304 228 /// 305 - pub fn html_body(response: Response, html: StringBuilder) -> Response { 229 + pub fn html_body(response: Response, html: StringTree) -> Response { 306 230 response 307 231 |> response.set_body(Text(html)) 308 - |> response.set_header("content-type", "text/html") 232 + |> response.set_header("content-type", "text/html; charset=utf-8") 309 233 } 310 234 311 235 /// Set the body of a response to a given JSON document, and set the ··· 316 240 /// # Examples 317 241 /// 318 242 /// ```gleam 319 - /// let body = string_builder.from_string("{\"name\": \"Joe\"}") 243 + /// let body = string_tree.from_string("{\"name\": \"Joe\"}") 320 244 /// response(201) 321 245 /// |> json_body(body) 322 - /// // -> Response(201, [#("content-type", "application/json")], Text(body)) 246 + /// // -> Response(201, [#("content-type", "application/json; charset=utf-8")], Text(body)) 323 247 /// ``` 324 248 /// 325 - pub fn json_body(response: Response, json: StringBuilder) -> Response { 249 + pub fn json_body(response: Response, json: StringTree) -> Response { 326 250 response 327 251 |> response.set_body(Text(json)) 328 - |> response.set_header("content-type", "application/json") 252 + |> response.set_header("content-type", "application/json; charset=utf-8") 329 253 } 330 254 331 - /// Set the body of a response to a given string builder. 255 + /// Set the body of a response to a given string tree. 332 256 /// 333 257 /// You likely want to also set the request `content-type` header to an 334 258 /// appropriate value for the format of the content. ··· 336 260 /// # Examples 337 261 /// 338 262 /// ```gleam 339 - /// let body = string_builder.from_string("Hello, Joe!") 263 + /// let body = string_tree.from_string("Hello, Joe!") 340 264 /// response(201) 341 - /// |> string_builder_body(body) 265 + /// |> string_tree_body(body) 342 266 /// // -> Response(201, [], Text(body)) 343 267 /// ``` 344 268 /// 345 - pub fn string_builder_body( 346 - response: Response, 347 - content: StringBuilder, 348 - ) -> Response { 269 + pub fn string_tree_body(response: Response, content: StringTree) -> Response { 349 270 response 350 271 |> response.set_body(Text(content)) 351 272 } 352 273 353 - /// Set the body of a response to a given string builder. 274 + /// Set the body of a response to a given string. 354 275 /// 355 276 /// You likely want to also set the request `content-type` header to an 356 277 /// appropriate value for the format of the content. ··· 364 285 /// // -> Response( 365 286 /// // 201, 366 287 /// // [], 367 - /// // Text(string_builder.from_string("Hello, Joe")) 288 + /// // Text(string_tree.from_string("Hello, Joe")) 368 289 /// // ) 369 290 /// ``` 370 291 /// 371 292 pub fn string_body(response: Response, content: String) -> Response { 372 293 response 373 - |> response.set_body(Text(string_builder.from_string(content))) 294 + |> response.set_body(Text(string_tree.from_string(content))) 374 295 } 375 296 376 297 /// Escape a string so that it can be safely included in a HTML document. ··· 710 631 /// The body of the request can be read from this connection using functions 711 632 /// such as `require_multipart_body`. 712 633 /// 713 - pub opaque type Connection { 714 - Connection( 715 - reader: Reader, 716 - max_body_size: Int, 717 - max_files_size: Int, 718 - read_chunk_size: Int, 719 - secret_key_base: String, 720 - temporary_directory: String, 721 - ) 722 - } 723 - 724 - fn make_connection(body_reader: Reader, secret_key_base: String) -> Connection { 725 - // TODO: replace `/tmp` with appropriate for the OS 726 - let prefix = "/tmp/gleam-wisp/" 727 - let temporary_directory = join_path(prefix, random_slug()) 728 - Connection( 729 - reader: body_reader, 730 - max_body_size: 8_000_000, 731 - max_files_size: 32_000_000, 732 - read_chunk_size: 1_000_000, 733 - temporary_directory: temporary_directory, 734 - secret_key_base: secret_key_base, 735 - ) 736 - } 634 + pub type Connection = 635 + internal.Connection 737 636 738 637 type BufferedReader { 739 - BufferedReader(reader: Reader, buffer: BitArray) 638 + BufferedReader(reader: internal.Reader, buffer: BitArray) 740 639 } 741 640 742 641 type Quotas { ··· 758 657 } 759 658 } 760 659 761 - fn buffered_read(reader: BufferedReader, chunk_size: Int) -> Result(Read, Nil) { 660 + fn buffered_read( 661 + reader: BufferedReader, 662 + chunk_size: Int, 663 + ) -> Result(internal.Read, Nil) { 762 664 case reader.buffer { 763 665 <<>> -> reader.reader(chunk_size) 764 - _ -> Ok(Chunk(reader.buffer, reader.reader)) 666 + _ -> Ok(internal.Chunk(reader.buffer, reader.reader)) 765 667 } 766 668 } 767 669 768 - type Reader = 769 - fn(Int) -> Result(Read, Nil) 770 - 771 - type Read { 772 - Chunk(BitArray, next: Reader) 773 - ReadingFinished 774 - } 775 - 776 670 /// Set the maximum permitted size of a request body of the request in bytes. 777 671 /// 778 672 /// If a body is larger than this size attempting to read the body will result ··· 784 678 /// instead use the `max_files_size` limit. 785 679 /// 786 680 pub fn set_max_body_size(request: Request, size: Int) -> Request { 787 - Connection(..request.body, max_body_size: size) 681 + internal.Connection(..request.body, max_body_size: size) 788 682 |> request.set_body(request, _) 789 683 } 790 684 ··· 808 702 case string.byte_size(key) < 64 { 809 703 True -> panic as "Secret key base must be at least 64 bytes long" 810 704 False -> 811 - Connection(..request.body, secret_key_base: key) 705 + internal.Connection(..request.body, secret_key_base: key) 812 706 |> request.set_body(request, _) 813 707 } 814 708 } ··· 827 721 /// 828 722 /// This limit only applies for files in a multipart body that get streamed to 829 723 /// disc. For headers and other content that gets read into memory use the 830 - /// `max_files_size` limit. 724 + /// `max_body_size` limit. 831 725 /// 832 726 pub fn set_max_files_size(request: Request, size: Int) -> Request { 833 - Connection(..request.body, max_files_size: size) 727 + internal.Connection(..request.body, max_files_size: size) 834 728 |> request.set_body(request, _) 835 729 } 836 730 ··· 850 744 /// been received from the client. 851 745 /// 852 746 pub fn set_read_chunk_size(request: Request, size: Int) -> Request { 853 - Connection(..request.body, read_chunk_size: size) 747 + internal.Connection(..request.body, read_chunk_size: size) 854 748 |> request.set_body(request, _) 855 749 } 856 750 ··· 864 758 /// A convenient alias for a HTTP request with a Wisp connection as the body. 865 759 /// 866 760 pub type Request = 867 - HttpRequest(Connection) 761 + HttpRequest(internal.Connection) 868 762 869 763 /// This middleware function ensures that the request has a specific HTTP 870 764 /// method, returning an empty response with status code 405: Method not allowed ··· 1062 956 } 1063 957 1064 958 fn read_body_loop( 1065 - reader: Reader, 959 + reader: internal.Reader, 1066 960 read_chunk_size: Int, 1067 961 max_body_size: Int, 1068 962 accumulator: BitArray, 1069 963 ) -> Result(BitArray, Nil) { 1070 964 use chunk <- result.try(reader(read_chunk_size)) 1071 965 case chunk { 1072 - ReadingFinished -> Ok(accumulator) 1073 - Chunk(chunk, next) -> { 966 + internal.ReadingFinished -> Ok(accumulator) 967 + internal.Chunk(chunk, next) -> { 1074 968 let accumulator = bit_array.append(accumulator, chunk) 1075 969 case bit_array.byte_size(accumulator) > max_body_size { 1076 970 True -> Error(Nil) ··· 1157 1051 next: fn() -> Response, 1158 1052 ) -> Response { 1159 1053 case list.key_find(request.headers, "content-type") { 1160 - Ok(content_type) if content_type == expected -> next() 1054 + Ok(content_type) -> 1055 + // This header may have further such as `; charset=utf-8`, so discard 1056 + // that if it exists. 1057 + case string.split_once(content_type, ";") { 1058 + Ok(#(content_type, _)) if content_type == expected -> next() 1059 + _ if content_type == expected -> next() 1060 + _ -> unsupported_media_type([expected]) 1061 + } 1062 + 1161 1063 _ -> unsupported_media_type([expected]) 1162 1064 } 1163 1065 } ··· 1368 1270 fn read_chunk( 1369 1271 reader: BufferedReader, 1370 1272 chunk_size: Int, 1371 - ) -> Result(#(BitArray, Reader), Response) { 1273 + ) -> Result(#(BitArray, internal.Reader), Response) { 1372 1274 buffered_read(reader, chunk_size) 1373 1275 |> result.replace_error(bad_request()) 1374 1276 |> result.try(fn(chunk) { 1375 1277 case chunk { 1376 - Chunk(chunk, next) -> Ok(#(chunk, next)) 1377 - ReadingFinished -> Error(bad_request()) 1278 + internal.Chunk(chunk, next) -> Ok(#(chunk, next)) 1279 + internal.ReadingFinished -> Error(bad_request()) 1378 1280 } 1379 1281 }) 1380 1282 } ··· 1518 1420 response 1519 1421 } 1520 1422 1521 - fn remove_preceeding_slashes(string: String) -> String { 1522 - case string { 1523 - "/" <> rest -> remove_preceeding_slashes(rest) 1524 - _ -> string 1525 - } 1526 - } 1527 - 1528 - // TODO: replace with simplifile function when it exists 1529 - fn join_path(a: String, b: String) -> String { 1530 - let b = remove_preceeding_slashes(b) 1531 - case string.ends_with(a, "/") { 1532 - True -> a <> b 1533 - False -> a <> "/" <> b 1534 - } 1535 - } 1536 - 1537 1423 /// A middleware function that serves files from a directory, along with a 1538 1424 /// suitable `content-type` header for known file extensions. 1539 1425 /// ··· 1581 1467 from directory: String, 1582 1468 next handler: fn() -> Response, 1583 1469 ) -> Response { 1584 - let path = remove_preceeding_slashes(req.path) 1585 - let prefix = remove_preceeding_slashes(prefix) 1470 + let path = internal.remove_preceeding_slashes(req.path) 1471 + let prefix = internal.remove_preceeding_slashes(prefix) 1586 1472 case req.method, string.starts_with(path, prefix) { 1587 1473 http.Get, True -> { 1588 1474 let path = 1589 1475 path 1590 - |> string.drop_left(string.length(prefix)) 1476 + |> string.drop_start(string.length(prefix)) 1591 1477 |> string.replace(each: "..", with: "") 1592 - |> join_path(directory, _) 1478 + |> internal.join_path(directory, _) 1593 1479 1594 1480 let mime_type = 1595 1481 req.path ··· 1598 1484 |> result.unwrap("") 1599 1485 |> marceau.extension_to_mime_type 1600 1486 1601 - case simplifile.verify_is_file(path) { 1487 + let content_type = case mime_type { 1488 + "application/json" | "text/" <> _ -> mime_type <> "; charset=utf-8" 1489 + _ -> mime_type 1490 + } 1491 + 1492 + case simplifile.is_file(path) { 1602 1493 Ok(True) -> 1603 1494 response.new(200) 1604 - |> response.set_header("content-type", mime_type) 1495 + |> response.set_header("content-type", content_type) 1605 1496 |> response.set_body(File(path)) 1606 1497 _ -> handler() 1607 1498 } ··· 1648 1539 1649 1540 /// Create a new temporary directory for the given request. 1650 1541 /// 1651 - /// If you are using the `mist_handler` function or another compliant web server 1542 + /// If you are using the Mist adapter or another compliant web server 1652 1543 /// adapter then this file will be deleted for you when the request is complete. 1653 1544 /// Otherwise you will need to call the `delete_temporary_files` function 1654 1545 /// yourself. ··· 1658 1549 ) -> Result(String, simplifile.FileError) { 1659 1550 let directory = request.body.temporary_directory 1660 1551 use _ <- result.try(simplifile.create_directory_all(directory)) 1661 - let path = join_path(directory, random_slug()) 1552 + let path = internal.join_path(directory, internal.random_slug()) 1662 1553 use _ <- result.map(simplifile.create_file(path)) 1663 1554 path 1664 1555 } 1665 1556 1666 1557 /// Delete any temporary files created for the given request. 1667 1558 /// 1668 - /// If you are using the `mist_handler` function or another compliant web server 1559 + /// If you are using the Mist adapter or another compliant web server 1669 1560 /// adapter then this file will be deleted for you when the request is complete. 1670 1561 /// Otherwise you will need to call this function yourself. 1671 1562 /// ··· 1708 1599 logging.configure() 1709 1600 } 1710 1601 1602 + /// Type to set the log level of the Erlang's logger 1603 + /// 1604 + /// See the [Erlang logger documentation][1] for more information. 1605 + /// 1606 + /// [1]: https://www.erlang.org/doc/man/logger 1607 + /// 1608 + pub type LogLevel { 1609 + EmergencyLevel 1610 + AlertLevel 1611 + CriticalLevel 1612 + ErrorLevel 1613 + WarningLevel 1614 + NoticeLevel 1615 + InfoLevel 1616 + DebugLevel 1617 + } 1618 + 1619 + fn log_level_to_logging_log_level(log_level: LogLevel) -> logging.LogLevel { 1620 + case log_level { 1621 + EmergencyLevel -> logging.Emergency 1622 + AlertLevel -> logging.Alert 1623 + CriticalLevel -> logging.Critical 1624 + ErrorLevel -> logging.Error 1625 + WarningLevel -> logging.Warning 1626 + NoticeLevel -> logging.Notice 1627 + InfoLevel -> logging.Info 1628 + DebugLevel -> logging.Debug 1629 + } 1630 + } 1631 + 1632 + /// Set the log level of the Erlang logger to `log_level`. 1633 + /// 1634 + /// See the [Erlang logger documentation][1] for more information. 1635 + /// 1636 + /// [1]: https://www.erlang.org/doc/man/logger 1637 + /// 1638 + pub fn set_logger_level(log_level: LogLevel) -> Nil { 1639 + logging.set_level(log_level_to_logging_log_level(log_level)) 1640 + } 1641 + 1711 1642 /// Log a message to the Erlang logger with the level of `emergency`. 1712 1643 /// 1713 1644 /// See the [Erlang logger documentation][1] for more information. ··· 1795 1726 /// Generate a random string of the given length. 1796 1727 /// 1797 1728 pub fn random_string(length: Int) -> String { 1798 - crypto.strong_random_bytes(length) 1799 - |> bit_array.base64_url_encode(False) 1800 - |> string.slice(0, length) 1729 + internal.random_string(length) 1801 1730 } 1802 1731 1803 1732 /// Sign a message which can later be verified using the `verify_signed_message` ··· 1832 1761 crypto.verify_signed_message(message, <<request.body.secret_key_base:utf8>>) 1833 1762 } 1834 1763 1835 - fn random_slug() -> String { 1836 - random_string(16) 1837 - } 1838 - 1839 1764 // 1840 1765 // Cookies 1841 1766 // ··· 1941 1866 pub fn create_canned_connection( 1942 1867 body: BitArray, 1943 1868 secret_key_base: String, 1944 - ) -> Connection { 1945 - make_connection( 1946 - fn(_size) { Ok(Chunk(body, fn(_size) { Ok(ReadingFinished) })) }, 1869 + ) -> internal.Connection { 1870 + internal.make_connection( 1871 + fn(_size) { 1872 + Ok(internal.Chunk(body, fn(_size) { Ok(internal.ReadingFinished) })) 1873 + }, 1947 1874 secret_key_base, 1948 1875 ) 1949 1876 }
test/fixture.dat

This is a binary file and will not be displayed.

+3 -3
test/wisp/testing_test.gleam
··· 2 2 import gleam/http/response 3 3 import gleam/json 4 4 import gleam/option.{None, Some} 5 - import gleam/string_builder 5 + import gleam/string_tree 6 6 import gleeunit/should 7 7 import wisp 8 8 import wisp/testing ··· 502 502 503 503 pub fn string_body_text_test() { 504 504 wisp.ok() 505 - |> response.set_body(wisp.Text(string_builder.from_string("Hello, Joe!"))) 505 + |> response.set_body(wisp.Text(string_tree.from_string("Hello, Joe!"))) 506 506 |> testing.string_body 507 507 |> should.equal("Hello, Joe!") 508 508 } ··· 523 523 524 524 pub fn bit_array_body_text_test() { 525 525 wisp.ok() 526 - |> response.set_body(wisp.Text(string_builder.from_string("Hello, Joe!"))) 526 + |> response.set_body(wisp.Text(string_tree.from_string("Hello, Joe!"))) 527 527 |> testing.bit_array_body 528 528 |> should.equal(<<"Hello, Joe!":utf8>>) 529 529 }
+54 -23
test/wisp_test.gleam
··· 1 + import exception 1 2 import gleam/bit_array 2 3 import gleam/crypto 3 4 import gleam/dict ··· 10 11 import gleam/list 11 12 import gleam/set 12 13 import gleam/string 13 - import gleam/string_builder 14 + import gleam/string_tree 14 15 import gleeunit 15 16 import gleeunit/should 16 17 import simplifile ··· 118 119 } 119 120 120 121 pub fn json_response_test() { 121 - let body = string_builder.from_string("{\"one\":1,\"two\":2}") 122 + let body = string_tree.from_string("{\"one\":1,\"two\":2}") 122 123 let response = wisp.json_response(body, 201) 123 124 response.status 124 125 |> should.equal(201) 125 126 response.headers 126 - |> should.equal([#("content-type", "application/json")]) 127 + |> should.equal([#("content-type", "application/json; charset=utf-8")]) 127 128 response 128 129 |> testing.string_body 129 130 |> should.equal("{\"one\":1,\"two\":2}") 130 131 } 131 132 132 133 pub fn html_response_test() { 133 - let body = string_builder.from_string("Hello, world!") 134 + let body = string_tree.from_string("Hello, world!") 134 135 let response = wisp.html_response(body, 200) 135 136 response.status 136 137 |> should.equal(200) 137 138 response.headers 138 - |> should.equal([#("content-type", "text/html")]) 139 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 139 140 response 140 141 |> testing.string_body 141 142 |> should.equal("Hello, world!") 142 143 } 143 144 144 145 pub fn html_body_test() { 145 - let body = string_builder.from_string("Hello, world!") 146 + let body = string_tree.from_string("Hello, world!") 146 147 let response = 147 148 wisp.method_not_allowed([http.Get]) 148 149 |> wisp.html_body(body) 149 150 response.status 150 151 |> should.equal(405) 151 152 response.headers 152 - |> should.equal([#("allow", "GET"), #("content-type", "text/html")]) 153 + |> should.equal([ 154 + #("allow", "GET"), 155 + #("content-type", "text/html; charset=utf-8"), 156 + ]) 153 157 response 154 158 |> testing.string_body 155 159 |> should.equal("Hello, world!") ··· 330 334 } 331 335 332 336 pub fn rescue_crashes_error_test() { 333 - // TODO: Determine how to silence the logger for this test. 337 + wisp.set_logger_level(wisp.CriticalLevel) 338 + use <- exception.defer(fn() { wisp.set_logger_level(wisp.InfoLevel) }) 339 + 334 340 { 335 341 use <- wisp.rescue_crashes 336 342 panic as "we need to crash to test the middleware" ··· 359 365 response.status 360 366 |> should.equal(200) 361 367 response.headers 362 - |> should.equal([#("content-type", "text/plain")]) 368 + |> should.equal([#("content-type", "text/plain; charset=utf-8")]) 363 369 response.body 364 370 |> should.equal(wisp.File("./test/fixture.txt")) 365 371 ··· 370 376 response.status 371 377 |> should.equal(200) 372 378 response.headers 373 - |> should.equal([#("content-type", "application/json")]) 379 + |> should.equal([#("content-type", "application/json; charset=utf-8")]) 374 380 response.body 375 381 |> should.equal(wisp.File("./test/fixture.json")) 376 382 383 + // Get some other file 384 + let response = 385 + testing.get("/stuff/test/fixture.dat", []) 386 + |> handler 387 + response.status 388 + |> should.equal(200) 389 + response.headers 390 + |> should.equal([#("content-type", "application/octet-stream")]) 391 + response.body 392 + |> should.equal(wisp.File("./test/fixture.dat")) 393 + 377 394 // Get something not handled by the static file server 378 395 let response = 379 396 testing.get("/stuff/this-does-not-exist", []) ··· 397 414 response.status 398 415 |> should.equal(200) 399 416 response.headers 400 - |> should.equal([#("content-type", "text/plain")]) 417 + |> should.equal([#("content-type", "text/plain; charset=utf-8")]) 401 418 response.body 402 419 |> should.equal(wisp.File("./test/fixture.txt")) 403 420 } ··· 413 430 response.status 414 431 |> should.equal(200) 415 432 response.headers 416 - |> should.equal([#("content-type", "text/plain")]) 433 + |> should.equal([#("content-type", "text/plain; charset=utf-8")]) 417 434 response.body 418 435 |> should.equal(wisp.File("./test/fixture.txt")) 419 436 } ··· 485 502 pub fn require_content_type_test() { 486 503 { 487 504 let request = testing.get("/", [#("content-type", "text/plain")]) 505 + use <- wisp.require_content_type(request, "text/plain") 506 + wisp.ok() 507 + } 508 + |> should.equal(wisp.ok()) 509 + } 510 + 511 + pub fn require_content_type_charset_test() { 512 + { 513 + let request = 514 + testing.get("/", [#("content-type", "text/plain; charset=utf-8")]) 488 515 use <- wisp.require_content_type(request, "text/plain") 489 516 wisp.ok() 490 517 } ··· 724 751 list.key_find(request.headers, "x-original-method") 725 752 |> should.equal(header) 726 753 727 - string_builder.from_string("Hello!") 754 + string_tree.from_string("Hello!") 728 755 |> wisp.html_response(201) 729 756 } 730 757 ··· 733 760 |> handler(Error(Nil)) 734 761 |> should.equal(Response( 735 762 201, 736 - [#("content-type", "text/html")], 737 - wisp.Text(string_builder.from_string("Hello!")), 763 + [#("content-type", "text/html; charset=utf-8")], 764 + wisp.Text(string_tree.from_string("Hello!")), 738 765 )) 739 766 740 767 testing.get("/", []) 741 768 |> request.set_method(http.Head) 742 769 |> handler(Ok("HEAD")) 743 - |> should.equal(Response(201, [#("content-type", "text/html")], wisp.Empty)) 770 + |> should.equal(Response( 771 + 201, 772 + [#("content-type", "text/html; charset=utf-8")], 773 + wisp.Empty, 774 + )) 744 775 745 776 testing.get("/", []) 746 777 |> request.set_method(http.Post) ··· 866 897 |> should.equal(Response( 867 898 200, 868 899 [], 869 - wisp.Text(string_builder.from_string("Hello, world!")), 900 + wisp.Text(string_tree.from_string("Hello, world!")), 870 901 )) 871 902 } 872 903 873 - pub fn string_builder_body_test() { 904 + pub fn string_tree_body_test() { 874 905 wisp.ok() 875 - |> wisp.string_builder_body(string_builder.from_string("Hello, world!")) 906 + |> wisp.string_tree_body(string_tree.from_string("Hello, world!")) 876 907 |> should.equal(Response( 877 908 200, 878 909 [], 879 - wisp.Text(string_builder.from_string("Hello, world!")), 910 + wisp.Text(string_tree.from_string("Hello, world!")), 880 911 )) 881 912 } 882 913 883 914 pub fn json_body_test() { 884 915 wisp.ok() 885 - |> wisp.json_body(string_builder.from_string("{\"one\":1,\"two\":2}")) 916 + |> wisp.json_body(string_tree.from_string("{\"one\":1,\"two\":2}")) 886 917 |> should.equal(Response( 887 918 200, 888 - [#("content-type", "application/json")], 889 - wisp.Text(string_builder.from_string("{\"one\":1,\"two\":2}")), 919 + [#("content-type", "application/json; charset=utf-8")], 920 + wisp.Text(string_tree.from_string("{\"one\":1,\"two\":2}")), 890 921 )) 891 922 } 892 923