🧚 A practical web framework for Gleam

Compare changes

Choose any two refs to compare.

Changed files
+431 -341
.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
src
test
+2 -2
.github/workflows/ci.yml
··· 13 - uses: actions/checkout@v4 14 - uses: erlef/setup-beam@v1 15 with: 16 - otp-version: "26.0" 17 - gleam-version: "1.2.0" 18 rebar3-version: "3" 19 # elixir-version: "1.14.2" 20 - run: gleam format --check src test
··· 13 - uses: actions/checkout@v4 14 - uses: erlef/setup-beam@v1 15 with: 16 + otp-version: "27.0" 17 + gleam-version: "1.4.0" 18 rebar3-version: "3" 19 # elixir-version: "1.14.2" 20 - run: gleam format --check src test
+22
CHANGELOG.md
··· 1 # Changelog 2 3 ## v0.16.0 - 2024-07-13 4 5 - HTML and JSON body functions now include `charset=utf-8` in the content-type
··· 1 # Changelog 2 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
+1 -1
examples/00-hello-world/gleam.toml
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 - mist = ">= 1.2.0 and < 2.0.0" 11 12 13 [dev-dependencies]
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 12 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 import app/web 4 5 /// The HTTP request handler- your application! 6 /// ··· 9 use _req <- web.middleware(req) 10 11 // Later we'll use templates, but for now a string will do. 12 - let body = string_builder.from_string("<h1>Hello, Joe!</h1>") 13 14 // Return a 200 OK response with the body and a HTML content type. 15 wisp.html_response(body, 200)
··· 1 import app/web 2 + import gleam/string_tree 3 + import wisp.{type Request, type Response} 4 5 /// The HTTP request handler- your application! 6 /// ··· 9 use _req <- web.middleware(req) 10 11 // Later we'll use templates, but for now a string will do. 12 + let body = string_tree.from_string("<h1>Hello, Joe!</h1>") 13 14 // Return a 200 OK response with the body and a HTML content type. 15 wisp.html_response(body, 200)
+3 -2
examples/00-hello-world/src/app.gleam
··· 1 import gleam/erlang/process 2 import mist 3 import wisp 4 - import app/router 5 6 pub fn main() { 7 // This sets the logger to print INFO level logs, and other sensible defaults ··· 14 15 // Start the Mist web server. 16 let assert Ok(_) = 17 - wisp.mist_handler(router.handle_request, secret_key_base) 18 |> mist.new 19 |> mist.port(8000) 20 |> mist.start_http
··· 1 + import app/router 2 import gleam/erlang/process 3 import mist 4 import wisp 5 + import wisp/wisp_mist 6 7 pub fn main() { 8 // This sets the logger to print INFO level logs, and other sensible defaults ··· 15 16 // Start the Mist web server. 17 let assert Ok(_) = 18 + wisp_mist.handler(router.handle_request, secret_key_base) 19 |> mist.new 20 |> mist.port(8000) 21 |> mist.start_http
+1 -1
examples/01-routing/gleam.toml
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 - mist = ">= 1.2.0 and < 2.0.0" 11 gleam_http = "~> 3.5" 12 13 [dev-dependencies]
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 gleam_http = "~> 3.5" 12 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 import app/web 5 6 pub fn handle_request(req: Request) -> Response { 7 use req <- web.middleware(req) ··· 31 // used to return a 405: Method Not Allowed response for all other methods. 32 use <- wisp.require_method(req, Get) 33 34 - let html = string_builder.from_string("Hello, Joe!") 35 wisp.ok() 36 |> wisp.html_body(html) 37 } ··· 48 49 fn list_comments() -> Response { 50 // In a later example we'll show how to read from a database. 51 - let html = string_builder.from_string("Comments!") 52 wisp.ok() 53 |> wisp.html_body(html) 54 } 55 56 fn create_comment(_req: Request) -> Response { 57 // In a later example we'll show how to parse data from the request body. 58 - let html = string_builder.from_string("Created") 59 wisp.created() 60 |> wisp.html_body(html) 61 } ··· 66 // The `id` path parameter has been passed to this function, so we could use 67 // it to look up a comment in a database. 68 // For now we'll just include in the response body. 69 - let html = string_builder.from_string("Comment with id " <> id) 70 wisp.ok() 71 |> wisp.html_body(html) 72 }
··· 1 import app/web 2 + import gleam/http.{Get, Post} 3 + import gleam/string_tree 4 + import wisp.{type Request, type Response} 5 6 pub fn handle_request(req: Request) -> Response { 7 use req <- web.middleware(req) ··· 31 // used to return a 405: Method Not Allowed response for all other methods. 32 use <- wisp.require_method(req, Get) 33 34 + let html = string_tree.from_string("Hello, Joe!") 35 wisp.ok() 36 |> wisp.html_body(html) 37 } ··· 48 49 fn list_comments() -> Response { 50 // In a later example we'll show how to read from a database. 51 + let html = string_tree.from_string("Comments!") 52 wisp.ok() 53 |> wisp.html_body(html) 54 } 55 56 fn create_comment(_req: Request) -> Response { 57 // In a later example we'll show how to parse data from the request body. 58 + let html = string_tree.from_string("Created") 59 wisp.created() 60 |> wisp.html_body(html) 61 } ··· 66 // The `id` path parameter has been passed to this function, so we could use 67 // it to look up a comment in a database. 68 // For now we'll just include in the response body. 69 + let html = string_tree.from_string("Comment with id " <> id) 70 wisp.ok() 71 |> wisp.html_body(html) 72 }
+3 -2
examples/01-routing/src/app.gleam
··· 1 import gleam/erlang/process 2 import mist 3 import wisp 4 - import app/router 5 6 pub fn main() { 7 wisp.configure_logger() 8 let secret_key_base = wisp.random_string(64) 9 10 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 |> mist.new 13 |> mist.port(8000) 14 |> mist.start_http
··· 1 + import app/router 2 import gleam/erlang/process 3 import mist 4 import wisp 5 + import wisp/wisp_mist 6 7 pub fn main() { 8 wisp.configure_logger() 9 let secret_key_base = wisp.random_string(64) 10 11 let assert Ok(_) = 12 + wisp_mist.handler(router.handle_request, secret_key_base) 13 |> mist.new 14 |> mist.port(8000) 15 |> mist.start_http
+1 -1
examples/02-working-with-form-data/gleam.toml
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 - mist = ">= 1.2.0 and < 2.0.0" 11 gleam_http = "~> 3.5" 12 13 [dev-dependencies]
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 gleam_http = "~> 3.5" 12 13 [dev-dependencies]
+3 -3
examples/02-working-with-form-data/src/app/router.gleam
··· 2 import gleam/http.{Get, Post} 3 import gleam/list 4 import gleam/result 5 - import gleam/string_builder 6 import wisp.{type Request, type Response} 7 8 pub fn handle_request(req: Request) -> Response { ··· 21 // In a larger application a template library or HTML form library might 22 // be used here instead of a string literal. 23 let html = 24 - string_builder.from_string( 25 "<form method='post'> 26 <label>Title: 27 <input type='text' name='title'> ··· 60 case result { 61 Ok(content) -> { 62 wisp.ok() 63 - |> wisp.html_body(string_builder.from_string(content)) 64 } 65 Error(_) -> { 66 wisp.bad_request()
··· 2 import gleam/http.{Get, Post} 3 import gleam/list 4 import gleam/result 5 + import gleam/string_tree 6 import wisp.{type Request, type Response} 7 8 pub fn handle_request(req: Request) -> Response { ··· 21 // In a larger application a template library or HTML form library might 22 // be used here instead of a string literal. 23 let html = 24 + string_tree.from_string( 25 "<form method='post'> 26 <label>Title: 27 <input type='text' name='title'> ··· 60 case result { 61 Ok(content) -> { 62 wisp.ok() 63 + |> wisp.html_body(string_tree.from_string(content)) 64 } 65 Error(_) -> { 66 wisp.bad_request()
+3 -2
examples/02-working-with-form-data/src/app.gleam
··· 1 import gleam/erlang/process 2 import mist 3 import wisp 4 - import app/router 5 6 pub fn main() { 7 wisp.configure_logger() 8 let secret_key_base = wisp.random_string(64) 9 10 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 |> mist.new 13 |> mist.port(8000) 14 |> mist.start_http
··· 1 + import app/router 2 import gleam/erlang/process 3 import mist 4 import wisp 5 + import wisp/wisp_mist 6 7 pub fn main() { 8 wisp.configure_logger() 9 let secret_key_base = wisp.random_string(64) 10 11 let assert Ok(_) = 12 + wisp_mist.handler(router.handle_request, secret_key_base) 13 |> mist.new 14 |> mist.port(8000) 15 |> mist.start_http
+1 -1
examples/03-working-with-json/gleam.toml
··· 8 wisp = { path = "../.." } 9 gleam_json = "~> 0.6" 10 gleam_erlang = "~> 0.23" 11 - mist = ">= 1.2.0 and < 2.0.0" 12 gleam_http = "~> 3.5" 13 14 [dev-dependencies]
··· 8 wisp = { path = "../.." } 9 gleam_json = "~> 0.6" 10 gleam_erlang = "~> 0.23" 11 + mist = ">= 2.0.0 and < 3.0.0" 12 gleam_http = "~> 3.5" 13 14 [dev-dependencies]
+3 -2
examples/03-working-with-json/src/app.gleam
··· 1 import gleam/erlang/process 2 import mist 3 import wisp 4 - import app/router 5 6 pub fn main() { 7 wisp.configure_logger() 8 let secret_key_base = wisp.random_string(64) 9 10 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 |> mist.new 13 |> mist.port(8000) 14 |> mist.start_http
··· 1 + import app/router 2 import gleam/erlang/process 3 import mist 4 import wisp 5 + import wisp/wisp_mist 6 7 pub fn main() { 8 wisp.configure_logger() 9 let secret_key_base = wisp.random_string(64) 10 11 let assert Ok(_) = 12 + wisp_mist.handler(router.handle_request, secret_key_base) 13 |> mist.new 14 |> mist.port(8000) 15 |> mist.start_http
+1 -1
examples/04-working-with-other-formats/gleam.toml
··· 8 wisp = { path = "../.." } 9 gsv = "~> 1.0" 10 gleam_erlang = "~> 0.23" 11 - mist = ">= 1.2.0 and < 2.0.0" 12 gleam_http = "~> 3.5" 13 14 [dev-dependencies]
··· 8 wisp = { path = "../.." } 9 gsv = "~> 1.0" 10 gleam_erlang = "~> 0.23" 11 + mist = ">= 2.0.0 and < 3.0.0" 12 gleam_http = "~> 3.5" 13 14 [dev-dependencies]
+3 -2
examples/04-working-with-other-formats/src/app.gleam
··· 1 import gleam/erlang/process 2 import mist 3 import wisp 4 - import app/router 5 6 pub fn main() { 7 wisp.configure_logger() 8 let secret_key_base = wisp.random_string(64) 9 10 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 |> mist.new 13 |> mist.port(8000) 14 |> mist.start_http
··· 1 + import app/router 2 import gleam/erlang/process 3 import mist 4 import wisp 5 + import wisp/wisp_mist 6 7 pub fn main() { 8 wisp.configure_logger() 9 let secret_key_base = wisp.random_string(64) 10 11 let assert Ok(_) = 12 + wisp_mist.handler(router.handle_request, secret_key_base) 13 |> mist.new 14 |> mist.port(8000) 15 |> mist.start_http
+1 -1
examples/05-using-a-database/gleam.toml
··· 9 gleam_json = "~> 0.6" 10 tiny_database = { path = "../utilities/tiny_database" } 11 gleam_erlang = "~> 0.23" 12 - mist = ">= 1.2.0 and < 2.0.0" 13 gleam_http = "~> 3.5" 14 15 [dev-dependencies]
··· 9 gleam_json = "~> 0.6" 10 tiny_database = { path = "../utilities/tiny_database" } 11 gleam_erlang = "~> 0.23" 12 + mist = ">= 2.0.0 and < 3.0.0" 13 gleam_http = "~> 3.5" 14 15 [dev-dependencies]
+2 -2
examples/05-using-a-database/src/app/web/people.gleam
··· 1 import app/web.{type Context} 2 import gleam/dynamic.{type Dynamic} 3 import gleam/http.{Get, Post} 4 import gleam/json 5 - import gleam/dict 6 import gleam/result.{try} 7 import tiny_database 8 import wisp.{type Request, type Response} ··· 122 // In this example we are not going to be reporting specific errors to the 123 // user, so we can discard the error and replace it with Nil. 124 result 125 - |> result.nil_error 126 } 127 128 /// Save a person to the database and return the id of the newly created record.
··· 1 import app/web.{type Context} 2 + import gleam/dict 3 import gleam/dynamic.{type Dynamic} 4 import gleam/http.{Get, Post} 5 import gleam/json 6 import gleam/result.{try} 7 import tiny_database 8 import wisp.{type Request, type Response} ··· 122 // In this example we are not going to be reporting specific errors to the 123 // user, so we can discard the error and replace it with Nil. 124 result 125 + |> result.replace_error(Nil) 126 } 127 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 gleam/erlang/process 2 - import tiny_database 3 import mist 4 import wisp 5 - import app/router 6 - import app/web 7 8 pub const data_directory = "tmp/data" 9 ··· 24 25 let assert Ok(_) = 26 handler 27 - |> wisp.mist_handler(secret_key_base) 28 |> mist.new 29 |> mist.port(8000) 30 |> mist.start_http
··· 1 + import app/router 2 + import app/web 3 import gleam/erlang/process 4 import mist 5 + import tiny_database 6 import wisp 7 + import wisp/wisp_mist 8 9 pub const data_directory = "tmp/data" 10 ··· 25 26 let assert Ok(_) = 27 handler 28 + |> wisp_mist.handler(secret_key_base) 29 |> mist.new 30 |> mist.port(8000) 31 |> mist.start_http
+1 -1
examples/06-serving-static-assets/gleam.toml
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 - mist = ">= 1.2.0 and < 2.0.0" 11 gleam_http = "~> 3.5" 12 13 [dev-dependencies]
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 gleam_http = "~> 3.5" 12 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 import app/web.{type Context} 4 5 const html = "<!DOCTYPE html> 6 <html lang=\"en\"> ··· 18 19 pub fn handle_request(req: Request, ctx: Context) -> Response { 20 use _req <- web.middleware(req, ctx) 21 - wisp.html_response(string_builder.from_string(html), 200) 22 }
··· 1 import app/web.{type Context} 2 + import gleam/string_tree 3 + import wisp.{type Request, type Response} 4 5 const html = "<!DOCTYPE html> 6 <html lang=\"en\"> ··· 18 19 pub fn handle_request(req: Request, ctx: Context) -> Response { 20 use _req <- web.middleware(req, ctx) 21 + wisp.html_response(string_tree.from_string(html), 200) 22 }
+4 -3
examples/06-serving-static-assets/src/app.gleam
··· 1 import gleam/erlang/process 2 import mist 3 import wisp 4 - import app/router 5 - import app/web.{Context} 6 7 pub fn main() { 8 wisp.configure_logger() ··· 16 let handler = router.handle_request(_, ctx) 17 18 let assert Ok(_) = 19 - wisp.mist_handler(handler, secret_key_base) 20 |> mist.new 21 |> mist.port(8000) 22 |> mist.start_http
··· 1 + import app/router 2 + import app/web.{Context} 3 import gleam/erlang/process 4 import mist 5 import wisp 6 + import wisp/wisp_mist 7 8 pub fn main() { 9 wisp.configure_logger() ··· 17 let handler = router.handle_request(_, ctx) 18 19 let assert Ok(_) = 20 + wisp_mist.handler(handler, secret_key_base) 21 |> mist.new 22 |> mist.port(8000) 23 |> mist.start_http
+5 -5
examples/06-serving-static-assets/test/app_test.gleam
··· 1 import gleeunit 2 import gleeunit/should 3 import wisp/testing 4 - import app 5 - import app/router 6 - import app/web.{type Context, Context} 7 8 pub fn main() { 9 gleeunit.main() ··· 38 |> should.equal(200) 39 40 response.headers 41 - |> should.equal([#("content-type", "text/css")]) 42 } 43 44 pub fn get_javascript_test() { ··· 50 |> should.equal(200) 51 52 response.headers 53 - |> should.equal([#("content-type", "text/javascript")]) 54 }
··· 1 + import app 2 + import app/router 3 + import app/web.{type Context, Context} 4 import gleeunit 5 import gleeunit/should 6 import wisp/testing 7 8 pub fn main() { 9 gleeunit.main() ··· 38 |> should.equal(200) 39 40 response.headers 41 + |> should.equal([#("content-type", "text/css; charset=utf-8")]) 42 } 43 44 pub fn get_javascript_test() { ··· 50 |> should.equal(200) 51 52 response.headers 53 + |> should.equal([#("content-type", "text/javascript; charset=utf-8")]) 54 }
+1 -1
examples/07-logging/gleam.toml
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 - mist = "~> 1.0" 11 12 [dev-dependencies] 13 gleeunit = "~> 1.0"
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 12 [dev-dependencies] 13 gleeunit = "~> 1.0"
+3 -2
examples/07-logging/src/app.gleam
··· 1 import gleam/erlang/process 2 import mist 3 import wisp 4 - import app/router 5 6 pub fn main() { 7 wisp.configure_logger() 8 let secret_key_base = wisp.random_string(64) 9 10 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 |> mist.new 13 |> mist.port(8000) 14 |> mist.start_http
··· 1 + import app/router 2 import gleam/erlang/process 3 import mist 4 import wisp 5 + import wisp/wisp_mist 6 7 pub fn main() { 8 wisp.configure_logger() 9 let secret_key_base = wisp.random_string(64) 10 11 let assert Ok(_) = 12 + wisp_mist.handler(router.handle_request, secret_key_base) 13 |> mist.new 14 |> mist.port(8000) 15 |> mist.start_http
+1 -1
examples/08-working-with-cookies/gleam.toml
··· 8 wisp = { path = "../.." } 9 gleam_crypto = "~> 1.0" 10 gleam_erlang = "~> 0.23" 11 - mist = ">= 1.2.0 and < 2.0.0" 12 gleam_http = "~> 3.5" 13 14 [dev-dependencies]
··· 8 wisp = { path = "../.." } 9 gleam_crypto = "~> 1.0" 10 gleam_erlang = "~> 0.23" 11 + mist = ">= 2.0.0 and < 3.0.0" 12 gleam_http = "~> 3.5" 13 14 [dev-dependencies]
+3 -3
examples/08-working-with-cookies/src/app/router.gleam
··· 1 import app/web 2 import gleam/http.{Delete, Get, Post} 3 import gleam/list 4 - import gleam/string_builder 5 import wisp.{type Request, type Response} 6 7 const cookie_name = "id" ··· 25 " <button type='submit'>Log out</button>", 26 "</form>", 27 ] 28 - |> string_builder.from_strings 29 |> wisp.html_response(200) 30 } 31 Error(_) -> { ··· 52 <button type='submit'>Log in</button> 53 </form> 54 " 55 - |> string_builder.from_string 56 |> wisp.html_response(200) 57 } 58
··· 1 import app/web 2 import gleam/http.{Delete, Get, Post} 3 import gleam/list 4 + import gleam/string_tree 5 import wisp.{type Request, type Response} 6 7 const cookie_name = "id" ··· 25 " <button type='submit'>Log out</button>", 26 "</form>", 27 ] 28 + |> string_tree.from_strings 29 |> wisp.html_response(200) 30 } 31 Error(_) -> { ··· 52 <button type='submit'>Log in</button> 53 </form> 54 " 55 + |> string_tree.from_string 56 |> wisp.html_response(200) 57 } 58
+3 -2
examples/08-working-with-cookies/src/app.gleam
··· 1 import gleam/erlang/process 2 import mist 3 import wisp 4 - import app/router 5 6 pub fn main() { 7 wisp.configure_logger() 8 let secret_key_base = wisp.random_string(64) 9 10 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 |> mist.new 13 |> mist.port(8000) 14 |> mist.start_http
··· 1 + import app/router 2 import gleam/erlang/process 3 import mist 4 import wisp 5 + import wisp/wisp_mist 6 7 pub fn main() { 8 wisp.configure_logger() 9 let secret_key_base = wisp.random_string(64) 10 11 let assert Ok(_) = 12 + wisp_mist.handler(router.handle_request, secret_key_base) 13 |> mist.new 14 |> mist.port(8000) 15 |> mist.start_http
+1 -1
examples/09-configuring-default-responses/README.md
··· 1 - # Wisp Example: Working with form data 2 3 ```sh 4 gleam run # Run the server
··· 1 + # Wisp Example: Configuring default responses 2 3 ```sh 4 gleam run # Run the server
+1 -1
examples/09-configuring-default-responses/gleam.toml
··· 6 [dependencies] 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 - mist = "~> 1.0" 10 gleam_erlang = "~> 0.23" 11 12 [dev-dependencies]
··· 6 [dependencies] 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 + mist = ">= 2.0.0 and < 3.0.0" 10 gleam_erlang = "~> 0.23" 11 12 [dev-dependencies]
+2 -2
examples/09-configuring-default-responses/src/app/router.gleam
··· 1 import app/web 2 - import gleam/string_builder 3 import wisp.{type Request, type Response} 4 5 pub fn handle_request(req: Request) -> Response { ··· 9 // This request returns a non-empty body. 10 [] -> { 11 "<h1>Hello, Joe!</h1>" 12 - |> string_builder.from_string 13 |> wisp.html_response(200) 14 } 15
··· 1 import app/web 2 + import gleam/string_tree 3 import wisp.{type Request, type Response} 4 5 pub fn handle_request(req: Request) -> Response { ··· 9 // This request returns a non-empty body. 10 [] -> { 11 "<h1>Hello, Joe!</h1>" 12 + |> string_tree.from_string 13 |> wisp.html_response(200) 14 } 15
+6 -6
examples/09-configuring-default-responses/src/app/web.gleam
··· 1 - import wisp 2 import gleam/bool 3 - import gleam/string_builder 4 5 pub fn middleware( 6 req: wisp.Request, ··· 32 case response.status { 33 404 | 405 -> 34 "<h1>There's nothing here</h1>" 35 - |> string_builder.from_string 36 |> wisp.html_body(response, _) 37 38 400 | 422 -> 39 "<h1>Bad request</h1>" 40 - |> string_builder.from_string 41 |> wisp.html_body(response, _) 42 43 413 -> 44 "<h1>Request entity too large</h1>" 45 - |> string_builder.from_string 46 |> wisp.html_body(response, _) 47 48 500 -> 49 "<h1>Internal server error</h1>" 50 - |> string_builder.from_string 51 |> wisp.html_body(response, _) 52 53 // For other status codes redirect to the home page
··· 1 import gleam/bool 2 + import gleam/string_tree 3 + import wisp 4 5 pub fn middleware( 6 req: wisp.Request, ··· 32 case response.status { 33 404 | 405 -> 34 "<h1>There's nothing here</h1>" 35 + |> string_tree.from_string 36 |> wisp.html_body(response, _) 37 38 400 | 422 -> 39 "<h1>Bad request</h1>" 40 + |> string_tree.from_string 41 |> wisp.html_body(response, _) 42 43 413 -> 44 "<h1>Request entity too large</h1>" 45 + |> string_tree.from_string 46 |> wisp.html_body(response, _) 47 48 500 -> 49 "<h1>Internal server error</h1>" 50 + |> string_tree.from_string 51 |> wisp.html_body(response, _) 52 53 // For other status codes redirect to the home page
+3 -2
examples/09-configuring-default-responses/src/app.gleam
··· 1 import gleam/erlang/process 2 import mist 3 import wisp 4 - import app/router 5 6 pub fn main() { 7 wisp.configure_logger() 8 let secret_key_base = wisp.random_string(64) 9 10 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 |> mist.new 13 |> mist.port(8000) 14 |> mist.start_http
··· 1 + import app/router 2 import gleam/erlang/process 3 import mist 4 import wisp 5 + import wisp/wisp_mist 6 7 pub fn main() { 8 wisp.configure_logger() 9 let secret_key_base = wisp.random_string(64) 10 11 let assert Ok(_) = 12 + wisp_mist.handler(router.handle_request, secret_key_base) 13 |> mist.new 14 |> mist.port(8000) 15 |> mist.start_http
+1 -1
examples/10-working-with-files/gleam.toml
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 - mist = ">= 1.2.0 and < 2.0.0" 11 gleam_http = "~> 3.5" 12 13 [dev-dependencies]
··· 7 gleam_stdlib = "~> 0.30" 8 wisp = { path = "../.." } 9 gleam_erlang = "~> 0.23" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 gleam_http = "~> 3.5" 12 13 [dev-dependencies]
+5 -5
examples/10-working-with-files/src/app/router.gleam
··· 1 import app/web 2 import gleam/http.{Get, Post} 3 import gleam/list 4 import gleam/result 5 - import gleam/string_builder 6 - import gleam/bytes_builder 7 import wisp.{type Request, type Response} 8 9 pub fn handle_request(req: Request) -> Response { ··· 35 fn show_home(req: Request) -> Response { 36 use <- wisp.require_method(req, Get) 37 html 38 - |> string_builder.from_string 39 |> wisp.html_response(200) 40 } 41 ··· 45 // In this case we have the file contents in memory as a string. 46 // This is good if we have just made the file, but if the file already exists 47 // on the disc then the approach in the next function is more efficient. 48 - let file_contents = bytes_builder.from_string("Hello, Joe!") 49 50 wisp.ok() 51 |> wisp.set_header("content-type", "text/plain") ··· 107 case result { 108 Ok(name) -> { 109 { "<p>Thank you for your file!" <> name <> "</p>" <> html } 110 - |> string_builder.from_string 111 |> wisp.html_response(200) 112 } 113 Error(_) -> {
··· 1 import app/web 2 + import gleam/bytes_tree 3 import gleam/http.{Get, Post} 4 import gleam/list 5 import gleam/result 6 + import gleam/string_tree 7 import wisp.{type Request, type Response} 8 9 pub fn handle_request(req: Request) -> Response { ··· 35 fn show_home(req: Request) -> Response { 36 use <- wisp.require_method(req, Get) 37 html 38 + |> string_tree.from_string 39 |> wisp.html_response(200) 40 } 41 ··· 45 // In this case we have the file contents in memory as a string. 46 // This is good if we have just made the file, but if the file already exists 47 // on the disc then the approach in the next function is more efficient. 48 + let file_contents = bytes_tree.from_string("Hello, Joe!") 49 50 wisp.ok() 51 |> wisp.set_header("content-type", "text/plain") ··· 107 case result { 108 Ok(name) -> { 109 { "<p>Thank you for your file!" <> name <> "</p>" <> html } 110 + |> string_tree.from_string 111 |> wisp.html_response(200) 112 } 113 Error(_) -> {
+3 -2
examples/10-working-with-files/src/app.gleam
··· 1 import gleam/erlang/process 2 import mist 3 import wisp 4 - import app/router 5 6 pub fn main() { 7 wisp.configure_logger() 8 let secret_key_base = wisp.random_string(64) 9 10 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 |> mist.new 13 |> mist.port(8000) 14 |> mist.start_http
··· 1 + import app/router 2 import gleam/erlang/process 3 import mist 4 import wisp 5 + import wisp/wisp_mist 6 7 pub fn main() { 8 wisp.configure_logger() 9 let secret_key_base = wisp.random_string(64) 10 11 let assert Ok(_) = 12 + wisp_mist.handler(router.handle_request, secret_key_base) 13 |> mist.new 14 |> mist.port(8000) 15 |> mist.start_http
+9 -10
gleam.toml
··· 1 name = "wisp" 2 - version = "0.16.0" 3 - gleam = ">= 0.32.0" 4 description = "A practical web framework for Gleam" 5 licences = ["Apache-2.0"] 6 7 repository = { type = "github", user = "gleam-wisp", repo = "wisp" } 8 - links = [ 9 - { title = "Sponsor", href = "https://github.com/sponsors/lpil" }, 10 - ] 11 12 [dependencies] 13 exception = ">= 2.0.0 and < 3.0.0" 14 gleam_crypto = ">= 1.0.0 and < 2.0.0" 15 gleam_erlang = ">= 0.21.0 and < 2.0.0" 16 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 = ">= 2.0.0 and < 3.0.0" 21 marceau = ">= 1.1.0 and < 2.0.0" 22 - logging = ">= 1.0.0 and < 2.0.0" 23 24 [dev-dependencies] 25 - gleeunit = "~> 1.0"
··· 1 name = "wisp" 2 + version = "1.3.0" 3 + gleam = ">= 1.4.0" 4 description = "A practical web framework for Gleam" 5 licences = ["Apache-2.0"] 6 7 repository = { type = "github", user = "gleam-wisp", repo = "wisp" } 8 + links = [{ title = "Sponsor", href = "https://github.com/sponsors/lpil" }] 9 10 [dependencies] 11 exception = ">= 2.0.0 and < 3.0.0" 12 gleam_crypto = ">= 1.0.0 and < 2.0.0" 13 gleam_erlang = ">= 0.21.0 and < 2.0.0" 14 gleam_http = ">= 3.5.0 and < 4.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" 19 marceau = ">= 1.1.0 and < 2.0.0" 20 + logging = ">= 1.2.0 and < 2.0.0" 21 + directories = ">= 1.0.0 and < 2.0.0" 22 23 [dev-dependencies] 24 + gleeunit = ">= 1.0.0 and < 2.0.0"
+22 -18
manifest.toml
··· 3 4 packages = [ 5 { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, 6 { 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.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 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" }, 16 { 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 { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 18 - { name = "logging", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "FCB111401BDB4703A440A94FF8CC7DA521112269C065F219C2766998333E7738" }, 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" }, 21 { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, 22 - { name = "simplifile", version = "2.0.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "95219227A43FCFE62C6E494F413A1D56FF953B68FE420698612E3D89A1EFE029" }, 23 - { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, 24 ] 25 26 [requirements] 27 exception = { version = ">= 2.0.0 and < 3.0.0" } 28 gleam_crypto = { version = ">= 1.0.0 and < 2.0.0" } 29 gleam_erlang = { version = ">= 0.21.0 and < 2.0.0" } 30 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 marceau = { version = ">= 1.1.0 and < 2.0.0" } 36 - mist = { version = ">= 1.2.0 and < 2.0.0" } 37 simplifile = { version = ">= 2.0.0 and < 3.0.0" }
··· 3 4 packages = [ 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" }, 8 { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, 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" }, 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" }, 19 { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 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" }, 24 { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, 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" }, 27 ] 28 29 [requirements] 30 + directories = { version = ">= 1.0.0 and < 2.0.0" } 31 exception = { version = ">= 2.0.0 and < 3.0.0" } 32 gleam_crypto = { version = ">= 1.0.0 and < 2.0.0" } 33 gleam_erlang = { version = ">= 0.21.0 and < 2.0.0" } 34 gleam_http = { version = ">= 3.5.0 and < 4.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" } 39 marceau = { version = ">= 1.1.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 import gleam/bit_array 2 - import gleam/bytes_builder 3 import gleam/crypto 4 import gleam/http 5 import gleam/http/request 6 import gleam/json.{type Json} 7 import gleam/option.{None, Some} 8 import gleam/string 9 - import gleam/string_builder 10 import gleam/uri 11 import simplifile 12 import wisp.{type Request, type Response, Bytes, Empty, File, Text} ··· 227 pub fn string_body(response: Response) -> String { 228 case response.body { 229 Empty -> "" 230 - Text(builder) -> string_builder.to_string(builder) 231 Bytes(bytes) -> { 232 - let data = bytes_builder.to_bit_array(bytes) 233 let assert Ok(string) = bit_array.to_string(data) 234 string 235 } ··· 250 pub fn bit_array_body(response: Response) -> BitArray { 251 case response.body { 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)) 256 File(path) -> { 257 let assert Ok(contents) = simplifile.read_bits(path) 258 contents
··· 1 import gleam/bit_array 2 + import gleam/bytes_tree 3 import gleam/crypto 4 import gleam/http 5 import gleam/http/request 6 import gleam/json.{type Json} 7 import gleam/option.{None, Some} 8 import gleam/string 9 + import gleam/string_tree 10 import gleam/uri 11 import simplifile 12 import wisp.{type Request, type Response, Bytes, Empty, File, Text} ··· 227 pub fn string_body(response: Response) -> String { 228 case response.body { 229 Empty -> "" 230 + Text(tree) -> string_tree.to_string(tree) 231 Bytes(bytes) -> { 232 + let data = bytes_tree.to_bit_array(bytes) 233 let assert Ok(string) = bit_array.to_string(data) 234 string 235 } ··· 250 pub fn bit_array_body(response: Response) -> BitArray { 251 case response.body { 252 Empty -> <<>> 253 + Bytes(tree) -> bytes_tree.to_bit_array(tree) 254 + Text(tree) -> bytes_tree.to_bit_array(bytes_tree.from_string_tree(tree)) 255 File(path) -> { 256 let assert Ok(contents) = simplifile.read_bits(path) 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 + }
+78 -212
src/wisp.gleam
··· 1 import exception 2 import gleam/bit_array 3 import gleam/bool 4 - import gleam/bytes_builder.{type BytesBuilder} 5 import gleam/crypto 6 import gleam/dict.{type Dict} 7 import gleam/dynamic.{type Dynamic} ··· 19 import gleam/option.{type Option} 20 import gleam/result 21 import gleam/string 22 - import gleam/string_builder.{type StringBuilder} 23 import gleam/uri 24 import logging 25 import marceau 26 - import mist 27 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 - } 113 114 // 115 // Responses ··· 120 pub type Body { 121 /// A body of unicode text. 122 /// 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. 125 /// 126 - Text(StringBuilder) 127 /// A body of binary data. 128 /// 129 - /// The body is represented using a `BytesBuilder`. If you have a `BitArray` 130 - /// you can use the `bytes_builder.from_bit_array` function to convert it. 131 /// 132 - Bytes(BytesBuilder) 133 /// A body of the contents of a file. 134 /// 135 /// This will be sent efficiently using the `send_file` function of the ··· 231 /// # Examples 232 /// 233 /// ```gleam 234 /// response(200) 235 - /// |> file_download_from_memory(named: "myfile.txt", containing: "Hello, Joe!") 236 /// // -> Response( 237 /// // 200, 238 /// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")], ··· 243 pub fn file_download_from_memory( 244 response: Response, 245 named name: String, 246 - containing data: BytesBuilder, 247 ) -> Response { 248 let name = uri.percent_encode(name) 249 response ··· 262 /// # Examples 263 /// 264 /// ```gleam 265 - /// let body = string_builder.from_string("<h1>Hello, Joe!</h1>") 266 /// html_response(body, 200) 267 /// // -> Response(200, [#("content-type", "text/html")], Text(body)) 268 /// ``` 269 /// 270 - pub fn html_response(html: StringBuilder, status: Int) -> Response { 271 HttpResponse( 272 status, 273 [#("content-type", "text/html; charset=utf-8")], ··· 283 /// # Examples 284 /// 285 /// ```gleam 286 - /// let body = string_builder.from_string("{\"name\": \"Joe\"}") 287 /// json_response(body, 200) 288 /// // -> Response(200, [#("content-type", "application/json")], Text(body)) 289 /// ``` 290 /// 291 - pub fn json_response(json: StringBuilder, status: Int) -> Response { 292 HttpResponse( 293 status, 294 [#("content-type", "application/json; charset=utf-8")], ··· 304 /// # Examples 305 /// 306 /// ```gleam 307 - /// let body = string_builder.from_string("<h1>Hello, Joe!</h1>") 308 /// response(201) 309 /// |> html_body(body) 310 /// // -> Response(201, [#("content-type", "text/html; charset=utf-8")], Text(body)) 311 /// ``` 312 /// 313 - pub fn html_body(response: Response, html: StringBuilder) -> Response { 314 response 315 |> response.set_body(Text(html)) 316 |> response.set_header("content-type", "text/html; charset=utf-8") ··· 324 /// # Examples 325 /// 326 /// ```gleam 327 - /// let body = string_builder.from_string("{\"name\": \"Joe\"}") 328 /// response(201) 329 /// |> json_body(body) 330 /// // -> Response(201, [#("content-type", "application/json; charset=utf-8")], Text(body)) 331 /// ``` 332 /// 333 - pub fn json_body(response: Response, json: StringBuilder) -> Response { 334 response 335 |> response.set_body(Text(json)) 336 |> response.set_header("content-type", "application/json; charset=utf-8") 337 } 338 339 - /// Set the body of a response to a given string builder. 340 /// 341 /// You likely want to also set the request `content-type` header to an 342 /// appropriate value for the format of the content. ··· 344 /// # Examples 345 /// 346 /// ```gleam 347 - /// let body = string_builder.from_string("Hello, Joe!") 348 /// response(201) 349 - /// |> string_builder_body(body) 350 /// // -> Response(201, [], Text(body)) 351 /// ``` 352 /// 353 - pub fn string_builder_body( 354 - response: Response, 355 - content: StringBuilder, 356 - ) -> Response { 357 response 358 |> response.set_body(Text(content)) 359 } 360 361 - /// Set the body of a response to a given string builder. 362 /// 363 /// You likely want to also set the request `content-type` header to an 364 /// appropriate value for the format of the content. ··· 372 /// // -> Response( 373 /// // 201, 374 /// // [], 375 - /// // Text(string_builder.from_string("Hello, Joe")) 376 /// // ) 377 /// ``` 378 /// 379 pub fn string_body(response: Response, content: String) -> Response { 380 response 381 - |> response.set_body(Text(string_builder.from_string(content))) 382 } 383 384 /// Escape a string so that it can be safely included in a HTML document. ··· 718 /// The body of the request can be read from this connection using functions 719 /// such as `require_multipart_body`. 720 /// 721 - pub opaque type Connection { 722 - Connection( 723 - reader: Reader, 724 - max_body_size: Int, 725 - max_files_size: Int, 726 - read_chunk_size: Int, 727 - secret_key_base: String, 728 - temporary_directory: String, 729 - ) 730 - } 731 - 732 - fn make_connection(body_reader: Reader, secret_key_base: String) -> Connection { 733 - // TODO: replace `/tmp` with appropriate for the OS 734 - let prefix = "/tmp/gleam-wisp/" 735 - let temporary_directory = join_path(prefix, random_slug()) 736 - Connection( 737 - reader: body_reader, 738 - max_body_size: 8_000_000, 739 - max_files_size: 32_000_000, 740 - read_chunk_size: 1_000_000, 741 - temporary_directory: temporary_directory, 742 - secret_key_base: secret_key_base, 743 - ) 744 - } 745 746 type BufferedReader { 747 - BufferedReader(reader: Reader, buffer: BitArray) 748 } 749 750 type Quotas { ··· 766 } 767 } 768 769 - fn buffered_read(reader: BufferedReader, chunk_size: Int) -> Result(Read, Nil) { 770 case reader.buffer { 771 <<>> -> reader.reader(chunk_size) 772 - _ -> Ok(Chunk(reader.buffer, reader.reader)) 773 } 774 } 775 776 - type Reader = 777 - fn(Int) -> Result(Read, Nil) 778 - 779 - type Read { 780 - Chunk(BitArray, next: Reader) 781 - ReadingFinished 782 - } 783 - 784 /// Set the maximum permitted size of a request body of the request in bytes. 785 /// 786 /// If a body is larger than this size attempting to read the body will result ··· 792 /// instead use the `max_files_size` limit. 793 /// 794 pub fn set_max_body_size(request: Request, size: Int) -> Request { 795 - Connection(..request.body, max_body_size: size) 796 |> request.set_body(request, _) 797 } 798 ··· 816 case string.byte_size(key) < 64 { 817 True -> panic as "Secret key base must be at least 64 bytes long" 818 False -> 819 - Connection(..request.body, secret_key_base: key) 820 |> request.set_body(request, _) 821 } 822 } ··· 835 /// 836 /// This limit only applies for files in a multipart body that get streamed to 837 /// disc. For headers and other content that gets read into memory use the 838 - /// `max_files_size` limit. 839 /// 840 pub fn set_max_files_size(request: Request, size: Int) -> Request { 841 - Connection(..request.body, max_files_size: size) 842 |> request.set_body(request, _) 843 } 844 ··· 858 /// been received from the client. 859 /// 860 pub fn set_read_chunk_size(request: Request, size: Int) -> Request { 861 - Connection(..request.body, read_chunk_size: size) 862 |> request.set_body(request, _) 863 } 864 ··· 872 /// A convenient alias for a HTTP request with a Wisp connection as the body. 873 /// 874 pub type Request = 875 - HttpRequest(Connection) 876 877 /// This middleware function ensures that the request has a specific HTTP 878 /// method, returning an empty response with status code 405: Method not allowed ··· 1070 } 1071 1072 fn read_body_loop( 1073 - reader: Reader, 1074 read_chunk_size: Int, 1075 max_body_size: Int, 1076 accumulator: BitArray, 1077 ) -> Result(BitArray, Nil) { 1078 use chunk <- result.try(reader(read_chunk_size)) 1079 case chunk { 1080 - ReadingFinished -> Ok(accumulator) 1081 - Chunk(chunk, next) -> { 1082 let accumulator = bit_array.append(accumulator, chunk) 1083 case bit_array.byte_size(accumulator) > max_body_size { 1084 True -> Error(Nil) ··· 1384 fn read_chunk( 1385 reader: BufferedReader, 1386 chunk_size: Int, 1387 - ) -> Result(#(BitArray, Reader), Response) { 1388 buffered_read(reader, chunk_size) 1389 |> result.replace_error(bad_request()) 1390 |> result.try(fn(chunk) { 1391 case chunk { 1392 - Chunk(chunk, next) -> Ok(#(chunk, next)) 1393 - ReadingFinished -> Error(bad_request()) 1394 } 1395 }) 1396 } ··· 1534 response 1535 } 1536 1537 - fn remove_preceeding_slashes(string: String) -> String { 1538 - case string { 1539 - "/" <> rest -> remove_preceeding_slashes(rest) 1540 - _ -> string 1541 - } 1542 - } 1543 - 1544 - // TODO: replace with simplifile function when it exists 1545 - fn join_path(a: String, b: String) -> String { 1546 - let b = remove_preceeding_slashes(b) 1547 - case string.ends_with(a, "/") { 1548 - True -> a <> b 1549 - False -> a <> "/" <> b 1550 - } 1551 - } 1552 - 1553 /// A middleware function that serves files from a directory, along with a 1554 /// suitable `content-type` header for known file extensions. 1555 /// ··· 1597 from directory: String, 1598 next handler: fn() -> Response, 1599 ) -> Response { 1600 - let path = remove_preceeding_slashes(req.path) 1601 - let prefix = remove_preceeding_slashes(prefix) 1602 case req.method, string.starts_with(path, prefix) { 1603 http.Get, True -> { 1604 let path = 1605 path 1606 - |> string.drop_left(string.length(prefix)) 1607 |> string.replace(each: "..", with: "") 1608 - |> join_path(directory, _) 1609 1610 let mime_type = 1611 req.path ··· 1669 1670 /// Create a new temporary directory for the given request. 1671 /// 1672 - /// If you are using the `mist_handler` function or another compliant web server 1673 /// adapter then this file will be deleted for you when the request is complete. 1674 /// Otherwise you will need to call the `delete_temporary_files` function 1675 /// yourself. ··· 1679 ) -> Result(String, simplifile.FileError) { 1680 let directory = request.body.temporary_directory 1681 use _ <- result.try(simplifile.create_directory_all(directory)) 1682 - let path = join_path(directory, random_slug()) 1683 use _ <- result.map(simplifile.create_file(path)) 1684 path 1685 } 1686 1687 /// Delete any temporary files created for the given request. 1688 /// 1689 - /// If you are using the `mist_handler` function or another compliant web server 1690 /// adapter then this file will be deleted for you when the request is complete. 1691 /// Otherwise you will need to call this function yourself. 1692 /// ··· 1736 /// [1]: https://www.erlang.org/doc/man/logger 1737 /// 1738 pub type LogLevel { 1739 - Emergency 1740 - Alert 1741 - Critical 1742 - Error 1743 - Warning 1744 - Notice 1745 - Info 1746 - Debug 1747 } 1748 1749 - fn log_level_to_logger_log_level(log_level: LogLevel) -> logging.LogLevel { 1750 case log_level { 1751 - Emergency -> logging.Emergency 1752 - Alert -> logging.Alert 1753 - Critical -> logging.Critical 1754 - Error -> logging.Error 1755 - Warning -> logging.Warning 1756 - Notice -> logging.Notice 1757 - Info -> logging.Info 1758 - Debug -> logging.Debug 1759 } 1760 } 1761 ··· 1766 /// [1]: https://www.erlang.org/doc/man/logger 1767 /// 1768 pub fn set_logger_level(log_level: LogLevel) -> Nil { 1769 - logging.set_level(log_level_to_logger_log_level(log_level)) 1770 } 1771 1772 /// Log a message to the Erlang logger with the level of `emergency`. ··· 1856 /// Generate a random string of the given length. 1857 /// 1858 pub fn random_string(length: Int) -> String { 1859 - crypto.strong_random_bytes(length) 1860 - |> bit_array.base64_url_encode(False) 1861 - |> string.slice(0, length) 1862 } 1863 1864 /// Sign a message which can later be verified using the `verify_signed_message` ··· 1893 crypto.verify_signed_message(message, <<request.body.secret_key_base:utf8>>) 1894 } 1895 1896 - fn random_slug() -> String { 1897 - random_string(16) 1898 - } 1899 - 1900 // 1901 // Cookies 1902 // ··· 2002 pub fn create_canned_connection( 2003 body: BitArray, 2004 secret_key_base: String, 2005 - ) -> Connection { 2006 - make_connection( 2007 - fn(_size) { Ok(Chunk(body, fn(_size) { Ok(ReadingFinished) })) }, 2008 secret_key_base, 2009 ) 2010 }
··· 1 import exception 2 import gleam/bit_array 3 import gleam/bool 4 + import gleam/bytes_tree.{type BytesTree} 5 import gleam/crypto 6 import gleam/dict.{type Dict} 7 import gleam/dynamic.{type Dynamic} ··· 19 import gleam/option.{type Option} 20 import gleam/result 21 import gleam/string 22 + import gleam/string_tree.{type StringTree} 23 import gleam/uri 24 import logging 25 import marceau 26 import simplifile 27 + import wisp/internal 28 29 // 30 // Responses ··· 35 pub type Body { 36 /// A body of unicode text. 37 /// 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. 40 /// 41 + Text(StringTree) 42 /// A body of binary data. 43 /// 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. 46 /// 47 + Bytes(BytesTree) 48 /// A body of the contents of a file. 49 /// 50 /// This will be sent efficiently using the `send_file` function of the ··· 146 /// # Examples 147 /// 148 /// ```gleam 149 + /// let content = bytes_tree.from_string("Hello, Joe!") 150 /// response(200) 151 + /// |> file_download_from_memory(named: "myfile.txt", containing: content) 152 /// // -> Response( 153 /// // 200, 154 /// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")], ··· 159 pub fn file_download_from_memory( 160 response: Response, 161 named name: String, 162 + containing data: BytesTree, 163 ) -> Response { 164 let name = uri.percent_encode(name) 165 response ··· 178 /// # Examples 179 /// 180 /// ```gleam 181 + /// let body = string_tree.from_string("<h1>Hello, Joe!</h1>") 182 /// html_response(body, 200) 183 /// // -> Response(200, [#("content-type", "text/html")], Text(body)) 184 /// ``` 185 /// 186 + pub fn html_response(html: StringTree, status: Int) -> Response { 187 HttpResponse( 188 status, 189 [#("content-type", "text/html; charset=utf-8")], ··· 199 /// # Examples 200 /// 201 /// ```gleam 202 + /// let body = string_tree.from_string("{\"name\": \"Joe\"}") 203 /// json_response(body, 200) 204 /// // -> Response(200, [#("content-type", "application/json")], Text(body)) 205 /// ``` 206 /// 207 + pub fn json_response(json: StringTree, status: Int) -> Response { 208 HttpResponse( 209 status, 210 [#("content-type", "application/json; charset=utf-8")], ··· 220 /// # Examples 221 /// 222 /// ```gleam 223 + /// let body = string_tree.from_string("<h1>Hello, Joe!</h1>") 224 /// response(201) 225 /// |> html_body(body) 226 /// // -> Response(201, [#("content-type", "text/html; charset=utf-8")], Text(body)) 227 /// ``` 228 /// 229 + pub fn html_body(response: Response, html: StringTree) -> Response { 230 response 231 |> response.set_body(Text(html)) 232 |> response.set_header("content-type", "text/html; charset=utf-8") ··· 240 /// # Examples 241 /// 242 /// ```gleam 243 + /// let body = string_tree.from_string("{\"name\": \"Joe\"}") 244 /// response(201) 245 /// |> json_body(body) 246 /// // -> Response(201, [#("content-type", "application/json; charset=utf-8")], Text(body)) 247 /// ``` 248 /// 249 + pub fn json_body(response: Response, json: StringTree) -> Response { 250 response 251 |> response.set_body(Text(json)) 252 |> response.set_header("content-type", "application/json; charset=utf-8") 253 } 254 255 + /// Set the body of a response to a given string tree. 256 /// 257 /// You likely want to also set the request `content-type` header to an 258 /// appropriate value for the format of the content. ··· 260 /// # Examples 261 /// 262 /// ```gleam 263 + /// let body = string_tree.from_string("Hello, Joe!") 264 /// response(201) 265 + /// |> string_tree_body(body) 266 /// // -> Response(201, [], Text(body)) 267 /// ``` 268 /// 269 + pub fn string_tree_body(response: Response, content: StringTree) -> Response { 270 response 271 |> response.set_body(Text(content)) 272 } 273 274 + /// Set the body of a response to a given string. 275 /// 276 /// You likely want to also set the request `content-type` header to an 277 /// appropriate value for the format of the content. ··· 285 /// // -> Response( 286 /// // 201, 287 /// // [], 288 + /// // Text(string_tree.from_string("Hello, Joe")) 289 /// // ) 290 /// ``` 291 /// 292 pub fn string_body(response: Response, content: String) -> Response { 293 response 294 + |> response.set_body(Text(string_tree.from_string(content))) 295 } 296 297 /// Escape a string so that it can be safely included in a HTML document. ··· 631 /// The body of the request can be read from this connection using functions 632 /// such as `require_multipart_body`. 633 /// 634 + pub type Connection = 635 + internal.Connection 636 637 type BufferedReader { 638 + BufferedReader(reader: internal.Reader, buffer: BitArray) 639 } 640 641 type Quotas { ··· 657 } 658 } 659 660 + fn buffered_read( 661 + reader: BufferedReader, 662 + chunk_size: Int, 663 + ) -> Result(internal.Read, Nil) { 664 case reader.buffer { 665 <<>> -> reader.reader(chunk_size) 666 + _ -> Ok(internal.Chunk(reader.buffer, reader.reader)) 667 } 668 } 669 670 /// Set the maximum permitted size of a request body of the request in bytes. 671 /// 672 /// If a body is larger than this size attempting to read the body will result ··· 678 /// instead use the `max_files_size` limit. 679 /// 680 pub fn set_max_body_size(request: Request, size: Int) -> Request { 681 + internal.Connection(..request.body, max_body_size: size) 682 |> request.set_body(request, _) 683 } 684 ··· 702 case string.byte_size(key) < 64 { 703 True -> panic as "Secret key base must be at least 64 bytes long" 704 False -> 705 + internal.Connection(..request.body, secret_key_base: key) 706 |> request.set_body(request, _) 707 } 708 } ··· 721 /// 722 /// This limit only applies for files in a multipart body that get streamed to 723 /// disc. For headers and other content that gets read into memory use the 724 + /// `max_body_size` limit. 725 /// 726 pub fn set_max_files_size(request: Request, size: Int) -> Request { 727 + internal.Connection(..request.body, max_files_size: size) 728 |> request.set_body(request, _) 729 } 730 ··· 744 /// been received from the client. 745 /// 746 pub fn set_read_chunk_size(request: Request, size: Int) -> Request { 747 + internal.Connection(..request.body, read_chunk_size: size) 748 |> request.set_body(request, _) 749 } 750 ··· 758 /// A convenient alias for a HTTP request with a Wisp connection as the body. 759 /// 760 pub type Request = 761 + HttpRequest(internal.Connection) 762 763 /// This middleware function ensures that the request has a specific HTTP 764 /// method, returning an empty response with status code 405: Method not allowed ··· 956 } 957 958 fn read_body_loop( 959 + reader: internal.Reader, 960 read_chunk_size: Int, 961 max_body_size: Int, 962 accumulator: BitArray, 963 ) -> Result(BitArray, Nil) { 964 use chunk <- result.try(reader(read_chunk_size)) 965 case chunk { 966 + internal.ReadingFinished -> Ok(accumulator) 967 + internal.Chunk(chunk, next) -> { 968 let accumulator = bit_array.append(accumulator, chunk) 969 case bit_array.byte_size(accumulator) > max_body_size { 970 True -> Error(Nil) ··· 1270 fn read_chunk( 1271 reader: BufferedReader, 1272 chunk_size: Int, 1273 + ) -> Result(#(BitArray, internal.Reader), Response) { 1274 buffered_read(reader, chunk_size) 1275 |> result.replace_error(bad_request()) 1276 |> result.try(fn(chunk) { 1277 case chunk { 1278 + internal.Chunk(chunk, next) -> Ok(#(chunk, next)) 1279 + internal.ReadingFinished -> Error(bad_request()) 1280 } 1281 }) 1282 } ··· 1420 response 1421 } 1422 1423 /// A middleware function that serves files from a directory, along with a 1424 /// suitable `content-type` header for known file extensions. 1425 /// ··· 1467 from directory: String, 1468 next handler: fn() -> Response, 1469 ) -> Response { 1470 + let path = internal.remove_preceeding_slashes(req.path) 1471 + let prefix = internal.remove_preceeding_slashes(prefix) 1472 case req.method, string.starts_with(path, prefix) { 1473 http.Get, True -> { 1474 let path = 1475 path 1476 + |> string.drop_start(string.length(prefix)) 1477 |> string.replace(each: "..", with: "") 1478 + |> internal.join_path(directory, _) 1479 1480 let mime_type = 1481 req.path ··· 1539 1540 /// Create a new temporary directory for the given request. 1541 /// 1542 + /// If you are using the Mist adapter or another compliant web server 1543 /// adapter then this file will be deleted for you when the request is complete. 1544 /// Otherwise you will need to call the `delete_temporary_files` function 1545 /// yourself. ··· 1549 ) -> Result(String, simplifile.FileError) { 1550 let directory = request.body.temporary_directory 1551 use _ <- result.try(simplifile.create_directory_all(directory)) 1552 + let path = internal.join_path(directory, internal.random_slug()) 1553 use _ <- result.map(simplifile.create_file(path)) 1554 path 1555 } 1556 1557 /// Delete any temporary files created for the given request. 1558 /// 1559 + /// If you are using the Mist adapter or another compliant web server 1560 /// adapter then this file will be deleted for you when the request is complete. 1561 /// Otherwise you will need to call this function yourself. 1562 /// ··· 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 ··· 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 1642 /// Log a message to the Erlang logger with the level of `emergency`. ··· 1726 /// Generate a random string of the given length. 1727 /// 1728 pub fn random_string(length: Int) -> String { 1729 + internal.random_string(length) 1730 } 1731 1732 /// Sign a message which can later be verified using the `verify_signed_message` ··· 1761 crypto.verify_signed_message(message, <<request.body.secret_key_base:utf8>>) 1762 } 1763 1764 // 1765 // Cookies 1766 // ··· 1866 pub fn create_canned_connection( 1867 body: BitArray, 1868 secret_key_base: String, 1869 + ) -> internal.Connection { 1870 + internal.make_connection( 1871 + fn(_size) { 1872 + Ok(internal.Chunk(body, fn(_size) { Ok(internal.ReadingFinished) })) 1873 + }, 1874 secret_key_base, 1875 ) 1876 }
+3 -3
test/wisp/testing_test.gleam
··· 2 import gleam/http/response 3 import gleam/json 4 import gleam/option.{None, Some} 5 - import gleam/string_builder 6 import gleeunit/should 7 import wisp 8 import wisp/testing ··· 502 503 pub fn string_body_text_test() { 504 wisp.ok() 505 - |> response.set_body(wisp.Text(string_builder.from_string("Hello, Joe!"))) 506 |> testing.string_body 507 |> should.equal("Hello, Joe!") 508 } ··· 523 524 pub fn bit_array_body_text_test() { 525 wisp.ok() 526 - |> response.set_body(wisp.Text(string_builder.from_string("Hello, Joe!"))) 527 |> testing.bit_array_body 528 |> should.equal(<<"Hello, Joe!":utf8>>) 529 }
··· 2 import gleam/http/response 3 import gleam/json 4 import gleam/option.{None, Some} 5 + import gleam/string_tree 6 import gleeunit/should 7 import wisp 8 import wisp/testing ··· 502 503 pub fn string_body_text_test() { 504 wisp.ok() 505 + |> response.set_body(wisp.Text(string_tree.from_string("Hello, Joe!"))) 506 |> testing.string_body 507 |> should.equal("Hello, Joe!") 508 } ··· 523 524 pub fn bit_array_body_text_test() { 525 wisp.ok() 526 + |> response.set_body(wisp.Text(string_tree.from_string("Hello, Joe!"))) 527 |> testing.bit_array_body 528 |> should.equal(<<"Hello, Joe!":utf8>>) 529 }
+16 -13
test/wisp_test.gleam
··· 1 import gleam/bit_array 2 import gleam/crypto 3 import gleam/dict ··· 10 import gleam/list 11 import gleam/set 12 import gleam/string 13 - import gleam/string_builder 14 import gleeunit 15 import gleeunit/should 16 import simplifile ··· 118 } 119 120 pub fn json_response_test() { 121 - let body = string_builder.from_string("{\"one\":1,\"two\":2}") 122 let response = wisp.json_response(body, 201) 123 response.status 124 |> should.equal(201) ··· 130 } 131 132 pub fn html_response_test() { 133 - let body = string_builder.from_string("Hello, world!") 134 let response = wisp.html_response(body, 200) 135 response.status 136 |> should.equal(200) ··· 142 } 143 144 pub fn html_body_test() { 145 - let body = string_builder.from_string("Hello, world!") 146 let response = 147 wisp.method_not_allowed([http.Get]) 148 |> wisp.html_body(body) ··· 333 } 334 335 pub fn rescue_crashes_error_test() { 336 - // TODO: Determine how to silence the logger for this test. 337 { 338 use <- wisp.rescue_crashes 339 panic as "we need to crash to test the middleware" ··· 748 list.key_find(request.headers, "x-original-method") 749 |> should.equal(header) 750 751 - string_builder.from_string("Hello!") 752 |> wisp.html_response(201) 753 } 754 ··· 758 |> should.equal(Response( 759 201, 760 [#("content-type", "text/html; charset=utf-8")], 761 - wisp.Text(string_builder.from_string("Hello!")), 762 )) 763 764 testing.get("/", []) ··· 894 |> should.equal(Response( 895 200, 896 [], 897 - wisp.Text(string_builder.from_string("Hello, world!")), 898 )) 899 } 900 901 - pub fn string_builder_body_test() { 902 wisp.ok() 903 - |> wisp.string_builder_body(string_builder.from_string("Hello, world!")) 904 |> should.equal(Response( 905 200, 906 [], 907 - wisp.Text(string_builder.from_string("Hello, world!")), 908 )) 909 } 910 911 pub fn json_body_test() { 912 wisp.ok() 913 - |> wisp.json_body(string_builder.from_string("{\"one\":1,\"two\":2}")) 914 |> should.equal(Response( 915 200, 916 [#("content-type", "application/json; charset=utf-8")], 917 - wisp.Text(string_builder.from_string("{\"one\":1,\"two\":2}")), 918 )) 919 } 920
··· 1 + import exception 2 import gleam/bit_array 3 import gleam/crypto 4 import gleam/dict ··· 11 import gleam/list 12 import gleam/set 13 import gleam/string 14 + import gleam/string_tree 15 import gleeunit 16 import gleeunit/should 17 import simplifile ··· 119 } 120 121 pub fn json_response_test() { 122 + let body = string_tree.from_string("{\"one\":1,\"two\":2}") 123 let response = wisp.json_response(body, 201) 124 response.status 125 |> should.equal(201) ··· 131 } 132 133 pub fn html_response_test() { 134 + let body = string_tree.from_string("Hello, world!") 135 let response = wisp.html_response(body, 200) 136 response.status 137 |> should.equal(200) ··· 143 } 144 145 pub fn html_body_test() { 146 + let body = string_tree.from_string("Hello, world!") 147 let response = 148 wisp.method_not_allowed([http.Get]) 149 |> wisp.html_body(body) ··· 334 } 335 336 pub fn rescue_crashes_error_test() { 337 + wisp.set_logger_level(wisp.CriticalLevel) 338 + use <- exception.defer(fn() { wisp.set_logger_level(wisp.InfoLevel) }) 339 + 340 { 341 use <- wisp.rescue_crashes 342 panic as "we need to crash to test the middleware" ··· 751 list.key_find(request.headers, "x-original-method") 752 |> should.equal(header) 753 754 + string_tree.from_string("Hello!") 755 |> wisp.html_response(201) 756 } 757 ··· 761 |> should.equal(Response( 762 201, 763 [#("content-type", "text/html; charset=utf-8")], 764 + wisp.Text(string_tree.from_string("Hello!")), 765 )) 766 767 testing.get("/", []) ··· 897 |> should.equal(Response( 898 200, 899 [], 900 + wisp.Text(string_tree.from_string("Hello, world!")), 901 )) 902 } 903 904 + pub fn string_tree_body_test() { 905 wisp.ok() 906 + |> wisp.string_tree_body(string_tree.from_string("Hello, world!")) 907 |> should.equal(Response( 908 200, 909 [], 910 + wisp.Text(string_tree.from_string("Hello, world!")), 911 )) 912 } 913 914 pub fn json_body_test() { 915 wisp.ok() 916 + |> wisp.json_body(string_tree.from_string("{\"one\":1,\"two\":2}")) 917 |> should.equal(Response( 918 200, 919 [#("content-type", "application/json; charset=utf-8")], 920 + wisp.Text(string_tree.from_string("{\"one\":1,\"two\":2}")), 921 )) 922 } 923