+28
-20
.github/workflows/ci.yml
+28
-20
.github/workflows/ci.yml
···
13
13
- uses: actions/checkout@v4
14
14
- uses: erlef/setup-beam@v1
15
15
with:
16
-
otp-version: "26.0"
17
-
gleam-version: "0.34.0-rc2"
16
+
otp-version: "27.0"
17
+
gleam-version: "1.4.0"
18
18
rebar3-version: "3"
19
19
# elixir-version: "1.14.2"
20
20
- run: gleam format --check src test
21
21
- run: gleam deps download
22
22
- run: gleam test
23
23
24
-
- name: "Example: 0-hello-world"
24
+
- name: "Example: 00-hello-world"
25
25
run: gleam test
26
-
working-directory: examples/0-hello-world
26
+
working-directory: examples/00-hello-world
27
27
28
-
- name: "Example: 1-routing"
28
+
- name: "Example: 01-routing"
29
29
run: gleam test
30
-
working-directory: examples/1-routing
30
+
working-directory: examples/01-routing
31
31
32
-
- name: "Example: 2-working-with-form-data"
32
+
- name: "Example: 02-working-with-form-data"
33
33
run: gleam test
34
-
working-directory: examples/2-working-with-form-data
34
+
working-directory: examples/02-working-with-form-data
35
35
36
-
- name: "Example: 3-working-with-json"
36
+
- name: "Example: 03-working-with-json"
37
37
run: gleam test
38
-
working-directory: examples/3-working-with-json
38
+
working-directory: examples/03-working-with-json
39
39
40
-
- name: "Example: 4-working-with-other-formats"
40
+
- name: "Example: 04-working-with-other-formats"
41
41
run: gleam test
42
-
working-directory: examples/4-working-with-other-formats
42
+
working-directory: examples/04-working-with-other-formats
43
43
44
-
- name: "Example: 5-using-a-database"
44
+
- name: "Example: 05-using-a-database"
45
45
run: gleam test
46
-
working-directory: examples/5-using-a-database
46
+
working-directory: examples/05-using-a-database
47
47
48
-
- name: "Example: 6-serving-static-assets"
48
+
- name: "Example: 06-serving-static-assets"
49
49
run: gleam test
50
-
working-directory: examples/6-serving-static-assets
50
+
working-directory: examples/06-serving-static-assets
51
51
52
-
- name: "Example: 7-logging"
52
+
- name: "Example: 07-logging"
53
53
run: gleam test
54
-
working-directory: examples/7-logging
54
+
working-directory: examples/07-logging
55
55
56
-
- name: "Example: 8-working-with-cookies"
56
+
- name: "Example: 08-working-with-cookies"
57
57
run: gleam test
58
-
working-directory: examples/8-working-with-cookies
58
+
working-directory: examples/08-working-with-cookies
59
+
60
+
- name: "Example: 09-configuring-default-responses"
61
+
run: gleam test
62
+
working-directory: examples/09-configuring-default-responses
63
+
64
+
- name: "Example: 10-working-with-files"
65
+
run: gleam test
66
+
working-directory: examples/10-working-with-files
+40
-1
CHANGELOG.md
+40
-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
33
+
34
+
- The `mist` version constraint has been increased to >= 1.2.0.
35
+
- The `simplifile` version constraint has been increased to >= 2.0.0.
36
+
- The `escape_html` function in the `wisp` module has been optimised.
37
+
38
+
## v0.14.0 - 2024-03-28
39
+
40
+
- The `mist` version constraint has been relaxed to permit 0.x or 1.x versions.
41
+
42
+
## v0.13.0 - 2024-03-23
4
43
5
44
- The `wisp` module gains the `file_download_from_memory` and `file_download`
6
45
functions.
+5
-5
README.md
+5
-5
README.md
···
17
17
connection or user session.
18
18
19
19
```gleam
20
-
import wisp.{Request, Response}
20
+
import wisp.{type Request, type Response}
21
21
22
22
pub type Context {
23
23
Context(secret: String)
···
41
41
such as images and CSS.
42
42
43
43
```gleam
44
-
import wisp.{Request, Response}
44
+
import wisp.{type Request, type Response}
45
45
46
46
pub fn handle_request(request: Request) -> Response {
47
-
use <- wisp.log_request
47
+
use <- wisp.log_request(request)
48
48
use <- wisp.serve_static(request, under: "/static", from: "/public")
49
49
wisp.ok()
50
50
}
···
55
55
The Wisp examples are a good place to start. They cover various scenarios and
56
56
include comments and tests.
57
57
58
-
- [Hello, World!](https://github.com/lpil/wisp/tree/main/examples/0-hello-world)
58
+
- [Hello, World!](https://github.com/lpil/wisp/tree/main/examples/00-hello-world)
59
59
- [Routing](https://github.com/lpil/wisp/tree/main/examples/01-routing)
60
60
- [Working with form data](https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data)
61
61
- [Working with JSON](https://github.com/lpil/wisp/tree/main/examples/03-working-with-json)
···
65
65
- [Logging](https://github.com/lpil/wisp/tree/main/examples/07-logging)
66
66
- [Working with cookies](https://github.com/lpil/wisp/tree/main/examples/08-working-with-cookies)
67
67
- [Configuring default responses](https://github.com/lpil/wisp/tree/main/examples/09-configuring-default-responses)
68
-
- [Working with files](https://github.com/lpil/wisp/tree/main/examples/09-working-with-files)
68
+
- [Working with files](https://github.com/lpil/wisp/tree/main/examples/10-working-with-files)
69
69
70
70
API documentation is available on [HexDocs](https://hexdocs.pm/wisp/).
71
71
docs/images/cover.png
docs/images/cover.png
This is a binary file and will not be displayed.
+87
-1
docs/images/wordmark.svg
+87
-1
docs/images/wordmark.svg
···
1
-
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="196" fill="none" viewBox="0 0 360 196"><g fill="#124514" filter="url(#a)"><path d="M32.576 87.92c-1.792-.427-3.456-1.237-4.992-2.432-1.536-1.195-2.73-2.347-3.584-3.456l3.84-6.4c4.01-6.485 7.68-10.837 11.008-13.056 3.328-2.219 6.87-3.328 10.624-3.328 3.328 0 6.23.853 8.704 2.56 2.475 1.707 4.352 4.01 5.632 6.912 1.365 2.901 2.048 6.144 2.048 9.728v15.616c0 6.229.768 10.837 2.304 13.824 1.621 2.901 4.096 4.352 7.424 4.352 4.267 0 7.424-2.304 9.472-6.912 2.048-4.608 3.072-10.88 3.072-18.816v-7.68c.768-.256 1.621-.47 2.56-.64a18.652 18.652 0 0 1 3.072-.256c2.133 0 4.053.299 5.76.896v7.68c0 8.96.853 15.488 2.56 19.584 1.792 4.096 4.48 6.144 8.064 6.144 2.901 0 5.291-1.28 7.168-3.84 1.877-2.645 3.285-6.229 4.224-10.752.939-4.608 1.408-9.899 1.408-15.872V80.24c-4.181-1.536-7.253-3.584-9.216-6.144.341-2.56 1.067-4.95 2.176-7.168 1.195-2.304 2.645-4.139 4.352-5.504 1.707-1.45 3.413-2.176 5.12-2.176 2.731 0 4.949 2.048 6.656 6.144 1.707 4.096 2.56 9.387 2.56 15.872 0 8.277-.939 15.573-2.816 21.888-1.877 6.315-4.693 11.264-8.448 14.848-3.669 3.499-8.192 5.248-13.568 5.248-3.499 0-6.656-.896-9.472-2.688-2.816-1.792-5.077-4.352-6.784-7.68-2.048 3.243-4.693 5.803-7.936 7.68-3.243 1.792-6.87 2.688-10.88 2.688-6.485 0-11.52-2.432-15.104-7.296-3.584-4.864-5.376-11.691-5.376-20.48V80.624c0-2.901-.47-5.333-1.408-7.296-.853-2.048-2.304-3.072-4.352-3.072-1.024 0-2.39.597-4.096 1.792-1.707 1.11-3.883 3.712-6.528 7.808l-5.248 8.064Zm140.039 35.328c-5.205 0-9.6-1.749-13.184-5.248-3.584-3.584-5.376-9.429-5.376-17.536V76.016c-2.645 1.45-5.675 2.603-9.088 3.456-3.328.853-6.869 1.28-10.624 1.28-4.181 0-7.808-.555-10.88-1.664-2.304-.768-3.456-2.261-3.456-4.48 0-1.621.469-2.987 1.408-4.096.853-1.11 1.877-1.664 3.072-1.664.341 0 .64.043.896.128.256 0 .512.043.768.128 1.451.341 2.901.64 4.352.896 1.451.17 2.944.256 4.48.256 4.352 0 8.491-.981 12.416-2.944 4.011-2.048 6.784-4.523 8.32-7.424 1.792-.17 3.627.043 5.504.64 1.877.597 3.371 1.621 4.48 3.072v35.456c0 4.949.64 8.405 1.92 10.368 1.365 1.877 3.328 2.816 5.888 2.816 1.195 0 2.091.512 2.688 1.536.683 1.024 1.024 2.389 1.024 4.096a6.373 6.373 0 0 1-1.152 3.712c-.683 1.109-1.835 1.664-3.456 1.664Zm-12.032-83.84c-2.389 0-4.437-.853-6.144-2.56-1.707-1.707-2.56-3.755-2.56-6.144 0-2.475.811-4.523 2.432-6.144 1.707-1.707 3.797-2.56 6.272-2.56s4.523.853 6.144 2.56c1.707 1.621 2.56 3.67 2.56 6.144 0 2.39-.853 4.437-2.56 6.144-1.621 1.707-3.669 2.56-6.144 2.56Zm12.083 83.84c-1.109 0-2.005-.512-2.688-1.536-.683-1.024-1.024-2.347-1.024-3.968 0-3.669 1.536-5.504 4.608-5.504 2.731 0 5.76-1.664 9.088-4.992 3.328-3.413 7.211-8.875 11.648-16.384l13.696-23.168a27.14 27.14 0 0 1-.256-3.84c0-4.267.768-7.424 2.304-9.472 1.621-2.048 2.944-3.285 3.968-3.712 1.451-.341 2.987-.299 4.608.128 1.707.341 3.243.939 4.608 1.792 1.365.853 2.304 1.877 2.816 3.072-.341 1.536-.981 3.37-1.92 5.504-.939 2.133-2.432 4.992-4.48 8.576.597.939 1.365 1.877 2.304 2.816.939.939 2.091 1.963 3.456 3.072l9.728 7.936c3.499 2.816 6.101 5.76 7.808 8.832 1.792 2.987 2.688 6.357 2.688 10.112 0 4.523-1.28 8.533-3.84 12.032.427.939.64 2.048.64 3.328 0 1.621-.427 2.901-1.28 3.84-.768 1.024-1.877 1.536-3.328 1.536-1.365 0-2.688-.085-3.968-.256-1.28-.171-2.688-.384-4.224-.64-2.304.597-4.736.896-7.296.896-2.987 0-5.973-.469-8.96-1.408-2.987-.853-6.101-2.261-9.344-4.224-.171-4.523 1.536-7.851 5.12-9.984 4.693 3.072 9.131 4.608 13.312 4.608 3.584 0 6.443-.939 8.576-2.816 2.133-1.963 3.2-4.437 3.2-7.424 0-3.755-2.048-7.253-6.144-10.496l-9.728-7.68a44.327 44.327 0 0 1-4.608-4.224l-12.8 21.632c-4.693 7.936-9.216 13.611-13.568 17.024-4.267 3.328-9.173 4.992-14.72 4.992Z"/><path d="M235.592 121.67c.683 1.024 1.621 1.536 2.816 1.536 8.533 0 16.128-2.944 22.784-8.832 6.741-5.888 12.715-14.848 17.92-26.88v81.024a19.31 19.31 0 0 0 5.888.896c.939 0 1.92-.085 2.944-.256a23.004 23.004 0 0 0 2.816-.64v-72.96l-1.408-25.088v-8.704c-1.621-.853-3.456-1.45-5.504-1.792-1.963-.341-3.669-.299-5.12.128-4.693 12.544-9.173 22.656-13.44 30.336-4.181 7.595-8.405 13.141-12.672 16.64-4.181 3.499-8.704 5.248-13.568 5.248-1.365 0-2.475.469-3.328 1.408-.853.939-1.28 2.261-1.28 3.968 0 1.621.384 2.944 1.152 3.968Z"/><path d="M307.559 123.456c-5.401 0-10.247-1.451-14.536-4.352-4.21-2.992-7.546-7.072-10.009-12.24-2.383-5.168-3.574-11.016-3.574-17.544 0-6.528 1.231-12.33 3.694-17.408 2.462-5.077 5.798-9.067 10.008-11.968 4.289-2.992 9.095-4.488 14.417-4.488 5.322 0 10.088 1.496 14.298 4.488 4.21 2.901 7.506 6.89 9.889 11.968 2.463 5.077 3.694 10.88 3.694 17.408 0 6.528-1.231 12.376-3.694 17.544-2.383 5.168-5.679 9.248-9.889 12.24-4.13 2.901-8.896 4.352-14.298 4.352Zm-.119-11.696c5.084 0 9.135-2.04 12.153-6.12 3.098-4.08 4.647-9.52 4.647-16.32 0-6.619-1.549-11.968-4.647-16.048-3.018-4.08-7.03-6.12-12.034-6.12-5.004 0-9.095 2.085-12.272 6.256-3.098 4.08-4.647 9.475-4.647 16.184 0 6.619 1.509 11.968 4.528 16.048 3.098 4.08 7.188 6.12 12.272 6.12Z"/></g><defs><filter id="a" width="359.44" height="195.456" x="0" y="0" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="2"/><feGaussianBlur stdDeviation="12"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_125_61"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_125_61" result="shape"/></filter></defs></svg>
1
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+
<svg
3
+
width="355"
4
+
height="196"
5
+
fill="none"
6
+
viewBox="0 0 355 196"
7
+
version="1.1"
8
+
id="svg5"
9
+
sodipodi:docname="wordmark.svg"
10
+
inkscape:version="1.3 (0e150ed, 2023-07-21)"
11
+
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
+
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
13
+
xmlns="http://www.w3.org/2000/svg"
14
+
xmlns:svg="http://www.w3.org/2000/svg">
15
+
<sodipodi:namedview
16
+
id="namedview5"
17
+
pagecolor="#ffffff"
18
+
bordercolor="#666666"
19
+
borderopacity="1.0"
20
+
inkscape:showpageshadow="2"
21
+
inkscape:pageopacity="0.0"
22
+
inkscape:pagecheckerboard="0"
23
+
inkscape:deskcolor="#d1d1d1"
24
+
inkscape:zoom="2.4081633"
25
+
inkscape:cx="250.19068"
26
+
inkscape:cy="117.72458"
27
+
inkscape:window-width="2560"
28
+
inkscape:window-height="1440"
29
+
inkscape:window-x="0"
30
+
inkscape:window-y="244"
31
+
inkscape:window-maximized="0"
32
+
inkscape:current-layer="g3" />
33
+
<g
34
+
fill="#124514"
35
+
filter="url(#a)"
36
+
id="g3">
37
+
<path
38
+
id="path1"
39
+
d="m 160.58203,22 c -2.475,0 -4.56448,0.853549 -6.27148,2.560547 -1.621,1.620998 -2.43164,3.66758 -2.43164,6.142578 0,2.388998 0.85354,4.437533 2.56054,6.144531 1.707,1.706999 3.75358,2.560547 6.14258,2.560547 2.475,0 4.52353,-0.853548 6.14453,-2.560547 1.707,-1.706998 2.56055,-3.754533 2.56055,-6.144531 0,-2.473998 -0.85355,-4.52158 -2.56055,-6.142578 C 165.10556,22.853549 163.05703,22 160.58203,22 Z M 216.25,50.447266 c -0.768,-0.02125 -1.51474,0.05411 -2.24023,0.224609 -1.024,0.427 -2.3458,1.664893 -3.9668,3.712891 -1.536,2.047998 -2.30469,5.203707 -2.30469,9.470703 -0.006,1.284443 0.0798,2.567516 0.25586,3.839843 l -13.69531,23.167969 c -4.437,7.508993 -8.32044,12.971769 -11.64844,16.384769 -3.328,3.32799 -6.35689,4.99218 -9.08789,4.99218 -0.0104,0 -0.0189,0.002 -0.0293,0.002 -0.008,-4e-5 -0.0139,-0.002 -0.0215,-0.002 -2.56,0 -4.52367,-0.9394 -5.88867,-2.8164 -1.28,-1.963 -1.91993,-5.41819 -1.91993,-10.367189 V 63.599609 c -1.10899,-1.450998 -2.60347,-2.475266 -4.48046,-3.072265 -1.877,-0.597 -3.71191,-0.808672 -5.50391,-0.638672 -1.536,2.900997 -4.30932,5.37583 -8.32031,7.423828 -3.925,1.962998 -8.06402,2.943359 -12.41602,2.943359 -0.50848,0 -1.00481,-0.02419 -1.5039,-0.04297 -0.38921,-1.737079 -0.84664,-3.379089 -1.44727,-4.820313 -1.707,-4.095996 -3.9233,-6.144531 -6.6543,-6.144531 -1.707,0 -3.41409,0.725783 -5.12109,2.175781 -1.707,1.364999 -3.15656,3.199909 -4.35156,5.503906 -1.109,2.217998 -1.83478,4.607972 -2.17578,7.167969 1.96299,2.559998 5.03384,4.608533 9.21484,6.144531 v 1.535157 c 0,5.972994 -0.4692,11.265051 -1.4082,15.873047 -0.939,4.522994 -2.34566,8.106954 -4.22266,10.751954 -1.877,2.56 -4.26697,3.83984 -7.16797,3.83984 -3.584,0 -6.27245,-2.04853 -8.06445,-6.14453 -1.707,-4.09599 -2.560549,-10.623992 -2.560549,-19.583983 v -7.679688 c -1.706998,-0.596999 -3.626768,-0.896484 -5.759765,-0.896484 -1.029309,4.23e-4 -2.057085,0.08591 -3.072266,0.255859 -0.938999,0.17 -1.790595,0.384626 -2.558594,0.640625 v 7.679688 c 0,7.935992 -1.024267,14.208413 -3.072265,18.816403 -2.047998,4.608 -5.205661,6.91211 -9.472657,6.91211 -3.327996,0 -5.802829,-1.45056 -7.423828,-4.35156 -1.535998,-2.987 -2.304687,-7.59522 -2.304687,-13.824219 V 78.447266 c 0,-3.583997 -0.681877,-6.825566 -2.046875,-9.726563 -1.279999,-2.901997 -3.157815,-5.205111 -5.632813,-6.912109 -2.473997,-1.706999 -5.375128,-2.560547 -8.703125,-2.560547 -3.753996,0 -7.297003,1.109127 -10.625,3.328125 -3.327996,2.218998 -6.997816,6.571647 -11.007812,13.05664 L 24,82.03125 c 0.853999,1.108999 2.047986,2.262032 3.583984,3.457031 1.535999,1.194999 3.20019,2.004641 4.992188,2.431641 l 5.248047,-8.064453 c 2.644997,-4.095996 4.820345,-6.696642 6.527343,-7.806641 1.705999,-1.194999 3.071705,-1.792969 4.095704,-1.792969 2.047998,0 3.500516,1.024268 4.353515,3.072266 0.937999,1.962998 1.40625,4.393925 1.40625,7.294922 v 14.849609 c 0,8.788994 1.792957,15.614524 5.376953,20.478514 3.583997,4.864 8.618522,7.29688 15.103516,7.29688 4.009996,0 7.637863,-0.8955 10.880859,-2.6875 3.242997,-1.877 5.887549,-4.43669 7.935547,-7.67969 1.706999,3.328 3.967206,5.88769 6.783204,7.67969 2.816,1.792 5.97366,2.6875 9.47266,2.6875 5.37599,0 9.89936,-1.74905 13.56835,-5.24805 3.755,-3.584 6.57027,-8.53266 8.44727,-14.84766 1.877,-6.31499 2.81641,-13.611676 2.81641,-21.888668 0,-0.180894 -0.0163,-0.338543 -0.0176,-0.517578 3.66835,-0.01736 7.13366,-0.438145 10.39258,-1.273438 3.41299,-0.852999 6.44289,-2.007032 9.08789,-3.457031 v 24.449215 c 0,8.107 1.79295,13.95116 5.37695,17.53516 3.584,3.499 7.9786,5.24805 13.18359,5.24805 0.0105,0 0.0188,-0.002 0.0293,-0.002 0.007,5e-5 0.014,0.002 0.0215,0.002 5.54699,0 10.4537,-1.66419 14.7207,-4.99219 4.35199,-3.413 8.87341,-9.08745 13.5664,-17.02344 L 213.7539,79.599609 c 1.43467,1.514684 2.97393,2.926805 4.60742,4.22461 l 9.72851,7.679687 c 4.096,3.242997 6.14454,6.741098 6.14454,10.496094 -1e-5,2.987 -1.06818,5.46083 -3.20118,7.42383 -2.13299,1.877 -4.99217,2.8164 -8.57617,2.8164 -4.18099,0 -8.61755,-1.53542 -13.31055,-4.60742 -3.58399,2.133 -5.29209,5.45943 -5.12109,9.98242 3.243,1.963 6.35675,3.37161 9.34375,4.22461 2.987,0.939 5.97394,1.40821 8.96094,1.40821 7.65209,-0.14681 17.74497,-1.05963 26.86133,-8.875 6.36426,-5.50545 14.71492,-14.846933 19.91992,-26.878909 v 81.023439 c 1.90471,0.60159 3.89123,0.90402 5.88867,0.89648 0.939,0 1.91936,-0.0849 2.94336,-0.25586 0.9513,-0.15433 1.89195,-0.3683 2.81641,-0.64062 v -51.2207 c 0.72696,0.63647 1.47542,1.24644 2.26367,1.80664 4.28898,2.90098 9.13417,4.35156 14.53515,4.35156 5.402,0 10.16885,-1.45058 14.29883,-4.35156 4.21,-2.992 7.50569,-7.07224 9.88867,-12.24024 2.463,-5.16798 3.69336,-11.01498 3.69336,-17.542968 0,-6.527986 -1.23036,-12.331213 -3.69336,-17.408203 -2.38298,-5.07799 -5.67867,-9.067756 -9.88867,-11.96875 -4.20998,-2.991994 -8.97683,-4.488281 -14.29883,-4.488281 -5.32198,0 -10.12703,1.496287 -14.41601,4.488281 -1.35803,0.935779 -2.61488,1.997819 -3.79102,3.160157 v -1.337891 c -1.621,-0.852998 -3.45592,-1.449016 -5.5039,-1.791016 -1.963,-0.340998 -3.66816,-0.300045 -5.11914,0.126953 -3.44724,10.882961 -9.17441,22.655954 -13.44141,30.335938 -3.95909,7.321306 -7.04512,10.70817 -9.76536,12.95663 0.008,-0.25554 0.1052,-0.41588 0.1052,-0.88241 0,-3.754997 -0.89745,-7.124332 -2.68945,-10.111329 -1.707,-3.071997 -4.30764,-6.016034 -7.80664,-8.832032 l -9.72852,-7.935547 c -1.36499,-1.108998 -2.51803,-2.133266 -3.45703,-3.072265 -0.939,-0.938999 -1.70573,-1.877407 -2.30273,-2.816406 2.048,-3.583997 3.53951,-6.443174 4.47851,-8.576172 0.939,-2.133998 1.58088,-3.967908 1.92188,-5.503907 -0.512,-1.194998 -1.45141,-2.219266 -2.81641,-3.072265 -1.365,-0.852999 -2.90237,-1.450016 -4.60937,-1.791016 -0.8105,-0.2135 -1.59919,-0.332265 -2.36719,-0.353515 z m 81.30859,16.705078 c 5.004,0 9.01716,2.039148 12.03516,6.11914 3.098,4.079992 4.64648,9.429842 4.64648,16.048828 0,6.799988 -1.54848,12.240328 -4.64648,16.320308 -3.018,4.08 -7.0703,6.11915 -12.1543,6.11915 -5.08398,0 -9.17348,-2.03915 -12.27148,-6.11915 -3.019,-4.07998 -4.52734,-9.429837 -4.52735,-16.048823 10e-6,-6.708986 1.54849,-12.103602 4.64649,-16.183594 3.177,-4.170992 7.2675,-6.255859 12.27148,-6.255859 z"
40
+
sodipodi:nodetypes="ssssscscssccccccscscscsccscssccccsccscsccccssscssccssccscccscssssccsscsscccscscscccccscsccccccccccccsccsccscccccccscccscccscsscscscscs" />
41
+
</g>
42
+
<defs
43
+
id="defs5">
44
+
<filter
45
+
id="a"
46
+
width="359.44"
47
+
height="195.45599"
48
+
x="0"
49
+
y="0"
50
+
color-interpolation-filters="sRGB"
51
+
filterUnits="userSpaceOnUse">
52
+
<feFlood
53
+
flood-opacity="0"
54
+
result="BackgroundImageFix"
55
+
id="feFlood3" />
56
+
<feColorMatrix
57
+
in="SourceAlpha"
58
+
result="hardAlpha"
59
+
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
60
+
id="feColorMatrix3" />
61
+
<feOffset
62
+
dy="2"
63
+
id="feOffset3" />
64
+
<feGaussianBlur
65
+
stdDeviation="12"
66
+
id="feGaussianBlur3" />
67
+
<feComposite
68
+
in2="hardAlpha"
69
+
operator="out"
70
+
id="feComposite3" />
71
+
<feColorMatrix
72
+
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"
73
+
id="feColorMatrix4" />
74
+
<feBlend
75
+
in2="BackgroundImageFix"
76
+
result="effect1_dropShadow_125_61"
77
+
id="feBlend4"
78
+
mode="normal" />
79
+
<feBlend
80
+
in="SourceGraphic"
81
+
in2="effect1_dropShadow_125_61"
82
+
result="shape"
83
+
id="feBlend5"
84
+
mode="normal" />
85
+
</filter>
86
+
</defs>
87
+
</svg>
+2
-1
examples/00-hello-world/gleam.toml
+2
-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/README.md
+1
-1
examples/01-routing/README.md
···
11
11
This example is based off of the ["Hello, World!" example][hello], so read that
12
12
one first. The additions are detailed here and commented in the code.
13
13
14
-
[hello]: https://github.com/lpil/wisp/tree/main/examples/0-hello-world
14
+
[hello]: https://github.com/lpil/wisp/tree/main/examples/00-hello-world
15
15
16
16
### `app/router` module
17
17
+1
-1
examples/01-routing/gleam.toml
+1
-1
examples/01-routing/gleam.toml
+8
-8
examples/01-routing/src/app/router.gleam
+8
-8
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
-
use _req <- web.middleware(req)
7
+
use req <- web.middleware(req)
8
8
9
9
// Wisp doesn't have a special router abstraction, instead we recommend using
10
10
// regular old pattern matching. This is faster than a router, is type safe,
···
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
+2
-2
examples/02-working-with-form-data/README.md
+2
-2
examples/02-working-with-form-data/README.md
···
11
11
concepts from the [routing example][routing] so read those first. The additions
12
12
are detailed here and commented in the code.
13
13
14
-
[hello]: https://github.com/lpil/wisp/tree/main/examples/0-hello-world
15
-
[routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing
14
+
[hello]: https://github.com/lpil/wisp/tree/main/examples/00-hello-world
15
+
[routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing
16
16
17
17
### `app/router` module
18
18
+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
+2
-2
examples/03-working-with-json/README.md
+2
-2
examples/03-working-with-json/README.md
···
12
12
concepts from the [routing example][routing] so read those first. The additions
13
13
are detailed here and commented in the code.
14
14
15
-
[hello]: https://github.com/lpil/wisp/tree/main/examples/0-hello-world
16
-
[routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing
15
+
[hello]: https://github.com/lpil/wisp/tree/main/examples/00-hello-world
16
+
[routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing
17
17
18
18
### `gleam.toml` file
19
19
+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
+2
-2
examples/04-working-with-other-formats/README.md
+2
-2
examples/04-working-with-other-formats/README.md
···
13
13
concepts from the [routing example][routing] so read those first. The additions
14
14
are detailed here and commented in the code.
15
15
16
-
[hello]: https://github.com/lpil/wisp/tree/main/examples/0-hello-world
17
-
[routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing
16
+
[hello]: https://github.com/lpil/wisp/tree/main/examples/00-hello-world
17
+
[routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing
18
18
19
19
### `gleam.toml` file
20
20
+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
+2
-2
examples/05-using-a-database/README.md
+2
-2
examples/05-using-a-database/README.md
···
1
-
# Wisp Example: Using A Database
1
+
# Wisp Example: Using a database
2
2
3
3
```sh
4
4
gleam run # Run the server
···
11
11
This example is based off of the ["working with JSON" example][json], so read
12
12
that first. The additions are detailed here and commented in the code.
13
13
14
-
[json]: https://github.com/lpil/wisp/tree/main/examples/3-working-with-json
14
+
[json]: https://github.com/lpil/wisp/tree/main/examples/03-working-with-json
15
15
16
16
### `gleam.toml` file
17
17
+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
+4
-3
examples/06-serving-static-assets/README.md
+4
-3
examples/06-serving-static-assets/README.md
···
5
5
gleam test # Run the tests
6
6
```
7
7
8
-
This example shows how to route requests to different handlers based on the
9
-
request path and method.
8
+
This example shows how to serve static assets. In this case we'll serve
9
+
a CSS file for page styling and a JavaScript file for updating the content
10
+
of the HTML page, but the same techniques can also be used for other file types.
10
11
11
12
This example is based off of the ["Hello, World!" example][hello], so read that
12
13
one first. The additions are detailed here and commented in the code.
13
14
14
-
[hello]: https://github.com/lpil/wisp/tree/main/examples/1-routing
15
+
[hello]: https://github.com/lpil/wisp/tree/main/examples/01-routing
15
16
16
17
### `priv/static` directory
17
18
+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
}
+2
-3
examples/07-logging/README.md
+2
-3
examples/07-logging/README.md
···
5
5
gleam test # Run the tests
6
6
```
7
7
8
-
This example shows how to route requests to different handlers based on the
9
-
request path and method.
8
+
This example shows how to log messages using the BEAM logger.
10
9
11
10
This example is based off of the ["routing" example][routing], so read that
12
11
one first. The additions are detailed here and commented in the code.
13
12
14
-
[routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing
13
+
[routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing
15
14
16
15
### `app/router` module
17
16
+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
+3
-3
examples/09-configuring-default-responses/README.md
+3
-3
examples/09-configuring-default-responses/README.md
···
1
-
# Wisp Example: Working with form data
1
+
# Wisp Example: Configuring default responses
2
2
3
3
```sh
4
4
gleam run # Run the server
···
12
12
13
13
You likely want your application to return a generic error page rather than an empty body, and this example shows how to do that.
14
14
15
-
This example is based off of the [routing example][routing] so read that first.
15
+
This example is based off of the ["routing" example][routing] so read that first.
16
16
The additions are detailed here and commented in the code.
17
17
18
-
[routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing
18
+
[routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing
19
19
20
20
### `app/router` module
21
21
+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
+2
-3
examples/10-working-with-files/README.md
+2
-3
examples/10-working-with-files/README.md
···
7
7
8
8
This example shows how to accept file uploads and allow users to download files.
9
9
10
-
This example is based off of the ["Working with form data" example][formdata],
10
+
This example is based off of the ["working with form data" example][formdata],
11
11
so read that first. The additions are detailed here and commented in the code.
12
12
13
13
[formdata]: https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data
···
18
18
19
19
### `app_test` module
20
20
21
-
Tests have been added that send requests with form data bodies and check that
22
-
the expected response is returned.
21
+
Tests have been added that upload and download files to verify the behaviour.
23
22
24
23
### Other files
25
24
+1
-1
examples/10-working-with-files/gleam.toml
+1
-1
examples/10-working-with-files/gleam.toml
+8
-8
examples/10-working-with-files/src/app/router.gleam
+8
-8
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")
52
52
// The content-disposition header is set by this function to ensure this is
53
53
// treated as a file download. If the file was uploaded by the user then you
54
-
// want to ensure that this header is ste as otherwise the browser may try to
55
-
// display the file, which could enable in cross-site scripting attacks.
54
+
// want to ensure that this header is set as otherwise the browser may try to
55
+
// display the file, which could enable cross-site scripting attacks.
56
56
|> wisp.file_download_from_memory(
57
57
named: "hello.txt",
58
58
containing: file_contents,
···
63
63
use <- wisp.require_method(req, Get)
64
64
65
65
// In this case the file exists on the disc.
66
-
// Here's we're using the project README, but in a real application you'd
66
+
// Here we're using the project README, but in a real application you'd
67
67
// probably have an absolute path to wherever it is you keep your files.
68
68
let file_path = "./README.md"
69
69
···
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
+2
-2
examples/utilities/tiny_database/gleam.toml
+2
-2
examples/utilities/tiny_database/gleam.toml
+9
-8
examples/utilities/tiny_database/manifest.toml
+9
-8
examples/utilities/tiny_database/manifest.toml
···
2
2
# You typically do not need to edit this file
3
3
4
4
packages = [
5
-
{ name = "gleam_erlang", version = "0.23.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "C21CFB816C114784E669FFF4BBF433535EEA9960FA2F216209B8691E87156B96" },
5
+
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
6
+
{ name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
7
+
{ name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
6
8
{ name = "gleam_json", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB405BD93A8828BCD870463DE29375E7B2D252D9D124C109E5B618AAC00B86FC" },
7
-
{ name = "gleam_otp", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang"], otp_app = "gleam_otp", source = "hex", outer_checksum = "18EF8242A5E54BA92F717C7222F03B3228AEE00D1F286D4C56C3E8C18AA2588E" },
8
-
{ name = "gleam_stdlib", version = "0.32.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "ABF00CDCCB66FABBCE351A50060964C4ACE798F95A0D78622C8A7DC838792577" },
9
-
{ name = "gleeunit", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D3682ED8C5F9CAE1C928F2506DE91625588CC752495988CBE0F5653A42A6F334" },
10
-
{ name = "ids", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib", "gleam_otp"], otp_app = "ids", source = "hex", outer_checksum = "912A21722E07E68117B92863D05B15BE97E5AEF4ECF47C2F567CECCD5A4F5739" },
11
-
{ name = "simplifile", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0BD6F0E7DA1A7E11D18B8AD48453225CAFCA4C8CFB4513D217B372D2866C501C" },
9
+
{ name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
10
+
{ name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
11
+
{ name = "simplifile", version = "2.0.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "95219227A43FCFE62C6E494F413A1D56FF953B68FE420698612E3D89A1EFE029" },
12
12
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
13
+
{ name = "youid", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_stdlib"], otp_app = "youid", source = "hex", outer_checksum = "15BF3E8173C8741930E23D22071CD55AE203B6E43B9E0C6C9E7D9F116808E418" },
13
14
]
14
15
15
16
[requirements]
16
17
gleam_json = { version = "~> 0.6" }
17
18
gleam_stdlib = { version = "~> 0.30" }
18
19
gleeunit = { version = "~> 1.0" }
19
-
ids = { version = "~> 0.8" }
20
-
simplifile = { version = "~> 1.0" }
20
+
simplifile = { version = "~> 2.0" }
21
+
youid = { version = ">= 1.1.0 and < 2.0.0" }
+4
-4
examples/utilities/tiny_database/src/tiny_database.gleam
+4
-4
examples/utilities/tiny_database/src/tiny_database.gleam
···
1
-
import gleam/result
1
+
import gleam/dict.{type Dict}
2
2
import gleam/dynamic
3
-
import ids/nanoid
4
3
import gleam/json
5
4
import gleam/list
6
-
import gleam/dict.{type Dict}
5
+
import gleam/result
7
6
import simplifile
7
+
import youid/uuid
8
8
9
9
pub opaque type Connection {
10
10
Connection(root: String)
···
44
44
values: Dict(String, String),
45
45
) -> Result(String, Nil) {
46
46
let assert Ok(_) = simplifile.create_directory_all(connection.root)
47
-
let id = nanoid.generate()
47
+
let id = uuid.v4_string()
48
48
let values =
49
49
values
50
50
|> dict.to_list
+2
-2
examples/utilities/tiny_database/test/tiny_database_test.gleam
+2
-2
examples/utilities/tiny_database/test/tiny_database_test.gleam
···
1
+
import gleam/dict
1
2
import gleeunit
2
3
import gleeunit/should
3
4
import tiny_database
4
-
import gleam/map
5
5
6
6
pub fn main() {
7
7
gleeunit.main()
···
10
10
pub fn insert_read_test() {
11
11
let connection = tiny_database.connect("tmp/data")
12
12
13
-
let data = map.from_list([#("name", "Alice"), #("profession", "Programmer")])
13
+
let data = dict.from_list([#("name", "Alice"), #("profession", "Programmer")])
14
14
15
15
let assert Ok(Nil) = tiny_database.truncate(connection)
16
16
let assert Ok([]) = tiny_database.list(connection)
+15
-16
gleam.toml
+15
-16
gleam.toml
···
1
1
name = "wisp"
2
-
version = "0.12.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
-
exception = "~> 2.0"
14
-
gleam_crypto = "~> 1.0"
15
-
gleam_erlang = "~> 0.21"
16
-
gleam_http = "~> 3.5"
17
-
gleam_json = "~> 0.6 or ~> 1.0"
18
-
gleam_stdlib = "~> 0.29 or ~> 1.0"
19
-
mist = "~> 0.13"
20
-
simplifile = "~> 1.4"
21
-
marceau = "~> 1.1"
22
-
logging = "~> 1.0"
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"
23
22
24
23
[dev-dependencies]
25
-
gleeunit = "~> 1.0"
24
+
gleeunit = ">= 1.0.0 and < 2.0.0"
+33
-24
manifest.toml
+33
-24
manifest.toml
···
2
2
# You typically do not need to edit this file
3
3
4
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" },
5
8
{ name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
6
-
{ name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
7
-
{ name = "gleam_erlang", version = "0.24.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "26BDB52E61889F56A291CB34167315780EE4AA20961917314446542C90D1C1A0" },
8
-
{ name = "gleam_http", version = "3.5.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "C2FC3322203B16F897C1818D9810F5DEFCE347F0751F3B44421E1261277A7373" },
9
-
{ name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" },
10
-
{ name = "gleam_otp", version = "0.9.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5FADBBEC5ECF3F8B6BE91101D432758503192AE2ADBAD5602158977341489F71" },
11
-
{ name = "gleam_stdlib", version = "0.35.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5443EEB74708454B65650FEBBB1EF5175057D1DEC62AEA9D7C6D96F41DA79152" },
12
-
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
13
-
{ name = "glisten", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "73BC09C8487C2FFC0963BFAB33ED2F0D636FDFA43B966E65C1251CBAB8458099" },
14
-
{ name = "logging", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "82C112ED9B6C30C1772A6FE2613B94B13F62EA35F5869A2630D13948D297BD39" },
15
-
{ name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" },
16
-
{ name = "mist", version = "0.17.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten"], otp_app = "mist", source = "hex", outer_checksum = "DA8ACEE52C1E4892A75181B3166A4876D8CBC69D555E4770250BC84C80F75524" },
17
-
{ name = "simplifile", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "AAFCF154F69B237D269FF2764890F61ABC4A7EF2A592D44D67627B99694539D9" },
18
-
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
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" },
19
27
]
20
28
21
29
[requirements]
22
-
exception = { version = "~> 2.0" }
23
-
gleam_crypto = { version = "~> 1.0" }
24
-
gleam_erlang = { version = "~> 0.21" }
25
-
gleam_http = { version = "~> 3.5" }
26
-
gleam_json = { version = "~> 0.6 or ~> 1.0" }
27
-
gleam_stdlib = { version = "~> 0.29 or ~> 1.0" }
28
-
gleeunit = { version = "~> 1.0" }
29
-
logging = { version = "~> 1.0" }
30
-
marceau = { version = "~> 1.1" }
31
-
mist = { version = "~> 0.13" }
32
-
simplifile = { version = "~> 1.4" }
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
+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
+
}
+354
-314
src/wisp.gleam
+354
-314
src/wisp.gleam
···
1
1
import exception
2
-
import gleam/bytes_builder.{type BytesBuilder}
3
2
import gleam/bit_array
4
3
import gleam/bool
5
-
import gleam/dict.{type Dict}
4
+
import gleam/bytes_tree.{type BytesTree}
6
5
import gleam/crypto
6
+
import gleam/dict.{type Dict}
7
7
import gleam/dynamic.{type Dynamic}
8
8
import gleam/erlang
9
+
import gleam/erlang/atom.{type Atom}
9
10
import gleam/http.{type Method}
10
11
import gleam/http/cookie
11
12
import gleam/http/request.{type Request as HttpRequest}
···
13
14
type Response as HttpResponse, Response as HttpResponse,
14
15
}
15
16
import gleam/int
16
-
import gleam/erlang/atom.{type Atom}
17
17
import gleam/json
18
18
import gleam/list
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
···
152
67
HttpResponse(Body)
153
68
154
69
/// Create an empty response with the given status code.
155
-
///
70
+
///
156
71
/// # Examples
157
-
///
72
+
///
158
73
/// ```gleam
159
74
/// response(200)
160
75
/// // -> Response(200, [], Empty)
161
76
/// ```
162
-
///
77
+
///
163
78
pub fn response(status: Int) -> Response {
164
79
HttpResponse(status, [], Empty)
165
80
}
166
81
167
82
/// Set the body of a response.
168
-
///
83
+
///
169
84
/// # Examples
170
-
///
85
+
///
171
86
/// ```gleam
172
87
/// response(200)
173
88
/// |> set_body(File("/tmp/myfile.txt"))
174
89
/// // -> Response(200, [], File("/tmp/myfile.txt"))
175
90
/// ```
176
-
///
91
+
///
177
92
pub fn set_body(response: Response, body: Body) -> Response {
178
93
response
179
94
|> response.set_body(body)
···
193
108
/// `set_body` function with the `File` body variant.
194
109
///
195
110
/// # Examples
196
-
///
111
+
///
197
112
/// ```gleam
198
113
/// response(200)
199
114
/// |> file_download(named: "myfile.txt", from: "/tmp/myfile.txt")
···
229
144
/// as this can result in cross-site scripting vulnerabilities.
230
145
///
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
···
255
171
}
256
172
257
173
/// Create a HTML response.
258
-
///
174
+
///
259
175
/// The body is expected to be valid HTML, though this is not validated.
260
176
/// The `content-type` header will be set to `text/html`.
261
-
///
177
+
///
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
-
///
270
-
pub fn html_response(html: StringBuilder, status: Int) -> Response {
271
-
HttpResponse(status, [#("content-type", "text/html")], Text(html))
185
+
///
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.
275
-
///
195
+
///
276
196
/// The body is expected to be valid JSON, though this is not validated.
277
197
/// The `content-type` header will be set to `application/json`.
278
-
///
198
+
///
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
-
///
287
-
pub fn json_response(json: StringBuilder, status: Int) -> Response {
288
-
HttpResponse(status, [#("content-type", "application/json")], Text(json))
206
+
///
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
292
216
/// `content-type` header to `text/html`.
293
-
///
217
+
///
294
218
/// The body is expected to be valid HTML, though this is not validated.
295
-
///
219
+
///
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
-
///
305
-
pub fn html_body(response: Response, html: StringBuilder) -> Response {
228
+
///
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
312
236
/// `content-type` header to `application/json`.
313
-
///
237
+
///
314
238
/// The body is expected to be valid JSON, though this is not validated.
315
-
///
239
+
///
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
-
///
325
-
pub fn json_body(response: Response, json: StringBuilder) -> Response {
248
+
///
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.
335
259
///
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
-
///
345
-
pub fn string_builder_body(
346
-
response: Response,
347
-
content: StringBuilder,
348
-
) -> Response {
268
+
///
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.
357
278
///
358
279
/// # Examples
359
-
///
280
+
///
360
281
/// ```gleam
361
-
/// let body =
282
+
/// let body =
362
283
/// response(201)
363
-
/// |> string_builder_body("Hello, Joe!")
284
+
/// |> string_body("Hello, Joe!")
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.
···
384
305
/// escape_html("<h1>Hello, Joe!</h1>")
385
306
/// // -> "<h1>Hello, Joe!</h1>"
386
307
/// ```
387
-
///
308
+
///
388
309
pub fn escape_html(content: String) -> String {
389
-
do_escape_html("", content)
310
+
let bits = <<content:utf8>>
311
+
let acc = do_escape_html(bits, 0, bits, [])
312
+
313
+
list.reverse(acc)
314
+
|> bit_array.concat
315
+
// We know the bit array produced by `do_escape_html` is still a valid utf8
316
+
// string so we coerce it without passing through the validation steps of
317
+
// `bit_array.to_string`.
318
+
|> coerce_bit_array_to_string
390
319
}
391
320
392
-
fn do_escape_html(escaped: String, content: String) -> String {
393
-
case string.pop_grapheme(content) {
394
-
Ok(#("<", xs)) -> do_escape_html(escaped <> "<", xs)
395
-
Ok(#(">", xs)) -> do_escape_html(escaped <> ">", xs)
396
-
Ok(#("&", xs)) -> do_escape_html(escaped <> "&", xs)
397
-
Ok(#(x, xs)) -> do_escape_html(escaped <> x, xs)
398
-
Error(_) -> escaped <> content
321
+
@external(erlang, "wisp_ffi", "coerce")
322
+
fn coerce_bit_array_to_string(bit_array: BitArray) -> String
323
+
324
+
// A possible way to escape chars would be to split the string into graphemes,
325
+
// traverse those one by one and accumulate them back into a string escaping
326
+
// ">", "<", etc. as we see them.
327
+
//
328
+
// However, we can be a lot more performant by working directly on the
329
+
// `BitArray` representing a Gleam UTF-8 String.
330
+
// This means that, instead of popping a grapheme at a time, we can work
331
+
// directly on BitArray slices: this has the big advantage of making sure we
332
+
// share as much as possible with the original string without having to build
333
+
// a new one from scratch.
334
+
//
335
+
fn do_escape_html(
336
+
bin: BitArray,
337
+
skip: Int,
338
+
original: BitArray,
339
+
acc: List(BitArray),
340
+
) -> List(BitArray) {
341
+
case bin {
342
+
// If we find a char to escape we just advance the `skip` counter so that
343
+
// it will be ignored in the following slice, then we append the escaped
344
+
// version to the accumulator.
345
+
<<"<":utf8, rest:bits>> -> {
346
+
let acc = [<<"<":utf8>>, ..acc]
347
+
do_escape_html(rest, skip + 1, original, acc)
348
+
}
349
+
350
+
<<">":utf8, rest:bits>> -> {
351
+
let acc = [<<">":utf8>>, ..acc]
352
+
do_escape_html(rest, skip + 1, original, acc)
353
+
}
354
+
355
+
<<"&":utf8, rest:bits>> -> {
356
+
let acc = [<<"&":utf8>>, ..acc]
357
+
do_escape_html(rest, skip + 1, original, acc)
358
+
}
359
+
360
+
// For any other bit that doesn't need to be escaped we go into an inner
361
+
// loop, consuming as much "non-escapable" chars as possible.
362
+
<<_char, rest:bits>> -> do_escape_html_regular(rest, skip, original, acc, 1)
363
+
364
+
<<>> -> acc
365
+
366
+
_ -> panic as "non byte aligned string, all strings should be byte aligned"
367
+
}
368
+
}
369
+
370
+
fn do_escape_html_regular(
371
+
bin: BitArray,
372
+
skip: Int,
373
+
original: BitArray,
374
+
acc: List(BitArray),
375
+
len: Int,
376
+
) -> List(BitArray) {
377
+
// Remember, if we're here it means we've found a char that doesn't need to be
378
+
// escaped, so what we want to do is advance the `len` counter until we reach
379
+
// a char that _does_ need to be escaped and take the slice going from
380
+
// `skip` with size `len`.
381
+
//
382
+
// Imagine we're escaping this string: "abc<def&ghi" and we've reached 'd':
383
+
// ```
384
+
// abc<def&ghi
385
+
// ^ `skip` points here
386
+
// ```
387
+
// We're going to be increasing `len` until we reach the '&':
388
+
// ```
389
+
// abc<def&ghi
390
+
// ^^^ len will be 3 when we reach the '&' that needs escaping
391
+
// ```
392
+
// So we take the slice corresponding to "def".
393
+
//
394
+
case bin {
395
+
// If we reach a char that has to be escaped we append the slice starting
396
+
// from `skip` with size `len` and the escaped char.
397
+
// This is what allows us to share as much of the original string as
398
+
// possible: we only allocate a new BitArray for the escaped chars,
399
+
// everything else is just a slice of the original String.
400
+
<<"<":utf8, rest:bits>> -> {
401
+
let assert Ok(slice) = bit_array.slice(original, skip, len)
402
+
let acc = [<<"<":utf8>>, slice, ..acc]
403
+
do_escape_html(rest, skip + len + 1, original, acc)
404
+
}
405
+
406
+
<<">":utf8, rest:bits>> -> {
407
+
let assert Ok(slice) = bit_array.slice(original, skip, len)
408
+
let acc = [<<">":utf8>>, slice, ..acc]
409
+
do_escape_html(rest, skip + len + 1, original, acc)
410
+
}
411
+
412
+
<<"&":utf8, rest:bits>> -> {
413
+
let assert Ok(slice) = bit_array.slice(original, skip, len)
414
+
let acc = [<<"&":utf8>>, slice, ..acc]
415
+
do_escape_html(rest, skip + len + 1, original, acc)
416
+
}
417
+
418
+
// If a char doesn't need escaping we keep increasing the length of the
419
+
// slice we're going to take.
420
+
<<_char, rest:bits>> ->
421
+
do_escape_html_regular(rest, skip, original, acc, len + 1)
422
+
423
+
<<>> ->
424
+
case skip {
425
+
0 -> [original]
426
+
_ -> {
427
+
let assert Ok(slice) = bit_array.slice(original, skip, len)
428
+
[slice, ..acc]
429
+
}
430
+
}
431
+
432
+
_ -> panic as "non byte aligned string, all strings should be byte aligned"
399
433
}
400
434
}
401
435
···
593
627
//
594
628
595
629
/// The connection to the client for a HTTP request.
596
-
///
630
+
///
597
631
/// The body of the request can be read from this connection using functions
598
632
/// such as `require_multipart_body`.
599
-
///
600
-
pub opaque type Connection {
601
-
Connection(
602
-
reader: Reader,
603
-
max_body_size: Int,
604
-
max_files_size: Int,
605
-
read_chunk_size: Int,
606
-
secret_key_base: String,
607
-
temporary_directory: String,
608
-
)
609
-
}
610
-
611
-
fn make_connection(body_reader: Reader, secret_key_base: String) -> Connection {
612
-
// TODO: replace `/tmp` with appropriate for the OS
613
-
let prefix = "/tmp/gleam-wisp/"
614
-
let temporary_directory = join_path(prefix, random_slug())
615
-
Connection(
616
-
reader: body_reader,
617
-
max_body_size: 8_000_000,
618
-
max_files_size: 32_000_000,
619
-
read_chunk_size: 1_000_000,
620
-
temporary_directory: temporary_directory,
621
-
secret_key_base: secret_key_base,
622
-
)
623
-
}
633
+
///
634
+
pub type Connection =
635
+
internal.Connection
624
636
625
637
type BufferedReader {
626
-
BufferedReader(reader: Reader, buffer: BitArray)
638
+
BufferedReader(reader: internal.Reader, buffer: BitArray)
627
639
}
628
640
629
641
type Quotas {
···
645
657
}
646
658
}
647
659
648
-
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) {
649
664
case reader.buffer {
650
665
<<>> -> reader.reader(chunk_size)
651
-
_ -> Ok(Chunk(reader.buffer, reader.reader))
666
+
_ -> Ok(internal.Chunk(reader.buffer, reader.reader))
652
667
}
653
-
}
654
-
655
-
type Reader =
656
-
fn(Int) -> Result(Read, Nil)
657
-
658
-
type Read {
659
-
Chunk(BitArray, next: Reader)
660
-
ReadingFinished
661
668
}
662
669
663
670
/// Set the maximum permitted size of a request body of the request in bytes.
···
671
678
/// instead use the `max_files_size` limit.
672
679
///
673
680
pub fn set_max_body_size(request: Request, size: Int) -> Request {
674
-
Connection(..request.body, max_body_size: size)
681
+
internal.Connection(..request.body, max_body_size: size)
675
682
|> request.set_body(request, _)
676
683
}
677
684
678
685
/// Get the maximum permitted size of a request body of the request in bytes.
679
-
///
686
+
///
680
687
pub fn get_max_body_size(request: Request) -> Int {
681
688
request.body.max_body_size
682
689
}
683
690
684
691
/// Set the secret key base used to sign cookies and other sensitive data.
685
-
///
692
+
///
686
693
/// This key must be at least 64 bytes long and should be kept secret. Anyone
687
694
/// with this secret will be able to manipulate signed cookies and other sensitive
688
695
/// data.
689
696
///
690
697
/// # Panics
691
-
///
698
+
///
692
699
/// This function will panic if the key is less than 64 bytes long.
693
700
///
694
701
pub fn set_secret_key_base(request: Request, key: String) -> Request {
695
702
case string.byte_size(key) < 64 {
696
703
True -> panic as "Secret key base must be at least 64 bytes long"
697
704
False ->
698
-
Connection(..request.body, secret_key_base: key)
705
+
internal.Connection(..request.body, secret_key_base: key)
699
706
|> request.set_body(request, _)
700
707
}
701
708
}
702
709
703
710
/// Get the secret key base used to sign cookies and other sensitive data.
704
-
///
711
+
///
705
712
pub fn get_secret_key_base(request: Request) -> String {
706
713
request.body.secret_key_base
707
714
}
···
714
721
///
715
722
/// This limit only applies for files in a multipart body that get streamed to
716
723
/// disc. For headers and other content that gets read into memory use the
717
-
/// `max_files_size` limit.
724
+
/// `max_body_size` limit.
718
725
///
719
726
pub fn set_max_files_size(request: Request, size: Int) -> Request {
720
-
Connection(..request.body, max_files_size: size)
727
+
internal.Connection(..request.body, max_files_size: size)
721
728
|> request.set_body(request, _)
722
729
}
723
730
724
731
/// Get the maximum permitted total size of a files uploaded by a request in
725
732
/// bytes.
726
-
///
733
+
///
727
734
pub fn get_max_files_size(request: Request) -> Int {
728
735
request.body.max_files_size
729
736
}
···
737
744
/// been received from the client.
738
745
///
739
746
pub fn set_read_chunk_size(request: Request, size: Int) -> Request {
740
-
Connection(..request.body, read_chunk_size: size)
747
+
internal.Connection(..request.body, read_chunk_size: size)
741
748
|> request.set_body(request, _)
742
749
}
743
750
744
751
/// Get the size limit for each chunk of the request body when read from the
745
752
/// client.
746
-
///
753
+
///
747
754
pub fn get_read_chunk_size(request: Request) -> Int {
748
755
request.body.read_chunk_size
749
756
}
750
757
751
758
/// A convenient alias for a HTTP request with a Wisp connection as the body.
752
-
///
759
+
///
753
760
pub type Request =
754
-
HttpRequest(Connection)
761
+
HttpRequest(internal.Connection)
755
762
756
763
/// This middleware function ensures that the request has a specific HTTP
757
764
/// method, returning an empty response with status code 405: Method not allowed
758
765
/// if the method is not correct.
759
766
///
760
767
/// # Examples
761
-
///
768
+
///
762
769
/// ```gleam
763
770
/// fn handle_request(request: Request) -> Response {
764
771
/// use <- wisp.require_method(request, http.Patch)
···
779
786
780
787
// TODO: re-export once Gleam has a syntax for that
781
788
/// Return the non-empty segments of a request path.
782
-
///
789
+
///
783
790
/// # Examples
784
791
///
785
792
/// ```gleam
···
793
800
794
801
// TODO: re-export once Gleam has a syntax for that
795
802
/// Set a given header to a given value, replacing any existing value.
796
-
///
803
+
///
797
804
/// # Examples
798
805
///
799
806
/// ```gleam
···
863
870
/// return an incorrect value, depending on the underlying web server. It is the
864
871
/// responsibility of the caller to cache the body if it is needed multiple
865
872
/// times.
866
-
///
873
+
///
867
874
/// If the body is larger than the `max_body_size` limit then an empty response
868
875
/// with status code 413: Entity too large will be returned to the client.
869
-
///
876
+
///
870
877
/// If the body is found not to be valid UTF-8 then an empty response with
871
878
/// status code 400: Bad request will be returned to the client.
872
-
///
879
+
///
873
880
/// # Examples
874
881
///
875
882
/// ```gleam
···
899
906
/// return an incorrect value, depending on the underlying web server. It is the
900
907
/// responsibility of the caller to cache the body if it is needed multiple
901
908
/// times.
902
-
///
909
+
///
903
910
/// If the body is larger than the `max_body_size` limit then an empty response
904
911
/// with status code 413: Entity too large will be returned to the client.
905
-
///
912
+
///
906
913
/// # Examples
907
914
///
908
915
/// ```gleam
···
925
932
// TODO: don't always return entity to large. Other errors are possible, such as
926
933
// network errors.
927
934
/// Read the entire body of the request as a bit string.
928
-
///
935
+
///
929
936
/// You may instead wish to use the `require_bit_array_body` or the
930
937
/// `require_string_body` middleware functions instead.
931
-
///
938
+
///
932
939
/// This function does not cache the body in any way, so if you call this
933
940
/// function (or any other body reading function) more than once it may hang or
934
941
/// return an incorrect value, depending on the underlying web server. It is the
935
942
/// responsibility of the caller to cache the body if it is needed multiple
936
943
/// times.
937
-
///
944
+
///
938
945
/// If the body is larger than the `max_body_size` limit then an empty response
939
946
/// with status code 413: Entity too large will be returned to the client.
940
-
///
947
+
///
941
948
pub fn read_body_to_bitstring(request: Request) -> Result(BitArray, Nil) {
942
949
let connection = request.body
943
950
read_body_loop(
···
949
956
}
950
957
951
958
fn read_body_loop(
952
-
reader: Reader,
959
+
reader: internal.Reader,
953
960
read_chunk_size: Int,
954
961
max_body_size: Int,
955
962
accumulator: BitArray,
956
963
) -> Result(BitArray, Nil) {
957
964
use chunk <- result.try(reader(read_chunk_size))
958
965
case chunk {
959
-
ReadingFinished -> Ok(accumulator)
960
-
Chunk(chunk, next) -> {
966
+
internal.ReadingFinished -> Ok(accumulator)
967
+
internal.Chunk(chunk, next) -> {
961
968
let accumulator = bit_array.append(accumulator, chunk)
962
969
case bit_array.byte_size(accumulator) > max_body_size {
963
970
True -> Error(Nil)
···
971
978
/// A middleware which extracts form data from the body of a request that is
972
979
/// encoded as either `application/x-www-form-urlencoded` or
973
980
/// `multipart/form-data`.
974
-
///
981
+
///
975
982
/// Extracted fields are sorted into alphabetical order by key, so if you wish
976
983
/// to use pattern matching the order can be relied upon.
977
-
///
984
+
///
978
985
/// ```gleam
979
986
/// fn handle_request(request: Request) -> Response {
980
987
/// use form <- wisp.require_form(request)
···
1003
1010
///
1004
1011
/// If the body cannot be parsed successfully then an empty response with status
1005
1012
/// code 400: Bad request will be returned to the client.
1006
-
///
1013
+
///
1007
1014
pub fn require_form(
1008
1015
request: Request,
1009
1016
next: fn(FormData) -> Response,
···
1030
1037
/// Unsupported media type if the header is not the expected value
1031
1038
///
1032
1039
/// # Examples
1033
-
///
1040
+
///
1034
1041
/// ```gleam
1035
1042
/// fn handle_request(request: Request) -> Response {
1036
1043
/// use <- wisp.require_content_type(request, "application/json")
···
1044
1051
next: fn() -> Response,
1045
1052
) -> Response {
1046
1053
case list.key_find(request.headers, "content-type") {
1047
-
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
+
1048
1063
_ -> unsupported_media_type([expected])
1049
1064
}
1050
1065
}
1051
1066
1052
1067
/// A middleware which extracts JSON from the body of a request.
1053
-
///
1068
+
///
1054
1069
/// ```gleam
1055
1070
/// fn handle_request(request: Request) -> Response {
1056
1071
/// use json <- wisp.require_json(request)
···
1071
1086
///
1072
1087
/// If the body cannot be parsed successfully then an empty response with status
1073
1088
/// code 400: Bad request will be returned to the client.
1074
-
///
1089
+
///
1075
1090
pub fn require_json(request: Request, next: fn(Dynamic) -> Response) -> Response {
1076
1091
use <- require_content_type(request, "application/json")
1077
1092
use body <- require_string_body(request)
···
1255
1270
fn read_chunk(
1256
1271
reader: BufferedReader,
1257
1272
chunk_size: Int,
1258
-
) -> Result(#(BitArray, Reader), Response) {
1273
+
) -> Result(#(BitArray, internal.Reader), Response) {
1259
1274
buffered_read(reader, chunk_size)
1260
1275
|> result.replace_error(bad_request())
1261
1276
|> result.try(fn(chunk) {
1262
1277
case chunk {
1263
-
Chunk(chunk, next) -> Ok(#(chunk, next))
1264
-
ReadingFinished -> Error(bad_request())
1278
+
internal.Chunk(chunk, next) -> Ok(#(chunk, next))
1279
+
internal.ReadingFinished -> Error(bad_request())
1265
1280
}
1266
1281
})
1267
1282
}
···
1305
1320
}
1306
1321
1307
1322
/// Data parsed from form sent in a request's body.
1308
-
///
1323
+
///
1309
1324
pub type FormData {
1310
1325
FormData(
1311
1326
/// String values of the form's fields.
···
1405
1420
response
1406
1421
}
1407
1422
1408
-
fn remove_preceeding_slashes(string: String) -> String {
1409
-
case string {
1410
-
"/" <> rest -> remove_preceeding_slashes(rest)
1411
-
_ -> string
1412
-
}
1413
-
}
1414
-
1415
-
// TODO: replace with simplifile function when it exists
1416
-
fn join_path(a: String, b: String) -> String {
1417
-
let b = remove_preceeding_slashes(b)
1418
-
case string.ends_with(a, "/") {
1419
-
True -> a <> b
1420
-
False -> a <> "/" <> b
1421
-
}
1422
-
}
1423
-
1424
1423
/// A middleware function that serves files from a directory, along with a
1425
1424
/// suitable `content-type` header for known file extensions.
1426
1425
///
···
1429
1428
///
1430
1429
/// The `under` parameter is the request path prefix that must match for the
1431
1430
/// file to be served.
1432
-
///
1431
+
///
1433
1432
/// | `under` | `from` | `request.path` | `file` |
1434
1433
/// |-----------|---------|--------------------|-------------------------|
1435
1434
/// | `/static` | `/data` | `/static/file.txt` | `/data/file.txt` |
···
1468
1467
from directory: String,
1469
1468
next handler: fn() -> Response,
1470
1469
) -> Response {
1471
-
let path = remove_preceeding_slashes(req.path)
1472
-
let prefix = remove_preceeding_slashes(prefix)
1470
+
let path = internal.remove_preceeding_slashes(req.path)
1471
+
let prefix = internal.remove_preceeding_slashes(prefix)
1473
1472
case req.method, string.starts_with(path, prefix) {
1474
1473
http.Get, True -> {
1475
1474
let path =
1476
1475
path
1477
-
|> string.drop_left(string.length(prefix))
1476
+
|> string.drop_start(string.length(prefix))
1478
1477
|> string.replace(each: "..", with: "")
1479
-
|> join_path(directory, _)
1478
+
|> internal.join_path(directory, _)
1480
1479
1481
1480
let mime_type =
1482
1481
req.path
···
1485
1484
|> result.unwrap("")
1486
1485
|> marceau.extension_to_mime_type
1487
1486
1488
-
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) {
1489
1493
Ok(True) ->
1490
1494
response.new(200)
1491
-
|> response.set_header("content-type", mime_type)
1495
+
|> response.set_header("content-type", content_type)
1492
1496
|> response.set_body(File(path))
1493
1497
_ -> handler()
1494
1498
}
···
1535
1539
1536
1540
/// Create a new temporary directory for the given request.
1537
1541
///
1538
-
/// 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
1539
1543
/// adapter then this file will be deleted for you when the request is complete.
1540
1544
/// Otherwise you will need to call the `delete_temporary_files` function
1541
1545
/// yourself.
···
1545
1549
) -> Result(String, simplifile.FileError) {
1546
1550
let directory = request.body.temporary_directory
1547
1551
use _ <- result.try(simplifile.create_directory_all(directory))
1548
-
let path = join_path(directory, random_slug())
1552
+
let path = internal.join_path(directory, internal.random_slug())
1549
1553
use _ <- result.map(simplifile.create_file(path))
1550
1554
path
1551
1555
}
1552
1556
1553
1557
/// Delete any temporary files created for the given request.
1554
1558
///
1555
-
/// 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
1556
1560
/// adapter then this file will be deleted for you when the request is complete.
1557
1561
/// Otherwise you will need to call this function yourself.
1558
1562
///
···
1576
1580
/// > erlang.priv_directory("my_app")
1577
1581
/// // -> Ok("/some/location/my_app/priv")
1578
1582
/// ```
1579
-
///
1583
+
///
1580
1584
pub const priv_directory = erlang.priv_directory
1581
1585
1582
1586
//
···
1585
1589
1586
1590
/// Configure the Erlang logger, setting the minimum log level to `info`, to be
1587
1591
/// called when your application starts.
1588
-
///
1592
+
///
1589
1593
/// You may wish to use an alternative for this such as one provided by a more
1590
1594
/// sophisticated logging library.
1591
-
///
1595
+
///
1592
1596
/// In future this function may be extended to change the output format.
1593
-
///
1597
+
///
1594
1598
pub fn configure_logger() -> Nil {
1595
1599
logging.configure()
1596
1600
}
1597
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
+
1598
1642
/// Log a message to the Erlang logger with the level of `emergency`.
1599
-
///
1643
+
///
1600
1644
/// See the [Erlang logger documentation][1] for more information.
1601
-
///
1645
+
///
1602
1646
/// [1]: https://www.erlang.org/doc/man/logger
1603
-
///
1647
+
///
1604
1648
pub fn log_emergency(message: String) -> Nil {
1605
1649
logging.log(logging.Emergency, message)
1606
1650
}
1607
1651
1608
1652
/// Log a message to the Erlang logger with the level of `alert`.
1609
-
///
1653
+
///
1610
1654
/// See the [Erlang logger documentation][1] for more information.
1611
-
///
1655
+
///
1612
1656
/// [1]: https://www.erlang.org/doc/man/logger
1613
-
///
1657
+
///
1614
1658
pub fn log_alert(message: String) -> Nil {
1615
1659
logging.log(logging.Alert, message)
1616
1660
}
1617
1661
1618
1662
/// Log a message to the Erlang logger with the level of `critical`.
1619
-
///
1663
+
///
1620
1664
/// See the [Erlang logger documentation][1] for more information.
1621
-
///
1665
+
///
1622
1666
/// [1]: https://www.erlang.org/doc/man/logger
1623
-
///
1667
+
///
1624
1668
pub fn log_critical(message: String) -> Nil {
1625
1669
logging.log(logging.Critical, message)
1626
1670
}
1627
1671
1628
1672
/// Log a message to the Erlang logger with the level of `error`.
1629
-
///
1673
+
///
1630
1674
/// See the [Erlang logger documentation][1] for more information.
1631
-
///
1675
+
///
1632
1676
/// [1]: https://www.erlang.org/doc/man/logger
1633
-
///
1677
+
///
1634
1678
pub fn log_error(message: String) -> Nil {
1635
1679
logging.log(logging.Error, message)
1636
1680
}
1637
1681
1638
1682
/// Log a message to the Erlang logger with the level of `warning`.
1639
-
///
1683
+
///
1640
1684
/// See the [Erlang logger documentation][1] for more information.
1641
-
///
1685
+
///
1642
1686
/// [1]: https://www.erlang.org/doc/man/logger
1643
-
///
1687
+
///
1644
1688
pub fn log_warning(message: String) -> Nil {
1645
1689
logging.log(logging.Warning, message)
1646
1690
}
1647
1691
1648
1692
/// Log a message to the Erlang logger with the level of `notice`.
1649
-
///
1693
+
///
1650
1694
/// See the [Erlang logger documentation][1] for more information.
1651
-
///
1695
+
///
1652
1696
/// [1]: https://www.erlang.org/doc/man/logger
1653
-
///
1697
+
///
1654
1698
pub fn log_notice(message: String) -> Nil {
1655
1699
logging.log(logging.Notice, message)
1656
1700
}
1657
1701
1658
1702
/// Log a message to the Erlang logger with the level of `info`.
1659
-
///
1703
+
///
1660
1704
/// See the [Erlang logger documentation][1] for more information.
1661
-
///
1705
+
///
1662
1706
/// [1]: https://www.erlang.org/doc/man/logger
1663
-
///
1707
+
///
1664
1708
pub fn log_info(message: String) -> Nil {
1665
1709
logging.log(logging.Info, message)
1666
1710
}
1667
1711
1668
1712
/// Log a message to the Erlang logger with the level of `debug`.
1669
-
///
1713
+
///
1670
1714
/// See the [Erlang logger documentation][1] for more information.
1671
-
///
1715
+
///
1672
1716
/// [1]: https://www.erlang.org/doc/man/logger
1673
-
///
1717
+
///
1674
1718
pub fn log_debug(message: String) -> Nil {
1675
1719
logging.log(logging.Debug, message)
1676
1720
}
···
1682
1726
/// Generate a random string of the given length.
1683
1727
///
1684
1728
pub fn random_string(length: Int) -> String {
1685
-
crypto.strong_random_bytes(length)
1686
-
|> bit_array.base64_url_encode(False)
1687
-
|> string.slice(0, length)
1729
+
internal.random_string(length)
1688
1730
}
1689
1731
1690
1732
/// Sign a message which can later be verified using the `verify_signed_message`
1691
1733
/// function to detect if the message has been tampered with.
1692
-
///
1734
+
///
1693
1735
/// Signed messages are not encrypted and can be read by anyone. They are not
1694
1736
/// suitable for storing sensitive information.
1695
-
///
1737
+
///
1696
1738
/// This function uses the secret key base from the request. If the secret
1697
1739
/// changes then the signature will no longer be verifiable.
1698
-
///
1740
+
///
1699
1741
pub fn sign_message(
1700
1742
request: Request,
1701
1743
message: BitArray,
···
1705
1747
}
1706
1748
1707
1749
/// Verify a signed message which was signed using the `sign_message` function.
1708
-
///
1750
+
///
1709
1751
/// Returns the content of the message if the signature is valid, otherwise
1710
1752
/// returns an error.
1711
-
///
1753
+
///
1712
1754
/// This function uses the secret key base from the request. If the secret
1713
1755
/// changes then the signature will no longer be verifiable.
1714
-
///
1756
+
///
1715
1757
pub fn verify_signed_message(
1716
1758
request: Request,
1717
1759
message: String,
1718
1760
) -> Result(BitArray, Nil) {
1719
1761
crypto.verify_signed_message(message, <<request.body.secret_key_base:utf8>>)
1720
-
}
1721
-
1722
-
fn random_slug() -> String {
1723
-
random_string(16)
1724
1762
}
1725
1763
1726
1764
//
···
1747
1785
///
1748
1786
/// ```gleam
1749
1787
/// wisp.ok()
1750
-
/// |> wisp.set_cookie("id", "123", wisp.PlainText, 60 * 60)
1788
+
/// |> wisp.set_cookie(request, "id", "123", wisp.PlainText, 60 * 60)
1751
1789
/// ```
1752
-
///
1790
+
///
1753
1791
/// Setting a signed cookie that the client can read but not modify:
1754
-
///
1792
+
///
1755
1793
/// ```gleam
1756
1794
/// wisp.ok()
1757
-
/// |> wisp.set_cookie("id", value, wisp.Signed, 60 * 60)
1795
+
/// |> wisp.set_cookie(request, "id", value, wisp.Signed, 60 * 60)
1758
1796
/// ```
1759
1797
///
1760
1798
pub fn set_cookie(
···
1794
1832
/// for a signed cookie, then `Error(Nil)` is returned.
1795
1833
///
1796
1834
/// ```gleam
1797
-
/// wisp.get_cookie(request, "group")
1835
+
/// wisp.get_cookie(request, "group", wisp.PlainText)
1798
1836
/// // -> Ok("A")
1799
1837
/// ```
1800
1838
///
···
1821
1859
1822
1860
// TODO: chunk the body
1823
1861
/// Create a connection which will return the given body when read.
1824
-
///
1862
+
///
1825
1863
/// This function is intended for use in tests, though you probably want the
1826
1864
/// `wisp/testing` module instead.
1827
-
///
1865
+
///
1828
1866
pub fn create_canned_connection(
1829
1867
body: BitArray,
1830
1868
secret_key_base: String,
1831
-
) -> Connection {
1832
-
make_connection(
1833
-
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
+
},
1834
1874
secret_key_base,
1835
1875
)
1836
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
}
+55
-24
test/wisp_test.gleam
+55
-24
test/wisp_test.gleam
···
1
+
import exception
1
2
import gleam/bit_array
2
3
import gleam/crypto
4
+
import gleam/dict
3
5
import gleam/dynamic.{type Dynamic}
4
6
import gleam/erlang
5
7
import gleam/http
···
7
9
import gleam/http/response.{Response}
8
10
import gleam/int
9
11
import gleam/list
10
-
import gleam/dict
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