+2
-2
.github/workflows/ci.yml
+2
-2
.github/workflows/ci.yml
+31
-1
CHANGELOG.md
+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
+1
-1
examples/00-hello-world/gleam.toml
+3
-3
examples/00-hello-world/src/app/router.gleam
+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
+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
+1
-1
examples/00-hello-world/test/app_test.gleam
+1
-1
examples/01-routing/gleam.toml
+1
-1
examples/01-routing/gleam.toml
+7
-7
examples/01-routing/src/app/router.gleam
+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
+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
+1
-1
examples/01-routing/test/app_test.gleam
+1
-1
examples/02-working-with-form-data/gleam.toml
+1
-1
examples/02-working-with-form-data/gleam.toml
+3
-3
examples/02-working-with-form-data/src/app/router.gleam
+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
+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
+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
+1
-1
examples/03-working-with-json/gleam.toml
+3
-2
examples/03-working-with-json/src/app.gleam
+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
+1
-1
examples/03-working-with-json/test/app_test.gleam
+1
-1
examples/04-working-with-other-formats/gleam.toml
+1
-1
examples/04-working-with-other-formats/gleam.toml
+3
-2
examples/04-working-with-other-formats/src/app.gleam
+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
+1
-1
examples/05-using-a-database/gleam.toml
+2
-2
examples/05-using-a-database/src/app/web/people.gleam
+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
+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
+1
-1
examples/05-using-a-database/test/app_test.gleam
+1
-1
examples/06-serving-static-assets/gleam.toml
+1
-1
examples/06-serving-static-assets/gleam.toml
+3
-3
examples/06-serving-static-assets/src/app/router.gleam
+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
+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
+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
+1
-1
examples/07-logging/gleam.toml
+3
-2
examples/07-logging/src/app.gleam
+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/09-configuring-default-responses/README.md
+1
-1
examples/09-configuring-default-responses/README.md
+1
-1
examples/09-configuring-default-responses/gleam.toml
+1
-1
examples/09-configuring-default-responses/gleam.toml
+2
-2
examples/09-configuring-default-responses/src/app/router.gleam
+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
+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
+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
+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
+1
-1
examples/10-working-with-files/gleam.toml
+5
-5
examples/10-working-with-files/src/app/router.gleam
+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
+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
+1
-1
examples/10-working-with-files/test/app_test.gleam
+1
-1
examples/utilities/tiny_database/gleam.toml
+1
-1
examples/utilities/tiny_database/gleam.toml
+3
-3
examples/utilities/tiny_database/manifest.toml
+3
-3
examples/utilities/tiny_database/manifest.toml
···
8
8
{ name = "gleam_json", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB405BD93A8828BCD870463DE29375E7B2D252D9D124C109E5B618AAC00B86FC" },
9
9
{ name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
10
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 = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
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
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" },
14
14
]
···
17
17
gleam_json = { version = "~> 0.6" }
18
18
gleam_stdlib = { version = "~> 0.30" }
19
19
gleeunit = { version = "~> 1.0" }
20
-
simplifile = { version = "~> 1.0" }
21
-
youid = { version = ">= 1.1.0 and < 2.0.0"}
20
+
simplifile = { version = "~> 2.0" }
21
+
youid = { version = ">= 1.1.0 and < 2.0.0" }
+10
-11
gleam.toml
+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
+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
+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
+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
+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
+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
test/fixture.dat
This is a binary file and will not be displayed.
+3
-3
test/wisp/testing_test.gleam
+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
+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