+29
-21
.github/workflows/ci.yml
+29
-21
.github/workflows/ci.yml
···
10
10
test-action:
11
11
runs-on: ubuntu-latest
12
12
steps:
13
-
- uses: actions/checkout@v3
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
+
run: gleam test
46
+
working-directory: examples/05-using-a-database
47
+
48
+
- name: "Example: 06-serving-static-assets"
49
+
run: gleam test
50
+
working-directory: examples/06-serving-static-assets
51
+
52
+
- name: "Example: 07-logging"
45
53
run: gleam test
46
-
working-directory: examples/5-using-a-database
54
+
working-directory: examples/07-logging
47
55
48
-
- name: "Example: 6-serving-static-assets"
56
+
- name: "Example: 08-working-with-cookies"
49
57
run: gleam test
50
-
working-directory: examples/6-serving-static-assets
58
+
working-directory: examples/08-working-with-cookies
51
59
52
-
- name: "Example: 7-logging"
60
+
- name: "Example: 09-configuring-default-responses"
53
61
run: gleam test
54
-
working-directory: examples/7-logging
62
+
working-directory: examples/09-configuring-default-responses
55
63
56
-
- name: "Example: 8-working-with-cookies"
64
+
- name: "Example: 10-working-with-files"
57
65
run: gleam test
58
-
working-directory: examples/8-working-with-cookies
66
+
working-directory: examples/10-working-with-files
+49
CHANGELOG.md
+49
CHANGELOG.md
···
1
1
# Changelog
2
2
3
+
## v1.3.0 - 2024-11-21
4
+
5
+
- Updated for `gleam_stdlib` v0.43.0.
6
+
7
+
## v1.2.0 - 2024-10-09
8
+
9
+
- The requirement for `gleam_json` has been relaxed to < 3.0.0.
10
+
- The requirement for `mist` has been relaxed to < 4.0.0.
11
+
- The Gleam version requirement has been corrected to `>= 1.1.0` from the
12
+
previously inaccurate `">= 0.32.0`.
13
+
14
+
## v1.1.0 - 2024-08-23
15
+
16
+
- Rather than using `/tmp`, the platform-specific temporary directory is
17
+
detected used.
18
+
19
+
## v1.0.0 - 2024-08-21
20
+
21
+
- The Mist web server related functions have been moved to the `wisp_mist`
22
+
module.
23
+
- The `wisp` module gains the `set_logger_level` function and `LogLevel` type.
24
+
25
+
## v0.16.0 - 2024-07-13
26
+
27
+
- HTML and JSON body functions now include `charset=utf-8` in the content-type
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
43
+
44
+
- The `wisp` module gains the `file_download_from_memory` and `file_download`
45
+
functions.
46
+
47
+
## v0.12.0 - 2024-02-17
48
+
49
+
- The output format used by the logger has been improved.
50
+
- Erlang SASL and supervisor logs are no longer emitted.
51
+
3
52
## v0.11.0 - 2024-02-03
4
53
5
54
- Updated for simplifile v1.4 and replaced the deprecated `simplifile.is_file`
+14
-13
README.md
+14
-13
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)
59
-
- [Routing](https://github.com/lpil/wisp/tree/main/examples/1-routing)
60
-
- [Working with form data](https://github.com/lpil/wisp/tree/main/examples/2-working-with-form-data)
61
-
- [Working with JSON](https://github.com/lpil/wisp/tree/main/examples/3-working-with-json)
62
-
- [Working with other formats](https://github.com/lpil/wisp/tree/main/examples/4-working-with-other-formats)
63
-
- [Using a database](https://github.com/lpil/wisp/tree/main/examples/5-using-a-database)
64
-
- [Serving static assets](https://github.com/lpil/wisp/tree/main/examples/6-serving-static-assets)
65
-
- [Logging](https://github.com/lpil/wisp/tree/main/examples/7-logging)
66
-
- [Working with cookies](https://github.com/lpil/wisp/tree/main/examples/8-working-with-cookies)
67
-
- [Configuring default responses](https://github.com/lpil/wisp/tree/main/examples/9-configuring-default-responses)
58
+
- [Hello, World!](https://github.com/lpil/wisp/tree/main/examples/00-hello-world)
59
+
- [Routing](https://github.com/lpil/wisp/tree/main/examples/01-routing)
60
+
- [Working with form data](https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data)
61
+
- [Working with JSON](https://github.com/lpil/wisp/tree/main/examples/03-working-with-json)
62
+
- [Working with other formats](https://github.com/lpil/wisp/tree/main/examples/04-working-with-other-formats)
63
+
- [Using a database](https://github.com/lpil/wisp/tree/main/examples/05-using-a-database)
64
+
- [Serving static assets](https://github.com/lpil/wisp/tree/main/examples/06-serving-static-assets)
65
+
- [Logging](https://github.com/lpil/wisp/tree/main/examples/07-logging)
66
+
- [Working with cookies](https://github.com/lpil/wisp/tree/main/examples/08-working-with-cookies)
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/10-working-with-files)
68
69
69
70
API documentation is available on [HexDocs](https://hexdocs.pm/wisp/).
70
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>
-42
examples/0-hello-world/README.md
-42
examples/0-hello-world/README.md
···
1
-
# Wisp Example: Hello, world!
2
-
3
-
```sh
4
-
gleam run # Run the server
5
-
gleam test # Run the tests
6
-
```
7
-
8
-
This example shows a minimal Wisp application, it does nothing but respond with
9
-
a greeting to any request.
10
-
11
-
The project has this structure:
12
-
13
-
```
14
-
├─ src
15
-
│ ├─ app
16
-
│ │ ├─ router.gleam
17
-
│ │ └─ web.gleam
18
-
│ └─ app.gleam
19
-
└─ test
20
-
└── app_test.gleam
21
-
```
22
-
23
-
In your project `app` would be replaced by the name of your application.
24
-
25
-
### `app` module
26
-
27
-
The entrypoint to the application. It performs initialisation and starts the
28
-
web server.
29
-
30
-
### `app/web` module
31
-
32
-
This module contains the application's middleware stack and any custom types,
33
-
middleware, and other functions that are used by the request handlers.
34
-
35
-
### `app/router` module
36
-
37
-
This module contains the application's request handlers. Or "handler" in this
38
-
case, as there's only one!
39
-
40
-
### `app_test` module
41
-
42
-
The tests for the application.
-13
examples/0-hello-world/gleam.toml
-13
examples/0-hello-world/gleam.toml
-16
examples/0-hello-world/src/app/router.gleam
-16
examples/0-hello-world/src/app/router.gleam
···
1
-
import wisp.{type Request, type Response}
2
-
import gleam/string_builder
3
-
import app/web
4
-
5
-
/// The HTTP request handler- your application!
6
-
///
7
-
pub fn handle_request(req: Request) -> Response {
8
-
// Apply the middleware stack for this request/response.
9
-
use _req <- web.middleware(req)
10
-
11
-
// Later we'll use templates, but for now a string will do.
12
-
let body = string_builder.from_string("<h1>Hello, Joe!</h1>")
13
-
14
-
// Return a 200 OK response with the body and a HTML content type.
15
-
wisp.html_response(body, 200)
16
-
}
-32
examples/0-hello-world/src/app/web.gleam
-32
examples/0-hello-world/src/app/web.gleam
···
1
-
import wisp
2
-
3
-
/// The middleware stack that the request handler uses. The stack is itself a
4
-
/// middleware function!
5
-
///
6
-
/// Middleware wrap each other, so the request travels through the stack from
7
-
/// top to bottom until it reaches the request handler, at which point the
8
-
/// response travels back up through the stack.
9
-
///
10
-
/// The middleware used here are the ones that are suitable for use in your
11
-
/// typical web application.
12
-
///
13
-
pub fn middleware(
14
-
req: wisp.Request,
15
-
handle_request: fn(wisp.Request) -> wisp.Response,
16
-
) -> wisp.Response {
17
-
// Permit browsers to simulate methods other than GET and POST using the
18
-
// `_method` query parameter.
19
-
let req = wisp.method_override(req)
20
-
21
-
// Log information about the request and response.
22
-
use <- wisp.log_request(req)
23
-
24
-
// Return a default 500 response if the request handler crashes.
25
-
use <- wisp.rescue_crashes
26
-
27
-
// Rewrite HEAD requests to GET requests and return an empty body.
28
-
use req <- wisp.handle_head(req)
29
-
30
-
// Handle the request!
31
-
handle_request(req)
32
-
}
-25
examples/0-hello-world/src/app.gleam
-25
examples/0-hello-world/src/app.gleam
···
1
-
import gleam/erlang/process
2
-
import mist
3
-
import wisp
4
-
import app/router
5
-
6
-
pub fn main() {
7
-
// This sets the logger to print INFO level logs, and other sensible defaults
8
-
// for a web application.
9
-
wisp.configure_logger()
10
-
11
-
// Here we generate a secret key, but in a real application you would want to
12
-
// load this from somewhere so that it is not regenerated on every restart.
13
-
let secret_key_base = wisp.random_string(64)
14
-
15
-
// Start the Mist web server.
16
-
let assert Ok(_) =
17
-
wisp.mist_handler(router.handle_request, secret_key_base)
18
-
|> mist.new
19
-
|> mist.port(8000)
20
-
|> mist.start_http
21
-
22
-
// The web server runs in new Erlang process, so put this one to sleep while
23
-
// it works concurrently.
24
-
process.sleep_forever()
25
-
}
-22
examples/0-hello-world/test/app_test.gleam
-22
examples/0-hello-world/test/app_test.gleam
···
1
-
import gleeunit
2
-
import gleeunit/should
3
-
import wisp/testing
4
-
import app/router
5
-
6
-
pub fn main() {
7
-
gleeunit.main()
8
-
}
9
-
10
-
pub fn hello_world_test() {
11
-
let response = router.handle_request(testing.get("/", []))
12
-
13
-
response.status
14
-
|> should.equal(200)
15
-
16
-
response.headers
17
-
|> should.equal([#("content-type", "text/html")])
18
-
19
-
response
20
-
|> testing.string_body
21
-
|> should.equal("<h1>Hello, Joe!</h1>")
22
-
}
+42
examples/00-hello-world/README.md
+42
examples/00-hello-world/README.md
···
1
+
# Wisp Example: Hello, world!
2
+
3
+
```sh
4
+
gleam run # Run the server
5
+
gleam test # Run the tests
6
+
```
7
+
8
+
This example shows a minimal Wisp application, it does nothing but respond with
9
+
a greeting to any request.
10
+
11
+
The project has this structure:
12
+
13
+
```
14
+
├─ src
15
+
│ ├─ app
16
+
│ │ ├─ router.gleam
17
+
│ │ └─ web.gleam
18
+
│ └─ app.gleam
19
+
└─ test
20
+
└── app_test.gleam
21
+
```
22
+
23
+
In your project `app` would be replaced by the name of your application.
24
+
25
+
### `app` module
26
+
27
+
The entrypoint to the application. It performs initialisation and starts the
28
+
web server.
29
+
30
+
### `app/web` module
31
+
32
+
This module contains the application's middleware stack and any custom types,
33
+
middleware, and other functions that are used by the request handlers.
34
+
35
+
### `app/router` module
36
+
37
+
This module contains the application's request handlers. Or "handler" in this
38
+
case, as there's only one!
39
+
40
+
### `app_test` module
41
+
42
+
The tests for the application.
+14
examples/00-hello-world/gleam.toml
+14
examples/00-hello-world/gleam.toml
···
1
+
name = "app"
2
+
version = "1.0.0"
3
+
description = "A Wisp example"
4
+
gleam = ">= 0.32.0"
5
+
6
+
[dependencies]
7
+
gleam_stdlib = "~> 0.30"
8
+
wisp = { path = "../.." }
9
+
gleam_erlang = "~> 0.23"
10
+
mist = ">= 2.0.0 and < 3.0.0"
11
+
12
+
13
+
[dev-dependencies]
14
+
gleeunit = "~> 1.0"
+16
examples/00-hello-world/src/app/router.gleam
+16
examples/00-hello-world/src/app/router.gleam
···
1
+
import app/web
2
+
import gleam/string_tree
3
+
import wisp.{type Request, type Response}
4
+
5
+
/// The HTTP request handler- your application!
6
+
///
7
+
pub fn handle_request(req: Request) -> Response {
8
+
// Apply the middleware stack for this request/response.
9
+
use _req <- web.middleware(req)
10
+
11
+
// Later we'll use templates, but for now a string will do.
12
+
let body = string_tree.from_string("<h1>Hello, Joe!</h1>")
13
+
14
+
// Return a 200 OK response with the body and a HTML content type.
15
+
wisp.html_response(body, 200)
16
+
}
+32
examples/00-hello-world/src/app/web.gleam
+32
examples/00-hello-world/src/app/web.gleam
···
1
+
import wisp
2
+
3
+
/// The middleware stack that the request handler uses. The stack is itself a
4
+
/// middleware function!
5
+
///
6
+
/// Middleware wrap each other, so the request travels through the stack from
7
+
/// top to bottom until it reaches the request handler, at which point the
8
+
/// response travels back up through the stack.
9
+
///
10
+
/// The middleware used here are the ones that are suitable for use in your
11
+
/// typical web application.
12
+
///
13
+
pub fn middleware(
14
+
req: wisp.Request,
15
+
handle_request: fn(wisp.Request) -> wisp.Response,
16
+
) -> wisp.Response {
17
+
// Permit browsers to simulate methods other than GET and POST using the
18
+
// `_method` query parameter.
19
+
let req = wisp.method_override(req)
20
+
21
+
// Log information about the request and response.
22
+
use <- wisp.log_request(req)
23
+
24
+
// Return a default 500 response if the request handler crashes.
25
+
use <- wisp.rescue_crashes
26
+
27
+
// Rewrite HEAD requests to GET requests and return an empty body.
28
+
use req <- wisp.handle_head(req)
29
+
30
+
// Handle the request!
31
+
handle_request(req)
32
+
}
+26
examples/00-hello-world/src/app.gleam
+26
examples/00-hello-world/src/app.gleam
···
1
+
import app/router
2
+
import gleam/erlang/process
3
+
import mist
4
+
import wisp
5
+
import wisp/wisp_mist
6
+
7
+
pub fn main() {
8
+
// This sets the logger to print INFO level logs, and other sensible defaults
9
+
// for a web application.
10
+
wisp.configure_logger()
11
+
12
+
// Here we generate a secret key, but in a real application you would want to
13
+
// load this from somewhere so that it is not regenerated on every restart.
14
+
let secret_key_base = wisp.random_string(64)
15
+
16
+
// Start the Mist web server.
17
+
let assert Ok(_) =
18
+
wisp_mist.handler(router.handle_request, secret_key_base)
19
+
|> mist.new
20
+
|> mist.port(8000)
21
+
|> mist.start_http
22
+
23
+
// The web server runs in new Erlang process, so put this one to sleep while
24
+
// it works concurrently.
25
+
process.sleep_forever()
26
+
}
+22
examples/00-hello-world/test/app_test.gleam
+22
examples/00-hello-world/test/app_test.gleam
···
1
+
import gleeunit
2
+
import gleeunit/should
3
+
import wisp/testing
4
+
import app/router
5
+
6
+
pub fn main() {
7
+
gleeunit.main()
8
+
}
9
+
10
+
pub fn hello_world_test() {
11
+
let response = router.handle_request(testing.get("/", []))
12
+
13
+
response.status
14
+
|> should.equal(200)
15
+
16
+
response.headers
17
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
18
+
19
+
response
20
+
|> testing.string_body
21
+
|> should.equal("<h1>Hello, Joe!</h1>")
22
+
}
+28
examples/01-routing/README.md
+28
examples/01-routing/README.md
···
1
+
# Wisp Example: Routing
2
+
3
+
```sh
4
+
gleam run # Run the server
5
+
gleam test # Run the tests
6
+
```
7
+
8
+
This example shows how to route requests to different handlers based on the
9
+
request path and method.
10
+
11
+
This example is based off of the ["Hello, World!" example][hello], so read that
12
+
one first. The additions are detailed here and commented in the code.
13
+
14
+
[hello]: https://github.com/lpil/wisp/tree/main/examples/00-hello-world
15
+
16
+
### `app/router` module
17
+
18
+
The `handle_request` function now pattern matches on the request and calls other
19
+
request handler functions depending on where the request should go.
20
+
21
+
### `app_test` module
22
+
23
+
Tests have been added for each of the routes. The `wisp/testing` module is used
24
+
to create different requests to test the application with.
25
+
26
+
### Other files
27
+
28
+
No changes have been made to the other files.
+14
examples/01-routing/gleam.toml
+14
examples/01-routing/gleam.toml
···
1
+
name = "app"
2
+
version = "1.0.0"
3
+
description = "A Wisp example"
4
+
gleam = ">= 0.32.0"
5
+
6
+
[dependencies]
7
+
gleam_stdlib = "~> 0.30"
8
+
wisp = { path = "../.." }
9
+
gleam_erlang = "~> 0.23"
10
+
mist = ">= 2.0.0 and < 3.0.0"
11
+
gleam_http = "~> 3.5"
12
+
13
+
[dev-dependencies]
14
+
gleeunit = "~> 1.0"
+72
examples/01-routing/src/app/router.gleam
+72
examples/01-routing/src/app/router.gleam
···
1
+
import app/web
2
+
import gleam/http.{Get, Post}
3
+
import gleam/string_tree
4
+
import wisp.{type Request, type Response}
5
+
6
+
pub fn handle_request(req: Request) -> Response {
7
+
use req <- web.middleware(req)
8
+
9
+
// Wisp doesn't have a special router abstraction, instead we recommend using
10
+
// regular old pattern matching. This is faster than a router, is type safe,
11
+
// and means you don't have to learn or be limited by a special DSL.
12
+
//
13
+
case wisp.path_segments(req) {
14
+
// This matches `/`.
15
+
[] -> home_page(req)
16
+
17
+
// This matches `/comments`.
18
+
["comments"] -> comments(req)
19
+
20
+
// This matches `/comments/:id`.
21
+
// The `id` segment is bound to a variable and passed to the handler.
22
+
["comments", id] -> show_comment(req, id)
23
+
24
+
// This matches all other paths.
25
+
_ -> wisp.not_found()
26
+
}
27
+
}
28
+
29
+
fn home_page(req: Request) -> Response {
30
+
// The home page can only be accessed via GET requests, so this middleware is
31
+
// used to return a 405: Method Not Allowed response for all other methods.
32
+
use <- wisp.require_method(req, Get)
33
+
34
+
let html = string_tree.from_string("Hello, Joe!")
35
+
wisp.ok()
36
+
|> wisp.html_body(html)
37
+
}
38
+
39
+
fn comments(req: Request) -> Response {
40
+
// This handler for `/comments` can respond to both GET and POST requests,
41
+
// so we pattern match on the method here.
42
+
case req.method {
43
+
Get -> list_comments()
44
+
Post -> create_comment(req)
45
+
_ -> wisp.method_not_allowed([Get, Post])
46
+
}
47
+
}
48
+
49
+
fn list_comments() -> Response {
50
+
// In a later example we'll show how to read from a database.
51
+
let html = string_tree.from_string("Comments!")
52
+
wisp.ok()
53
+
|> wisp.html_body(html)
54
+
}
55
+
56
+
fn create_comment(_req: Request) -> Response {
57
+
// In a later example we'll show how to parse data from the request body.
58
+
let html = string_tree.from_string("Created")
59
+
wisp.created()
60
+
|> wisp.html_body(html)
61
+
}
62
+
63
+
fn show_comment(req: Request, id: String) -> Response {
64
+
use <- wisp.require_method(req, Get)
65
+
66
+
// The `id` path parameter has been passed to this function, so we could use
67
+
// it to look up a comment in a database.
68
+
// For now we'll just include in the response body.
69
+
let html = string_tree.from_string("Comment with id " <> id)
70
+
wisp.ok()
71
+
|> wisp.html_body(html)
72
+
}
+13
examples/01-routing/src/app/web.gleam
+13
examples/01-routing/src/app/web.gleam
···
1
+
import wisp
2
+
3
+
pub fn middleware(
4
+
req: wisp.Request,
5
+
handle_request: fn(wisp.Request) -> wisp.Response,
6
+
) -> wisp.Response {
7
+
let req = wisp.method_override(req)
8
+
use <- wisp.log_request(req)
9
+
use <- wisp.rescue_crashes
10
+
use req <- wisp.handle_head(req)
11
+
12
+
handle_request(req)
13
+
}
+18
examples/01-routing/src/app.gleam
+18
examples/01-routing/src/app.gleam
···
1
+
import app/router
2
+
import gleam/erlang/process
3
+
import mist
4
+
import wisp
5
+
import wisp/wisp_mist
6
+
7
+
pub fn main() {
8
+
wisp.configure_logger()
9
+
let secret_key_base = wisp.random_string(64)
10
+
11
+
let assert Ok(_) =
12
+
wisp_mist.handler(router.handle_request, secret_key_base)
13
+
|> mist.new
14
+
|> mist.port(8000)
15
+
|> mist.start_http
16
+
17
+
process.sleep_forever()
18
+
}
+75
examples/01-routing/test/app_test.gleam
+75
examples/01-routing/test/app_test.gleam
···
1
+
import gleeunit
2
+
import gleeunit/should
3
+
import wisp/testing
4
+
import app/router
5
+
6
+
pub fn main() {
7
+
gleeunit.main()
8
+
}
9
+
10
+
pub fn get_home_page_test() {
11
+
let request = testing.get("/", [])
12
+
let response = router.handle_request(request)
13
+
14
+
response.status
15
+
|> should.equal(200)
16
+
17
+
response.headers
18
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
19
+
20
+
response
21
+
|> testing.string_body
22
+
|> should.equal("Hello, Joe!")
23
+
}
24
+
25
+
pub fn post_home_page_test() {
26
+
let request = testing.post("/", [], "a body")
27
+
let response = router.handle_request(request)
28
+
response.status
29
+
|> should.equal(405)
30
+
}
31
+
32
+
pub fn page_not_found_test() {
33
+
let request = testing.get("/nothing-here", [])
34
+
let response = router.handle_request(request)
35
+
response.status
36
+
|> should.equal(404)
37
+
}
38
+
39
+
pub fn get_comments_test() {
40
+
let request = testing.get("/comments", [])
41
+
let response = router.handle_request(request)
42
+
response.status
43
+
|> should.equal(200)
44
+
}
45
+
46
+
pub fn post_comments_test() {
47
+
let request = testing.post("/comments", [], "")
48
+
let response = router.handle_request(request)
49
+
response.status
50
+
|> should.equal(201)
51
+
}
52
+
53
+
pub fn delete_comments_test() {
54
+
let request = testing.delete("/comments", [], "")
55
+
let response = router.handle_request(request)
56
+
response.status
57
+
|> should.equal(405)
58
+
}
59
+
60
+
pub fn get_comment_test() {
61
+
let request = testing.get("/comments/123", [])
62
+
let response = router.handle_request(request)
63
+
response.status
64
+
|> should.equal(200)
65
+
response
66
+
|> testing.string_body
67
+
|> should.equal("Comment with id 123")
68
+
}
69
+
70
+
pub fn delete_comment_test() {
71
+
let request = testing.delete("/comments/123", [], "")
72
+
let response = router.handle_request(request)
73
+
response.status
74
+
|> should.equal(405)
75
+
}
+29
examples/02-working-with-form-data/README.md
+29
examples/02-working-with-form-data/README.md
···
1
+
# Wisp Example: Working with form data
2
+
3
+
```sh
4
+
gleam run # Run the server
5
+
gleam test # Run the tests
6
+
```
7
+
8
+
This example shows how to read urlencoded and multipart formdata from a request
9
+
10
+
This example is based off of the ["Hello, World!" example][hello], and uses
11
+
concepts from the [routing example][routing] so read those first. The additions
12
+
are detailed here and commented in the code.
13
+
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
+
17
+
### `app/router` module
18
+
19
+
The `handle_request` function has been updated to read the form data from the
20
+
request body and make use of values from it.
21
+
22
+
### `app_test` module
23
+
24
+
Tests have been added that send requests with form data bodies and check that
25
+
the expected response is returned.
26
+
27
+
### Other files
28
+
29
+
No changes have been made to the other files.
+14
examples/02-working-with-form-data/gleam.toml
+14
examples/02-working-with-form-data/gleam.toml
···
1
+
name = "app"
2
+
version = "1.0.0"
3
+
description = "A Wisp example"
4
+
gleam = ">= 0.32.0"
5
+
6
+
[dependencies]
7
+
gleam_stdlib = "~> 0.30"
8
+
wisp = { path = "../.." }
9
+
gleam_erlang = "~> 0.23"
10
+
mist = ">= 2.0.0 and < 3.0.0"
11
+
gleam_http = "~> 3.5"
12
+
13
+
[dev-dependencies]
14
+
gleeunit = "~> 1.0"
+69
examples/02-working-with-form-data/src/app/router.gleam
+69
examples/02-working-with-form-data/src/app/router.gleam
···
1
+
import app/web
2
+
import gleam/http.{Get, Post}
3
+
import gleam/list
4
+
import gleam/result
5
+
import gleam/string_tree
6
+
import wisp.{type Request, type Response}
7
+
8
+
pub fn handle_request(req: Request) -> Response {
9
+
use req <- web.middleware(req)
10
+
11
+
// For GET requests, show the form,
12
+
// for POST requests we use the data from the form
13
+
case req.method {
14
+
Get -> show_form()
15
+
Post -> handle_form_submission(req)
16
+
_ -> wisp.method_not_allowed(allowed: [Get, Post])
17
+
}
18
+
}
19
+
20
+
pub fn show_form() -> Response {
21
+
// In a larger application a template library or HTML form library might
22
+
// be used here instead of a string literal.
23
+
let html =
24
+
string_tree.from_string(
25
+
"<form method='post'>
26
+
<label>Title:
27
+
<input type='text' name='title'>
28
+
</label>
29
+
<label>Name:
30
+
<input type='text' name='name'>
31
+
</label>
32
+
<input type='submit' value='Submit'>
33
+
</form>",
34
+
)
35
+
wisp.ok()
36
+
|> wisp.html_body(html)
37
+
}
38
+
39
+
pub fn handle_form_submission(req: Request) -> Response {
40
+
// This middleware parses a `wisp.FormData` from the request body.
41
+
// It returns an error response if the body is not valid form data, or
42
+
// if the content-type is not `application/x-www-form-urlencoded` or
43
+
// `multipart/form-data`, or if the body is too large.
44
+
use formdata <- wisp.require_form(req)
45
+
46
+
// The list and result module are used here to extract the values from the
47
+
// form data.
48
+
// Alternatively you could also pattern match on the list of values (they are
49
+
// sorted into alphabetical order), or use a HTML form library.
50
+
let result = {
51
+
use title <- result.try(list.key_find(formdata.values, "title"))
52
+
use name <- result.try(list.key_find(formdata.values, "name"))
53
+
let greeting =
54
+
"Hi, " <> wisp.escape_html(title) <> " " <> wisp.escape_html(name) <> "!"
55
+
Ok(greeting)
56
+
}
57
+
58
+
// An appropriate response is returned depending on whether the form data
59
+
// could be successfully handled or not.
60
+
case result {
61
+
Ok(content) -> {
62
+
wisp.ok()
63
+
|> wisp.html_body(string_tree.from_string(content))
64
+
}
65
+
Error(_) -> {
66
+
wisp.bad_request()
67
+
}
68
+
}
69
+
}
+12
examples/02-working-with-form-data/src/app/web.gleam
+12
examples/02-working-with-form-data/src/app/web.gleam
···
1
+
import wisp
2
+
3
+
pub fn middleware(
4
+
req: wisp.Request,
5
+
handle_request: fn(wisp.Request) -> wisp.Response,
6
+
) -> wisp.Response {
7
+
let req = wisp.method_override(req)
8
+
use <- wisp.log_request(req)
9
+
use <- wisp.rescue_crashes
10
+
use req <- wisp.handle_head(req)
11
+
handle_request(req)
12
+
}
+18
examples/02-working-with-form-data/src/app.gleam
+18
examples/02-working-with-form-data/src/app.gleam
···
1
+
import app/router
2
+
import gleam/erlang/process
3
+
import mist
4
+
import wisp
5
+
import wisp/wisp_mist
6
+
7
+
pub fn main() {
8
+
wisp.configure_logger()
9
+
let secret_key_base = wisp.random_string(64)
10
+
11
+
let assert Ok(_) =
12
+
wisp_mist.handler(router.handle_request, secret_key_base)
13
+
|> mist.new
14
+
|> mist.port(8000)
15
+
|> mist.start_http
16
+
17
+
process.sleep_forever()
18
+
}
+63
examples/02-working-with-form-data/test/app_test.gleam
+63
examples/02-working-with-form-data/test/app_test.gleam
···
1
+
import gleeunit
2
+
import gleeunit/should
3
+
import gleam/string
4
+
import wisp/testing
5
+
import app/router
6
+
7
+
pub fn main() {
8
+
gleeunit.main()
9
+
}
10
+
11
+
pub fn view_form_test() {
12
+
let response = router.handle_request(testing.get("/", []))
13
+
14
+
response.status
15
+
|> should.equal(200)
16
+
17
+
response.headers
18
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
19
+
20
+
response
21
+
|> testing.string_body
22
+
|> string.contains("<form method='post'>")
23
+
|> should.equal(True)
24
+
}
25
+
26
+
pub fn submit_wrong_content_type_test() {
27
+
let response = router.handle_request(testing.post("/", [], ""))
28
+
29
+
response.status
30
+
|> should.equal(415)
31
+
32
+
response.headers
33
+
|> should.equal([
34
+
#("accept", "application/x-www-form-urlencoded, multipart/form-data"),
35
+
])
36
+
}
37
+
38
+
pub fn submit_missing_parameters_test() {
39
+
// The `METHOD_form` functions are used to create a request with a
40
+
// `x-www-form-urlencoded` body, with the appropriate `content-type` header.
41
+
let response =
42
+
testing.post_form("/", [], [])
43
+
|> router.handle_request()
44
+
45
+
response.status
46
+
|> should.equal(400)
47
+
}
48
+
49
+
pub fn submit_successful_test() {
50
+
let response =
51
+
testing.post_form("/", [], [#("title", "Captain"), #("name", "Caveman")])
52
+
|> router.handle_request()
53
+
54
+
response.status
55
+
|> should.equal(200)
56
+
57
+
response.headers
58
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
59
+
60
+
response
61
+
|> testing.string_body
62
+
|> should.equal("Hi, Captain Caveman!")
63
+
}
+35
examples/03-working-with-json/README.md
+35
examples/03-working-with-json/README.md
···
1
+
# Wisp Example: Working with JSON
2
+
3
+
```sh
4
+
gleam run # Run the server
5
+
gleam test # Run the tests
6
+
```
7
+
8
+
This example shows how to read JSON from a request and return JSON in the
9
+
response.
10
+
11
+
This example is based off of the ["Hello, World!" example][hello], and uses
12
+
concepts from the [routing example][routing] so read those first. The additions
13
+
are detailed here and commented in the code.
14
+
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
+
18
+
### `gleam.toml` file
19
+
20
+
The `gleam_json` JSON package has been added as a dependency.
21
+
22
+
### `app/router` module
23
+
24
+
The `handle_request` function has been updated to read JSON from the
25
+
request body, decode it using the Gleam standard library, and return JSON
26
+
back to the client.
27
+
28
+
### `app_test` module
29
+
30
+
Tests have been added that send requests with JSON bodies and check that the
31
+
expected response is returned.
32
+
33
+
### Other files
34
+
35
+
No changes have been made to the other files.
+15
examples/03-working-with-json/gleam.toml
+15
examples/03-working-with-json/gleam.toml
···
1
+
name = "app"
2
+
version = "1.0.0"
3
+
description = "A Wisp example"
4
+
gleam = ">= 0.32.0"
5
+
6
+
[dependencies]
7
+
gleam_stdlib = "~> 0.30"
8
+
wisp = { path = "../.." }
9
+
gleam_json = "~> 0.6"
10
+
gleam_erlang = "~> 0.23"
11
+
mist = ">= 2.0.0 and < 3.0.0"
12
+
gleam_http = "~> 3.5"
13
+
14
+
[dev-dependencies]
15
+
gleeunit = "~> 1.0"
+61
examples/03-working-with-json/src/app/router.gleam
+61
examples/03-working-with-json/src/app/router.gleam
···
1
+
import app/web
2
+
import gleam/dynamic.{type Dynamic}
3
+
import gleam/http.{Post}
4
+
import gleam/json
5
+
import gleam/result
6
+
import wisp.{type Request, type Response}
7
+
8
+
// This type is going to be parsed and decoded from the request body.
9
+
pub type Person {
10
+
Person(name: String, is_cool: Bool)
11
+
}
12
+
13
+
// To decode the type we need a dynamic decoder.
14
+
// See the standard library documentation for more information on decoding
15
+
// dynamic values [1].
16
+
//
17
+
// [1]: https://hexdocs.pm/gleam_stdlib/gleam/dynamic.html
18
+
fn decode_person(json: Dynamic) -> Result(Person, dynamic.DecodeErrors) {
19
+
let decoder =
20
+
dynamic.decode2(
21
+
Person,
22
+
dynamic.field("name", dynamic.string),
23
+
dynamic.field("is-cool", dynamic.bool),
24
+
)
25
+
decoder(json)
26
+
}
27
+
28
+
pub fn handle_request(req: Request) -> Response {
29
+
use req <- web.middleware(req)
30
+
use <- wisp.require_method(req, Post)
31
+
32
+
// This middleware parses a `Dynamic` value from the request body.
33
+
// It returns an error response if the body is not valid JSON, or
34
+
// if the content-type is not `application/json`, or if the body
35
+
// is too large.
36
+
use json <- wisp.require_json(req)
37
+
38
+
let result = {
39
+
// The dynamic value can be decoded into a `Person` value.
40
+
use person <- result.try(decode_person(json))
41
+
42
+
// And then a JSON response can be created from the person.
43
+
let object =
44
+
json.object([
45
+
#("name", json.string(person.name)),
46
+
#("is-cool", json.bool(person.is_cool)),
47
+
#("saved", json.bool(True)),
48
+
])
49
+
Ok(json.to_string_builder(object))
50
+
}
51
+
52
+
// An appropriate response is returned depending on whether the JSON could be
53
+
// successfully handled or not.
54
+
case result {
55
+
Ok(json) -> wisp.json_response(json, 201)
56
+
57
+
// In a real application we would probably want to return some JSON error
58
+
// object, but for this example we'll just return an empty response.
59
+
Error(_) -> wisp.unprocessable_entity()
60
+
}
61
+
}
+13
examples/03-working-with-json/src/app/web.gleam
+13
examples/03-working-with-json/src/app/web.gleam
···
1
+
import wisp
2
+
3
+
pub fn middleware(
4
+
req: wisp.Request,
5
+
handle_request: fn(wisp.Request) -> wisp.Response,
6
+
) -> wisp.Response {
7
+
let req = wisp.method_override(req)
8
+
use <- wisp.log_request(req)
9
+
use <- wisp.rescue_crashes
10
+
use req <- wisp.handle_head(req)
11
+
12
+
handle_request(req)
13
+
}
+18
examples/03-working-with-json/src/app.gleam
+18
examples/03-working-with-json/src/app.gleam
···
1
+
import app/router
2
+
import gleam/erlang/process
3
+
import mist
4
+
import wisp
5
+
import wisp/wisp_mist
6
+
7
+
pub fn main() {
8
+
wisp.configure_logger()
9
+
let secret_key_base = wisp.random_string(64)
10
+
11
+
let assert Ok(_) =
12
+
wisp_mist.handler(router.handle_request, secret_key_base)
13
+
|> mist.new
14
+
|> mist.port(8000)
15
+
|> mist.start_http
16
+
17
+
process.sleep_forever()
18
+
}
+57
examples/03-working-with-json/test/app_test.gleam
+57
examples/03-working-with-json/test/app_test.gleam
···
1
+
import app/router
2
+
import gleam/json
3
+
import gleeunit
4
+
import gleeunit/should
5
+
import wisp/testing
6
+
7
+
pub fn main() {
8
+
gleeunit.main()
9
+
}
10
+
11
+
pub fn get_test() {
12
+
let response = router.handle_request(testing.get("/", []))
13
+
14
+
response.status
15
+
|> should.equal(405)
16
+
}
17
+
18
+
pub fn submit_wrong_content_type_test() {
19
+
let response = router.handle_request(testing.post("/", [], ""))
20
+
21
+
response.status
22
+
|> should.equal(415)
23
+
24
+
response.headers
25
+
|> should.equal([#("accept", "application/json")])
26
+
}
27
+
28
+
pub fn submit_missing_parameters_test() {
29
+
let json = json.object([#("name", json.string("Joe"))])
30
+
31
+
// The `METHOD_json` functions are used to create a request with a JSON body,
32
+
// with the appropriate `content-type` header.
33
+
let response =
34
+
testing.post_json("/", [], json)
35
+
|> router.handle_request()
36
+
37
+
response.status
38
+
|> should.equal(422)
39
+
}
40
+
41
+
pub fn submit_successful_test() {
42
+
let json =
43
+
json.object([#("name", json.string("Joe")), #("is-cool", json.bool(True))])
44
+
let response =
45
+
testing.post_json("/", [], json)
46
+
|> router.handle_request()
47
+
48
+
response.status
49
+
|> should.equal(201)
50
+
51
+
response.headers
52
+
|> should.equal([#("content-type", "application/json; charset=utf-8")])
53
+
54
+
response
55
+
|> testing.string_body
56
+
|> should.equal("{\"name\":\"Joe\",\"is-cool\":true,\"saved\":true}")
57
+
}
+36
examples/04-working-with-other-formats/README.md
+36
examples/04-working-with-other-formats/README.md
···
1
+
# Wisp Example: Working with other formats
2
+
3
+
```sh
4
+
gleam run # Run the server
5
+
gleam test # Run the tests
6
+
```
7
+
8
+
This example shows how to read and return formats that do not have special
9
+
support in Wisp. In this case we'll use CSV, but the same techniques can be used
10
+
for any format.
11
+
12
+
This example is based off of the ["Hello, World!" example][hello], and uses
13
+
concepts from the [routing example][routing] so read those first. The additions
14
+
are detailed here and commented in the code.
15
+
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
+
19
+
### `gleam.toml` file
20
+
21
+
The `gsv` CSV package has been added as a dependency.
22
+
23
+
### `app/router` module
24
+
25
+
The `handle_request` function has been updated to read a string from the
26
+
request body, decode it using the `gsv` library, and return some CSV data
27
+
back to the client.
28
+
29
+
### `app_test` module
30
+
31
+
Tests have been added that send requests with CSV bodies and check that the
32
+
expected response is returned.
33
+
34
+
### Other files
35
+
36
+
No changes have been made to the other files.
+15
examples/04-working-with-other-formats/gleam.toml
+15
examples/04-working-with-other-formats/gleam.toml
···
1
+
name = "app"
2
+
version = "1.0.0"
3
+
description = "A Wisp example"
4
+
gleam = ">= 0.32.0"
5
+
6
+
[dependencies]
7
+
gleam_stdlib = "~> 0.30"
8
+
wisp = { path = "../.." }
9
+
gsv = "~> 1.0"
10
+
gleam_erlang = "~> 0.23"
11
+
mist = ">= 2.0.0 and < 3.0.0"
12
+
gleam_http = "~> 3.5"
13
+
14
+
[dev-dependencies]
15
+
gleeunit = "~> 1.0"
+61
examples/04-working-with-other-formats/src/app/router.gleam
+61
examples/04-working-with-other-formats/src/app/router.gleam
···
1
+
import app/web
2
+
import gleam/int
3
+
import gleam/http.{Post}
4
+
import gleam/list
5
+
import gleam/result
6
+
import gleam/string
7
+
import gsv
8
+
import wisp.{type Request, type Response}
9
+
10
+
pub fn handle_request(req: Request) -> Response {
11
+
use req <- web.middleware(req)
12
+
use <- wisp.require_method(req, Post)
13
+
14
+
// We want to accept only CSV content, so we use this middleware to check the
15
+
// correct content type header is set, and return an error response if not.
16
+
use <- wisp.require_content_type(req, "text/csv")
17
+
18
+
// This middleware reads the body of the request and returns it as a string,
19
+
// erroring if the body is not valid UTF-8, or if the body is too large.
20
+
//
21
+
// If you want to get a bit-string and don't need specifically UTF-8 encoded
22
+
// data then the `wisp.require_bit_string_body` middleware can be used
23
+
// instead.
24
+
use body <- wisp.require_string_body(req)
25
+
26
+
// Now that we have the body we can parse and process it.
27
+
// In this case we expect it to be a CSV file with a header row, but in your
28
+
// application it could be XML, protobuf, or anything else.
29
+
let result = {
30
+
// The GSV library is used to parse the CSV.
31
+
use rows <- result.try(gsv.to_lists(body))
32
+
33
+
// Get the first row, which is the header row.
34
+
use headers <- result.try(list.first(rows))
35
+
36
+
// Define the table we want to send back to the client.
37
+
let table = [
38
+
["headers", "row-count"],
39
+
[string.join(headers, ","), int.to_string(list.length(rows) - 1)],
40
+
]
41
+
42
+
// Convert the table to CSV.
43
+
let csv = gsv.from_lists(table, ",", gsv.Unix)
44
+
45
+
Ok(csv)
46
+
}
47
+
48
+
// An appropriate response is returned depending on whether the CSV could be
49
+
// successfully handled or not.
50
+
case result {
51
+
Ok(csv) -> {
52
+
wisp.ok()
53
+
|> wisp.set_header("content-type", "text/csv")
54
+
|> wisp.string_body(csv)
55
+
}
56
+
57
+
Error(_error) -> {
58
+
wisp.unprocessable_entity()
59
+
}
60
+
}
61
+
}
+13
examples/04-working-with-other-formats/src/app/web.gleam
+13
examples/04-working-with-other-formats/src/app/web.gleam
···
1
+
import wisp
2
+
3
+
pub fn middleware(
4
+
req: wisp.Request,
5
+
handle_request: fn(wisp.Request) -> wisp.Response,
6
+
) -> wisp.Response {
7
+
let req = wisp.method_override(req)
8
+
use <- wisp.log_request(req)
9
+
use <- wisp.rescue_crashes
10
+
use req <- wisp.handle_head(req)
11
+
12
+
handle_request(req)
13
+
}
+18
examples/04-working-with-other-formats/src/app.gleam
+18
examples/04-working-with-other-formats/src/app.gleam
···
1
+
import app/router
2
+
import gleam/erlang/process
3
+
import mist
4
+
import wisp
5
+
import wisp/wisp_mist
6
+
7
+
pub fn main() {
8
+
wisp.configure_logger()
9
+
let secret_key_base = wisp.random_string(64)
10
+
11
+
let assert Ok(_) =
12
+
wisp_mist.handler(router.handle_request, secret_key_base)
13
+
|> mist.new
14
+
|> mist.port(8000)
15
+
|> mist.start_http
16
+
17
+
process.sleep_forever()
18
+
}
+44
examples/04-working-with-other-formats/test/app_test.gleam
+44
examples/04-working-with-other-formats/test/app_test.gleam
···
1
+
import app/router
2
+
import gleeunit
3
+
import gleeunit/should
4
+
import wisp/testing
5
+
6
+
pub fn main() {
7
+
gleeunit.main()
8
+
}
9
+
10
+
pub fn get_test() {
11
+
let response = router.handle_request(testing.get("/", []))
12
+
13
+
response.status
14
+
|> should.equal(405)
15
+
}
16
+
17
+
pub fn post_wrong_content_type_test() {
18
+
let response = router.handle_request(testing.post("/", [], ""))
19
+
20
+
response.status
21
+
|> should.equal(415)
22
+
23
+
response.headers
24
+
|> should.equal([#("accept", "text/csv")])
25
+
}
26
+
27
+
pub fn post_successful_test() {
28
+
let csv = "name,is-cool\nJoe,true\nJosé,true\n"
29
+
30
+
let response =
31
+
testing.post("/", [], csv)
32
+
|> testing.set_header("content-type", "text/csv")
33
+
|> router.handle_request()
34
+
35
+
response.status
36
+
|> should.equal(200)
37
+
38
+
response.headers
39
+
|> should.equal([#("content-type", "text/csv")])
40
+
41
+
response
42
+
|> testing.string_body
43
+
|> should.equal("headers,row-count\n\"name,is-cool\",2")
44
+
}
+50
examples/05-using-a-database/README.md
+50
examples/05-using-a-database/README.md
···
1
+
# Wisp Example: Using a database
2
+
3
+
```sh
4
+
gleam run # Run the server
5
+
gleam test # Run the tests
6
+
```
7
+
8
+
This example shows how to use a database, using a `Context` type to hold the
9
+
database connection.
10
+
11
+
This example is based off of the ["working with JSON" example][json], so read
12
+
that first. The additions are detailed here and commented in the code.
13
+
14
+
[json]: https://github.com/lpil/wisp/tree/main/examples/03-working-with-json
15
+
16
+
### `gleam.toml` file
17
+
18
+
The `tiny_database` package has been added as a dependency. In a real project
19
+
you would like use a proper database such as Postgres or SQLite.
20
+
21
+
### `app/web` module
22
+
23
+
A new `Context` type has been created to hold the database connection.
24
+
25
+
### `app` module
26
+
27
+
The `main` function now starts by creating a database connection and passing it
28
+
to the handler function in a `Context` record.
29
+
30
+
### `app/router` module
31
+
32
+
The `handle_request` function has been updated to route requests to functions in
33
+
the new `app/web/people` module.
34
+
35
+
### `app/web/people` module
36
+
37
+
This module has been created to hold all the functions for working with the
38
+
"people" feature, including their request handlers.
39
+
40
+
### `app_test` module
41
+
42
+
The `with_context` function has been added to create a `Context` record with a
43
+
database connection, and to setup the database.
44
+
45
+
The tests have been updated to verify that the application saves and retrieves
46
+
the data correctly.
47
+
48
+
### Other files
49
+
50
+
No changes have been made to the other files.
+16
examples/05-using-a-database/gleam.toml
+16
examples/05-using-a-database/gleam.toml
···
1
+
name = "app"
2
+
version = "1.0.0"
3
+
description = "A Wisp example"
4
+
gleam = ">= 0.32.0"
5
+
6
+
[dependencies]
7
+
gleam_stdlib = "~> 0.30"
8
+
wisp = { path = "../.." }
9
+
gleam_json = "~> 0.6"
10
+
tiny_database = { path = "../utilities/tiny_database" }
11
+
gleam_erlang = "~> 0.23"
12
+
mist = ">= 2.0.0 and < 3.0.0"
13
+
gleam_http = "~> 3.5"
14
+
15
+
[dev-dependencies]
16
+
gleeunit = "~> 1.0"
+19
examples/05-using-a-database/src/app/router.gleam
+19
examples/05-using-a-database/src/app/router.gleam
···
1
+
import app/web.{type Context}
2
+
import app/web/people
3
+
import wisp.{type Request, type Response}
4
+
5
+
pub fn handle_request(req: Request, ctx: Context) -> Response {
6
+
use req <- web.middleware(req)
7
+
8
+
// A new `app/web/people` module now contains the handlers and other functions
9
+
// relating to the People feature of the application.
10
+
//
11
+
// The router module now only deals with routing, and dispatches to the
12
+
// feature modules for handling requests.
13
+
//
14
+
case wisp.path_segments(req) {
15
+
["people"] -> people.all(req, ctx)
16
+
["people", id] -> people.one(req, ctx, id)
17
+
_ -> wisp.not_found()
18
+
}
19
+
}
+152
examples/05-using-a-database/src/app/web/people.gleam
+152
examples/05-using-a-database/src/app/web/people.gleam
···
1
+
import app/web.{type Context}
2
+
import gleam/dict
3
+
import gleam/dynamic.{type Dynamic}
4
+
import gleam/http.{Get, Post}
5
+
import gleam/json
6
+
import gleam/result.{try}
7
+
import tiny_database
8
+
import wisp.{type Request, type Response}
9
+
10
+
// This request handler is used for requests to `/people`.
11
+
//
12
+
pub fn all(req: Request, ctx: Context) -> Response {
13
+
// Dispatch to the appropriate handler based on the HTTP method.
14
+
case req.method {
15
+
Get -> list_people(ctx)
16
+
Post -> create_person(req, ctx)
17
+
_ -> wisp.method_not_allowed([Get, Post])
18
+
}
19
+
}
20
+
21
+
// This request handler is used for requests to `/people/:id`.
22
+
//
23
+
pub fn one(req: Request, ctx: Context, id: String) -> Response {
24
+
// Dispatch to the appropriate handler based on the HTTP method.
25
+
case req.method {
26
+
Get -> read_person(ctx, id)
27
+
_ -> wisp.method_not_allowed([Get])
28
+
}
29
+
}
30
+
31
+
pub type Person {
32
+
Person(name: String, favourite_colour: String)
33
+
}
34
+
35
+
// This handler returns a list of all the people in the database, in JSON
36
+
// format.
37
+
//
38
+
pub fn list_people(ctx: Context) -> Response {
39
+
let result = {
40
+
// Get all the ids from the database.
41
+
use ids <- try(tiny_database.list(ctx.db))
42
+
43
+
// Convert the ids into a JSON array of objects.
44
+
Ok(
45
+
json.to_string_builder(
46
+
json.object([
47
+
#(
48
+
"people",
49
+
json.array(ids, fn(id) { json.object([#("id", json.string(id))]) }),
50
+
),
51
+
]),
52
+
),
53
+
)
54
+
}
55
+
56
+
case result {
57
+
// When everything goes well we return a 200 response with the JSON.
58
+
Ok(json) -> wisp.json_response(json, 200)
59
+
60
+
// In a later example we will see how to return specific errors to the user
61
+
// depending on what went wrong. For now we will just return a 500 error.
62
+
Error(Nil) -> wisp.internal_server_error()
63
+
}
64
+
}
65
+
66
+
pub fn create_person(req: Request, ctx: Context) -> Response {
67
+
// Read the JSON from the request body.
68
+
use json <- wisp.require_json(req)
69
+
70
+
let result = {
71
+
// Decode the JSON into a Person record.
72
+
use person <- try(decode_person(json))
73
+
74
+
// Save the person to the database.
75
+
use id <- try(save_to_database(ctx.db, person))
76
+
77
+
// Construct a JSON payload with the id of the newly created person.
78
+
Ok(json.to_string_builder(json.object([#("id", json.string(id))])))
79
+
}
80
+
81
+
// Return an appropriate response depending on whether everything went well or
82
+
// if there was an error.
83
+
case result {
84
+
Ok(json) -> wisp.json_response(json, 201)
85
+
Error(Nil) -> wisp.unprocessable_entity()
86
+
}
87
+
}
88
+
89
+
pub fn read_person(ctx: Context, id: String) -> Response {
90
+
let result = {
91
+
// Read the person with the given id from the database.
92
+
use person <- try(read_from_database(ctx.db, id))
93
+
94
+
// Construct a JSON payload with the person's details.
95
+
Ok(
96
+
json.to_string_builder(
97
+
json.object([
98
+
#("id", json.string(id)),
99
+
#("name", json.string(person.name)),
100
+
#("favourite-colour", json.string(person.favourite_colour)),
101
+
]),
102
+
),
103
+
)
104
+
}
105
+
106
+
// Return an appropriate response.
107
+
case result {
108
+
Ok(json) -> wisp.json_response(json, 200)
109
+
Error(Nil) -> wisp.not_found()
110
+
}
111
+
}
112
+
113
+
fn decode_person(json: Dynamic) -> Result(Person, Nil) {
114
+
let decoder =
115
+
dynamic.decode2(
116
+
Person,
117
+
dynamic.field("name", dynamic.string),
118
+
dynamic.field("favourite-colour", dynamic.string),
119
+
)
120
+
let result = decoder(json)
121
+
122
+
// In this example we are not going to be reporting specific errors to the
123
+
// user, so we can discard the error and replace it with Nil.
124
+
result
125
+
|> result.replace_error(Nil)
126
+
}
127
+
128
+
/// Save a person to the database and return the id of the newly created record.
129
+
pub fn save_to_database(
130
+
db: tiny_database.Connection,
131
+
person: Person,
132
+
) -> Result(String, Nil) {
133
+
// In a real application you might use a database client with some SQL here.
134
+
// Instead we create a simple dict and save that.
135
+
let data =
136
+
dict.from_list([
137
+
#("name", person.name),
138
+
#("favourite-colour", person.favourite_colour),
139
+
])
140
+
tiny_database.insert(db, data)
141
+
}
142
+
143
+
pub fn read_from_database(
144
+
db: tiny_database.Connection,
145
+
id: String,
146
+
) -> Result(Person, Nil) {
147
+
// In a real application you might use a database client with some SQL here.
148
+
use data <- try(tiny_database.read(db, id))
149
+
use name <- try(dict.get(data, "name"))
150
+
use favourite_colour <- try(dict.get(data, "favourite-colour"))
151
+
Ok(Person(name, favourite_colour))
152
+
}
+25
examples/05-using-a-database/src/app/web.gleam
+25
examples/05-using-a-database/src/app/web.gleam
···
1
+
import wisp
2
+
import tiny_database
3
+
4
+
// A new Context type, which holds any additional data that the request handlers
5
+
// need in addition to the request.
6
+
//
7
+
// Here it is holding a database connection, but it could hold anything else
8
+
// such as API keys, IO performing functions (so they can be swapped out in
9
+
// tests for mock implementations), configuration, and so on.
10
+
//
11
+
pub type Context {
12
+
Context(db: tiny_database.Connection)
13
+
}
14
+
15
+
pub fn middleware(
16
+
req: wisp.Request,
17
+
handle_request: fn(wisp.Request) -> wisp.Response,
18
+
) -> wisp.Response {
19
+
let req = wisp.method_override(req)
20
+
use <- wisp.log_request(req)
21
+
use <- wisp.rescue_crashes
22
+
use req <- wisp.handle_head(req)
23
+
24
+
handle_request(req)
25
+
}
+34
examples/05-using-a-database/src/app.gleam
+34
examples/05-using-a-database/src/app.gleam
···
1
+
import app/router
2
+
import app/web
3
+
import gleam/erlang/process
4
+
import mist
5
+
import tiny_database
6
+
import wisp
7
+
import wisp/wisp_mist
8
+
9
+
pub const data_directory = "tmp/data"
10
+
11
+
pub fn main() {
12
+
wisp.configure_logger()
13
+
let secret_key_base = wisp.random_string(64)
14
+
15
+
// A database creation is created here, when the program starts.
16
+
// This connection is used by all requests.
17
+
use db <- tiny_database.with_connection(data_directory)
18
+
19
+
// A context is constructed to hold the database connection.
20
+
let context = web.Context(db: db)
21
+
22
+
// The handle_request function is partially applied with the context to make
23
+
// the request handler function that only takes a request.
24
+
let handler = router.handle_request(_, context)
25
+
26
+
let assert Ok(_) =
27
+
handler
28
+
|> wisp_mist.handler(secret_key_base)
29
+
|> mist.new
30
+
|> mist.port(8000)
31
+
|> mist.start_http
32
+
33
+
process.sleep_forever()
34
+
}
+108
examples/05-using-a-database/test/app_test.gleam
+108
examples/05-using-a-database/test/app_test.gleam
···
1
+
import app
2
+
import app/router
3
+
import app/web.{type Context, Context}
4
+
import app/web/people.{Person}
5
+
import gleam/json
6
+
import gleeunit
7
+
import gleeunit/should
8
+
import tiny_database
9
+
import wisp/testing
10
+
11
+
pub fn main() {
12
+
gleeunit.main()
13
+
}
14
+
15
+
fn with_context(testcase: fn(Context) -> t) -> t {
16
+
// Create a new database connection for this test
17
+
use db <- tiny_database.with_connection(app.data_directory)
18
+
19
+
// Truncate the database so there is no prexisting data from previous tests
20
+
let assert Ok(_) = tiny_database.truncate(db)
21
+
let context = Context(db: db)
22
+
23
+
// Run the test with the context
24
+
testcase(context)
25
+
}
26
+
27
+
pub fn get_unknown_test() {
28
+
use ctx <- with_context
29
+
let request = testing.get("/", [])
30
+
let response = router.handle_request(request, ctx)
31
+
32
+
response.status
33
+
|> should.equal(404)
34
+
}
35
+
36
+
pub fn list_people_test() {
37
+
use ctx <- with_context
38
+
39
+
let response = router.handle_request(testing.get("/people", []), ctx)
40
+
response.status
41
+
|> should.equal(200)
42
+
response.headers
43
+
|> should.equal([#("content-type", "application/json; charset=utf-8")])
44
+
45
+
// Initially there are no people in the database
46
+
response
47
+
|> testing.string_body
48
+
|> should.equal("{\"people\":[]}")
49
+
50
+
// Create a new person
51
+
let assert Ok(id) = people.save_to_database(ctx.db, Person("Jane", "Red"))
52
+
53
+
// The id of the new person is listed by the API
54
+
let response = router.handle_request(testing.get("/people", []), ctx)
55
+
response
56
+
|> testing.string_body
57
+
|> should.equal("{\"people\":[{\"id\":\"" <> id <> "\"}]}")
58
+
}
59
+
60
+
pub fn create_person_test() {
61
+
use ctx <- with_context
62
+
let json =
63
+
json.object([
64
+
#("name", json.string("Lucy")),
65
+
#("favourite-colour", json.string("Pink")),
66
+
])
67
+
let request = testing.post_json("/people", [], json)
68
+
let response = router.handle_request(request, ctx)
69
+
70
+
response.status
71
+
|> should.equal(201)
72
+
73
+
// The request created a new person in the database
74
+
let assert Ok([id]) = tiny_database.list(ctx.db)
75
+
76
+
response
77
+
|> testing.string_body
78
+
|> should.equal("{\"id\":\"" <> id <> "\"}")
79
+
}
80
+
81
+
pub fn create_person_missing_parameters_test() {
82
+
use ctx <- with_context
83
+
let json = json.object([#("name", json.string("Lucy"))])
84
+
let request = testing.post_json("/people", [], json)
85
+
let response = router.handle_request(request, ctx)
86
+
87
+
response.status
88
+
|> should.equal(422)
89
+
90
+
// Nothing was created in the database
91
+
let assert Ok([]) = tiny_database.list(ctx.db)
92
+
}
93
+
94
+
pub fn read_person_test() {
95
+
use ctx <- with_context
96
+
let assert Ok(id) = people.save_to_database(ctx.db, Person("Jane", "Red"))
97
+
let request = testing.get("/people/" <> id, [])
98
+
let response = router.handle_request(request, ctx)
99
+
100
+
response.status
101
+
|> should.equal(200)
102
+
103
+
response
104
+
|> testing.string_body
105
+
|> should.equal(
106
+
"{\"id\":\"" <> id <> "\",\"name\":\"Jane\",\"favourite-colour\":\"Red\"}",
107
+
)
108
+
}
+44
examples/06-serving-static-assets/README.md
+44
examples/06-serving-static-assets/README.md
···
1
+
# Wisp Example: Serving static assets
2
+
3
+
```sh
4
+
gleam run # Run the server
5
+
gleam test # Run the tests
6
+
```
7
+
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.
11
+
12
+
This example is based off of the ["Hello, World!" example][hello], so read that
13
+
one first. The additions are detailed here and commented in the code.
14
+
15
+
[hello]: https://github.com/lpil/wisp/tree/main/examples/01-routing
16
+
17
+
### `priv/static` directory
18
+
19
+
This directory contains the static assets that will be served by the application.
20
+
21
+
### `app/web` module
22
+
23
+
A `Context` type has been defined to hold the path to the directory containing
24
+
the static assets.
25
+
26
+
The `serve_static` middleware has been added to the middleware stack to serve
27
+
the static assets.
28
+
29
+
### `app` module
30
+
31
+
The `main` function now starts by determining the path to the static assets
32
+
directory and constructs a `Context` record to pass to the handler function.
33
+
34
+
### `app/router` module
35
+
36
+
The `handle_request` function now returns a page of HTML.
37
+
38
+
### `app_test` module
39
+
40
+
Tests have been added to ensure that the static assets are served correctly.
41
+
42
+
### Other files
43
+
44
+
No changes have been made to the other files.
+14
examples/06-serving-static-assets/gleam.toml
+14
examples/06-serving-static-assets/gleam.toml
···
1
+
name = "app"
2
+
version = "1.0.0"
3
+
description = "A Wisp example"
4
+
gleam = ">= 0.32.0"
5
+
6
+
[dependencies]
7
+
gleam_stdlib = "~> 0.30"
8
+
wisp = { path = "../.." }
9
+
gleam_erlang = "~> 0.23"
10
+
mist = ">= 2.0.0 and < 3.0.0"
11
+
gleam_http = "~> 3.5"
12
+
13
+
[dev-dependencies]
14
+
gleeunit = "~> 1.0"
+6
examples/06-serving-static-assets/priv/static/main.js
+6
examples/06-serving-static-assets/priv/static/main.js
+9
examples/06-serving-static-assets/priv/static/styles.css
+9
examples/06-serving-static-assets/priv/static/styles.css
+22
examples/06-serving-static-assets/src/app/router.gleam
+22
examples/06-serving-static-assets/src/app/router.gleam
···
1
+
import app/web.{type Context}
2
+
import gleam/string_tree
3
+
import wisp.{type Request, type Response}
4
+
5
+
const html = "<!DOCTYPE html>
6
+
<html lang=\"en\">
7
+
<head>
8
+
<meta charset=\"utf-8\">
9
+
<title>Wisp Example</title>
10
+
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
11
+
<link rel=\"stylesheet\" href=\"/static/styles.css\">
12
+
</head>
13
+
<body>
14
+
<script src=\"/static/main.js\"></script>
15
+
</body>
16
+
</html>
17
+
"
18
+
19
+
pub fn handle_request(req: Request, ctx: Context) -> Response {
20
+
use _req <- web.middleware(req, ctx)
21
+
wisp.html_response(string_tree.from_string(html), 200)
22
+
}
+19
examples/06-serving-static-assets/src/app/web.gleam
+19
examples/06-serving-static-assets/src/app/web.gleam
···
1
+
import wisp
2
+
3
+
pub type Context {
4
+
Context(static_directory: String)
5
+
}
6
+
7
+
pub fn middleware(
8
+
req: wisp.Request,
9
+
ctx: Context,
10
+
handle_request: fn(wisp.Request) -> wisp.Response,
11
+
) -> wisp.Response {
12
+
let req = wisp.method_override(req)
13
+
use <- wisp.log_request(req)
14
+
use <- wisp.rescue_crashes
15
+
use req <- wisp.handle_head(req)
16
+
use <- wisp.serve_static(req, under: "/static", from: ctx.static_directory)
17
+
18
+
handle_request(req)
19
+
}
+35
examples/06-serving-static-assets/src/app.gleam
+35
examples/06-serving-static-assets/src/app.gleam
···
1
+
import app/router
2
+
import app/web.{Context}
3
+
import gleam/erlang/process
4
+
import mist
5
+
import wisp
6
+
import wisp/wisp_mist
7
+
8
+
pub fn main() {
9
+
wisp.configure_logger()
10
+
let secret_key_base = wisp.random_string(64)
11
+
12
+
// A context is constructed holding the static directory path.
13
+
let ctx = Context(static_directory: static_directory())
14
+
15
+
// The handle_request function is partially applied with the context to make
16
+
// the request handler function that only takes a request.
17
+
let handler = router.handle_request(_, ctx)
18
+
19
+
let assert Ok(_) =
20
+
wisp_mist.handler(handler, secret_key_base)
21
+
|> mist.new
22
+
|> mist.port(8000)
23
+
|> mist.start_http
24
+
25
+
process.sleep_forever()
26
+
}
27
+
28
+
pub fn static_directory() -> String {
29
+
// The priv directory is where we store non-Gleam and non-Erlang files,
30
+
// including static assets to be served.
31
+
// This function returns an absolute path and works both in development and in
32
+
// production after compilation.
33
+
let assert Ok(priv_directory) = wisp.priv_directory("app")
34
+
priv_directory <> "/static"
35
+
}
+54
examples/06-serving-static-assets/test/app_test.gleam
+54
examples/06-serving-static-assets/test/app_test.gleam
···
1
+
import app
2
+
import app/router
3
+
import app/web.{type Context, Context}
4
+
import gleeunit
5
+
import gleeunit/should
6
+
import wisp/testing
7
+
8
+
pub fn main() {
9
+
gleeunit.main()
10
+
}
11
+
12
+
fn with_context(testcase: fn(Context) -> t) -> t {
13
+
// Create the context to use in tests
14
+
let context = Context(static_directory: app.static_directory())
15
+
16
+
// Run the test with the context
17
+
testcase(context)
18
+
}
19
+
20
+
pub fn get_home_page_test() {
21
+
use ctx <- with_context
22
+
let request = testing.get("/", [])
23
+
let response = router.handle_request(request, ctx)
24
+
25
+
response.status
26
+
|> should.equal(200)
27
+
28
+
response.headers
29
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
30
+
}
31
+
32
+
pub fn get_stylesheet_test() {
33
+
use ctx <- with_context
34
+
let request = testing.get("/static/styles.css", [])
35
+
let response = router.handle_request(request, ctx)
36
+
37
+
response.status
38
+
|> should.equal(200)
39
+
40
+
response.headers
41
+
|> should.equal([#("content-type", "text/css; charset=utf-8")])
42
+
}
43
+
44
+
pub fn get_javascript_test() {
45
+
use ctx <- with_context
46
+
let request = testing.get("/static/main.js", [])
47
+
let response = router.handle_request(request, ctx)
48
+
49
+
response.status
50
+
|> should.equal(200)
51
+
52
+
response.headers
53
+
|> should.equal([#("content-type", "text/javascript; charset=utf-8")])
54
+
}
+21
examples/07-logging/README.md
+21
examples/07-logging/README.md
···
1
+
# Wisp Example: Logging
2
+
3
+
```sh
4
+
gleam run # Run the server
5
+
gleam test # Run the tests
6
+
```
7
+
8
+
This example shows how to log messages using the BEAM logger.
9
+
10
+
This example is based off of the ["routing" example][routing], so read that
11
+
one first. The additions are detailed here and commented in the code.
12
+
13
+
[routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing
14
+
15
+
### `app/router` module
16
+
17
+
The `handle_request` function now logs messages depending on the request.
18
+
19
+
### Other files
20
+
21
+
No changes have been made to the other files.
+13
examples/07-logging/gleam.toml
+13
examples/07-logging/gleam.toml
···
1
+
name = "app"
2
+
version = "1.0.0"
3
+
description = "A Wisp example"
4
+
gleam = ">= 0.32.0"
5
+
6
+
[dependencies]
7
+
gleam_stdlib = "~> 0.30"
8
+
wisp = { path = "../.." }
9
+
gleam_erlang = "~> 0.23"
10
+
mist = ">= 2.0.0 and < 3.0.0"
11
+
12
+
[dev-dependencies]
13
+
gleeunit = "~> 1.0"
+41
examples/07-logging/src/app/router.gleam
+41
examples/07-logging/src/app/router.gleam
···
1
+
import wisp.{type Request, type Response}
2
+
import app/web
3
+
4
+
// Wisp has functions for logging messages using the BEAM logger.
5
+
//
6
+
// Messages can be logged at different levels. From most important to least
7
+
// important they are:
8
+
// - emergency
9
+
// - alert
10
+
// - critical
11
+
// - error
12
+
// - warning
13
+
// - notice
14
+
// - info
15
+
// - debug
16
+
//
17
+
pub fn handle_request(req: Request) -> Response {
18
+
use _req <- web.middleware(req)
19
+
20
+
case wisp.path_segments(req) {
21
+
[] -> {
22
+
wisp.log_debug("The home page")
23
+
wisp.ok()
24
+
}
25
+
26
+
["about"] -> {
27
+
wisp.log_info("They're reading about us")
28
+
wisp.ok()
29
+
}
30
+
31
+
["secret"] -> {
32
+
wisp.log_error("The secret page was found!")
33
+
wisp.ok()
34
+
}
35
+
36
+
_ -> {
37
+
wisp.log_warning("User requested a route that does not exist")
38
+
wisp.not_found()
39
+
}
40
+
}
41
+
}
+13
examples/07-logging/src/app/web.gleam
+13
examples/07-logging/src/app/web.gleam
···
1
+
import wisp
2
+
3
+
pub fn middleware(
4
+
req: wisp.Request,
5
+
handle_request: fn(wisp.Request) -> wisp.Response,
6
+
) -> wisp.Response {
7
+
let req = wisp.method_override(req)
8
+
use <- wisp.log_request(req)
9
+
use <- wisp.rescue_crashes
10
+
use req <- wisp.handle_head(req)
11
+
12
+
handle_request(req)
13
+
}
+18
examples/07-logging/src/app.gleam
+18
examples/07-logging/src/app.gleam
···
1
+
import app/router
2
+
import gleam/erlang/process
3
+
import mist
4
+
import wisp
5
+
import wisp/wisp_mist
6
+
7
+
pub fn main() {
8
+
wisp.configure_logger()
9
+
let secret_key_base = wisp.random_string(64)
10
+
11
+
let assert Ok(_) =
12
+
wisp_mist.handler(router.handle_request, secret_key_base)
13
+
|> mist.new
14
+
|> mist.port(8000)
15
+
|> mist.start_http
16
+
17
+
process.sleep_forever()
18
+
}
+16
examples/07-logging/test/app_test.gleam
+16
examples/07-logging/test/app_test.gleam
···
1
+
import gleeunit
2
+
import gleeunit/should
3
+
import wisp/testing
4
+
import app/router
5
+
6
+
pub fn main() {
7
+
gleeunit.main()
8
+
}
9
+
10
+
pub fn get_home_page_test() {
11
+
let request = testing.get("/", [])
12
+
let response = router.handle_request(request)
13
+
14
+
response.status
15
+
|> should.equal(200)
16
+
}
+36
examples/09-configuring-default-responses/README.md
+36
examples/09-configuring-default-responses/README.md
···
1
+
# Wisp Example: Configuring default responses
2
+
3
+
```sh
4
+
gleam run # Run the server
5
+
gleam test # Run the tests
6
+
```
7
+
8
+
Wisp has a response body value called `Empty`, which is just that: an empty
9
+
body. It can be returned by middleware functions such as `wisp.require_json`
10
+
when the request isn't suitable for request handler to run, such as if the
11
+
request body contains invalid JSON.
12
+
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
+
15
+
This example is based off of the ["routing" example][routing] so read that first.
16
+
The additions are detailed here and commented in the code.
17
+
18
+
[routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing
19
+
20
+
### `app/router` module
21
+
22
+
The `handle_request` function has been updated to return responses with the
23
+
`wisp.Empty` body.
24
+
25
+
### `app/web` module
26
+
27
+
The `middleware` function has been updated to return default responses when an
28
+
`wisp.Empty` response body is returned.
29
+
30
+
### `app_test` module
31
+
32
+
Tests have been added to test each of the .
33
+
34
+
### Other files
35
+
36
+
No changes have been made to the other files.
+13
examples/09-configuring-default-responses/gleam.toml
+13
examples/09-configuring-default-responses/gleam.toml
···
1
+
name = "app"
2
+
version = "1.0.0"
3
+
description = "A Wisp example"
4
+
gleam = ">= 0.32.0"
5
+
6
+
[dependencies]
7
+
gleam_stdlib = "~> 0.30"
8
+
wisp = { path = "../.." }
9
+
mist = ">= 2.0.0 and < 3.0.0"
10
+
gleam_erlang = "~> 0.23"
11
+
12
+
[dev-dependencies]
13
+
gleeunit = "~> 1.0"
+24
examples/09-configuring-default-responses/src/app/router.gleam
+24
examples/09-configuring-default-responses/src/app/router.gleam
···
1
+
import app/web
2
+
import gleam/string_tree
3
+
import wisp.{type Request, type Response}
4
+
5
+
pub fn handle_request(req: Request) -> Response {
6
+
use req <- web.middleware(req)
7
+
8
+
case wisp.path_segments(req) {
9
+
// This request returns a non-empty body.
10
+
[] -> {
11
+
"<h1>Hello, Joe!</h1>"
12
+
|> string_tree.from_string
13
+
|> wisp.html_response(200)
14
+
}
15
+
16
+
// These routes return `wisp.Empty` bodies.
17
+
["internal-server-error"] -> wisp.internal_server_error()
18
+
["unprocessable-entity"] -> wisp.unprocessable_entity()
19
+
["method-not-allowed"] -> wisp.method_not_allowed([])
20
+
["entity-too-large"] -> wisp.entity_too_large()
21
+
["bad-request"] -> wisp.bad_request()
22
+
_ -> wisp.not_found()
23
+
}
24
+
}
+56
examples/09-configuring-default-responses/src/app/web.gleam
+56
examples/09-configuring-default-responses/src/app/web.gleam
···
1
+
import gleam/bool
2
+
import gleam/string_tree
3
+
import wisp
4
+
5
+
pub fn middleware(
6
+
req: wisp.Request,
7
+
handle_request: fn(wisp.Request) -> wisp.Response,
8
+
) -> wisp.Response {
9
+
let req = wisp.method_override(req)
10
+
use <- wisp.log_request(req)
11
+
use <- wisp.rescue_crashes
12
+
use req <- wisp.handle_head(req)
13
+
14
+
// This new middleware has been added to the stack.
15
+
// It is defined below.
16
+
use <- default_responses
17
+
18
+
handle_request(req)
19
+
}
20
+
21
+
pub fn default_responses(handle_request: fn() -> wisp.Response) -> wisp.Response {
22
+
let response = handle_request()
23
+
24
+
// The `bool.guard` function is used to return the original request if the
25
+
// body is not `wisp.Empty`.
26
+
use <- bool.guard(when: response.body != wisp.Empty, return: response)
27
+
28
+
// You can use any logic to return appropriate responses depending on what is
29
+
// best for your application.
30
+
// I'm going to match on the status code and depending on what it is add
31
+
// different HTML as the body. This is a good option for most applications.
32
+
case response.status {
33
+
404 | 405 ->
34
+
"<h1>There's nothing here</h1>"
35
+
|> string_tree.from_string
36
+
|> wisp.html_body(response, _)
37
+
38
+
400 | 422 ->
39
+
"<h1>Bad request</h1>"
40
+
|> string_tree.from_string
41
+
|> wisp.html_body(response, _)
42
+
43
+
413 ->
44
+
"<h1>Request entity too large</h1>"
45
+
|> string_tree.from_string
46
+
|> wisp.html_body(response, _)
47
+
48
+
500 ->
49
+
"<h1>Internal server error</h1>"
50
+
|> string_tree.from_string
51
+
|> wisp.html_body(response, _)
52
+
53
+
// For other status codes redirect to the home page
54
+
_ -> wisp.redirect("/")
55
+
}
56
+
}
+18
examples/09-configuring-default-responses/src/app.gleam
+18
examples/09-configuring-default-responses/src/app.gleam
···
1
+
import app/router
2
+
import gleam/erlang/process
3
+
import mist
4
+
import wisp
5
+
import wisp/wisp_mist
6
+
7
+
pub fn main() {
8
+
wisp.configure_logger()
9
+
let secret_key_base = wisp.random_string(64)
10
+
11
+
let assert Ok(_) =
12
+
wisp_mist.handler(router.handle_request, secret_key_base)
13
+
|> mist.new
14
+
|> mist.port(8000)
15
+
|> mist.start_http
16
+
17
+
process.sleep_forever()
18
+
}
+115
examples/09-configuring-default-responses/test/app_test.gleam
+115
examples/09-configuring-default-responses/test/app_test.gleam
···
1
+
import gleeunit
2
+
import gleeunit/should
3
+
import gleam/string
4
+
import wisp/testing
5
+
import app/router
6
+
7
+
pub fn main() {
8
+
gleeunit.main()
9
+
}
10
+
11
+
pub fn home_test() {
12
+
let response = router.handle_request(testing.get("/", []))
13
+
14
+
response.status
15
+
|> should.equal(200)
16
+
17
+
response.headers
18
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
19
+
20
+
let assert True =
21
+
response
22
+
|> testing.string_body
23
+
|> string.contains("<h1>Hello, Joe!</h1>")
24
+
}
25
+
26
+
pub fn internal_server_error_test() {
27
+
let response =
28
+
router.handle_request(testing.get("/internal-server-error", []))
29
+
30
+
response.status
31
+
|> should.equal(500)
32
+
33
+
response.headers
34
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
35
+
36
+
let assert True =
37
+
response
38
+
|> testing.string_body
39
+
|> string.contains("<h1>Internal server error</h1>")
40
+
}
41
+
42
+
pub fn unprocessable_entity_test() {
43
+
let response = router.handle_request(testing.get("/unprocessable-entity", []))
44
+
45
+
response.status
46
+
|> should.equal(422)
47
+
48
+
response.headers
49
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
50
+
51
+
let assert True =
52
+
response
53
+
|> testing.string_body
54
+
|> string.contains("<h1>Bad request</h1>")
55
+
}
56
+
57
+
pub fn bad_request_test() {
58
+
let response = router.handle_request(testing.get("/bad-request", []))
59
+
60
+
response.status
61
+
|> should.equal(400)
62
+
63
+
response.headers
64
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
65
+
66
+
let assert True =
67
+
response
68
+
|> testing.string_body
69
+
|> string.contains("<h1>Bad request</h1>")
70
+
}
71
+
72
+
pub fn method_not_allowed_test() {
73
+
let response = router.handle_request(testing.get("/method-not-allowed", []))
74
+
75
+
response.status
76
+
|> should.equal(405)
77
+
78
+
response.headers
79
+
|> should.equal([#("allow", ""), #("content-type", "text/html; charset=utf-8")])
80
+
81
+
let assert True =
82
+
response
83
+
|> testing.string_body
84
+
|> string.contains("<h1>There's nothing here</h1>")
85
+
}
86
+
87
+
pub fn not_found_test() {
88
+
let response = router.handle_request(testing.get("/not-found", []))
89
+
90
+
response.status
91
+
|> should.equal(404)
92
+
93
+
response.headers
94
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
95
+
96
+
let assert True =
97
+
response
98
+
|> testing.string_body
99
+
|> string.contains("<h1>There's nothing here</h1>")
100
+
}
101
+
102
+
pub fn entity_too_large_test() {
103
+
let response = router.handle_request(testing.get("/entity-too-large", []))
104
+
105
+
response.status
106
+
|> should.equal(413)
107
+
108
+
response.headers
109
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
110
+
111
+
let assert True =
112
+
response
113
+
|> testing.string_body
114
+
|> string.contains("<h1>Request entity too large</h1>")
115
+
}
-28
examples/1-routing/README.md
-28
examples/1-routing/README.md
···
1
-
# Wisp Example: Routing
2
-
3
-
```sh
4
-
gleam run # Run the server
5
-
gleam test # Run the tests
6
-
```
7
-
8
-
This example shows how to route requests to different handlers based on the
9
-
request path and method.
10
-
11
-
This example is based off of the ["Hello, World!" example][hello], so read that
12
-
one first. The additions are detailed here and commented in the code.
13
-
14
-
[hello]: https://github.com/lpil/wisp/tree/main/examples/0-hello-world
15
-
16
-
### `app/router` module
17
-
18
-
The `handle_request` function now pattern matches on the request and calls other
19
-
request handler functions depending on where the request should go.
20
-
21
-
### `app_test` module
22
-
23
-
Tests have been added for each of the routes. The `wisp/testing` module is used
24
-
to create different requests to test the application with.
25
-
26
-
### Other files
27
-
28
-
No changes have been made to the other files.
-14
examples/1-routing/gleam.toml
-14
examples/1-routing/gleam.toml
···
1
-
name = "app"
2
-
version = "1.0.0"
3
-
description = "A Wisp example"
4
-
gleam = ">= 0.32.0"
5
-
6
-
[dependencies]
7
-
gleam_stdlib = "~> 0.30"
8
-
wisp = { path = "../.." }
9
-
gleam_erlang = "~> 0.23"
10
-
mist = "~> 0.14"
11
-
gleam_http = "~> 3.5"
12
-
13
-
[dev-dependencies]
14
-
gleeunit = "~> 1.0"
-72
examples/1-routing/src/app/router.gleam
-72
examples/1-routing/src/app/router.gleam
···
1
-
import wisp.{type Request, type Response}
2
-
import gleam/string_builder
3
-
import gleam/http.{Get, Post}
4
-
import app/web
5
-
6
-
pub fn handle_request(req: Request) -> Response {
7
-
use _req <- web.middleware(req)
8
-
9
-
// Wisp doesn't have a special router abstraction, instead we recommend using
10
-
// regular old pattern matching. This is faster than a router, is type safe,
11
-
// and means you don't have to learn or be limited by a special DSL.
12
-
//
13
-
case wisp.path_segments(req) {
14
-
// This matches `/`.
15
-
[] -> home_page(req)
16
-
17
-
// This matches `/comments`.
18
-
["comments"] -> comments(req)
19
-
20
-
// This matches `/comments/:id`.
21
-
// The `id` segment is bound to a variable and passed to the handler.
22
-
["comments", id] -> show_comment(req, id)
23
-
24
-
// This matches all other paths.
25
-
_ -> wisp.not_found()
26
-
}
27
-
}
28
-
29
-
fn home_page(req: Request) -> Response {
30
-
// The home page can only be accessed via GET requests, so this middleware is
31
-
// used to return a 405: Method Not Allowed response for all other methods.
32
-
use <- wisp.require_method(req, Get)
33
-
34
-
let html = string_builder.from_string("Hello, Joe!")
35
-
wisp.ok()
36
-
|> wisp.html_body(html)
37
-
}
38
-
39
-
fn comments(req: Request) -> Response {
40
-
// This handler for `/comments` can respond to both GET and POST requests,
41
-
// so we pattern match on the method here.
42
-
case req.method {
43
-
Get -> list_comments()
44
-
Post -> create_comment(req)
45
-
_ -> wisp.method_not_allowed([Get, Post])
46
-
}
47
-
}
48
-
49
-
fn list_comments() -> Response {
50
-
// In a later example we'll show how to read from a database.
51
-
let html = string_builder.from_string("Comments!")
52
-
wisp.ok()
53
-
|> wisp.html_body(html)
54
-
}
55
-
56
-
fn create_comment(_req: Request) -> Response {
57
-
// In a later example we'll show how to parse data from the request body.
58
-
let html = string_builder.from_string("Created")
59
-
wisp.created()
60
-
|> wisp.html_body(html)
61
-
}
62
-
63
-
fn show_comment(req: Request, id: String) -> Response {
64
-
use <- wisp.require_method(req, Get)
65
-
66
-
// The `id` path parameter has been passed to this function, so we could use
67
-
// it to look up a comment in a database.
68
-
// For now we'll just include in the response body.
69
-
let html = string_builder.from_string("Comment with id " <> id)
70
-
wisp.ok()
71
-
|> wisp.html_body(html)
72
-
}
-13
examples/1-routing/src/app/web.gleam
-13
examples/1-routing/src/app/web.gleam
···
1
-
import wisp
2
-
3
-
pub fn middleware(
4
-
req: wisp.Request,
5
-
handle_request: fn(wisp.Request) -> wisp.Response,
6
-
) -> wisp.Response {
7
-
let req = wisp.method_override(req)
8
-
use <- wisp.log_request(req)
9
-
use <- wisp.rescue_crashes
10
-
use req <- wisp.handle_head(req)
11
-
12
-
handle_request(req)
13
-
}
-17
examples/1-routing/src/app.gleam
-17
examples/1-routing/src/app.gleam
···
1
-
import gleam/erlang/process
2
-
import mist
3
-
import wisp
4
-
import app/router
5
-
6
-
pub fn main() {
7
-
wisp.configure_logger()
8
-
let secret_key_base = wisp.random_string(64)
9
-
10
-
let assert Ok(_) =
11
-
wisp.mist_handler(router.handle_request, secret_key_base)
12
-
|> mist.new
13
-
|> mist.port(8000)
14
-
|> mist.start_http
15
-
16
-
process.sleep_forever()
17
-
}
-75
examples/1-routing/test/app_test.gleam
-75
examples/1-routing/test/app_test.gleam
···
1
-
import gleeunit
2
-
import gleeunit/should
3
-
import wisp/testing
4
-
import app/router
5
-
6
-
pub fn main() {
7
-
gleeunit.main()
8
-
}
9
-
10
-
pub fn get_home_page_test() {
11
-
let request = testing.get("/", [])
12
-
let response = router.handle_request(request)
13
-
14
-
response.status
15
-
|> should.equal(200)
16
-
17
-
response.headers
18
-
|> should.equal([#("content-type", "text/html")])
19
-
20
-
response
21
-
|> testing.string_body
22
-
|> should.equal("Hello, Joe!")
23
-
}
24
-
25
-
pub fn post_home_page_test() {
26
-
let request = testing.post("/", [], "a body")
27
-
let response = router.handle_request(request)
28
-
response.status
29
-
|> should.equal(405)
30
-
}
31
-
32
-
pub fn page_not_found_test() {
33
-
let request = testing.get("/nothing-here", [])
34
-
let response = router.handle_request(request)
35
-
response.status
36
-
|> should.equal(404)
37
-
}
38
-
39
-
pub fn get_comments_test() {
40
-
let request = testing.get("/comments", [])
41
-
let response = router.handle_request(request)
42
-
response.status
43
-
|> should.equal(200)
44
-
}
45
-
46
-
pub fn post_comments_test() {
47
-
let request = testing.post("/comments", [], "")
48
-
let response = router.handle_request(request)
49
-
response.status
50
-
|> should.equal(201)
51
-
}
52
-
53
-
pub fn delete_comments_test() {
54
-
let request = testing.delete("/comments", [], "")
55
-
let response = router.handle_request(request)
56
-
response.status
57
-
|> should.equal(405)
58
-
}
59
-
60
-
pub fn get_comment_test() {
61
-
let request = testing.get("/comments/123", [])
62
-
let response = router.handle_request(request)
63
-
response.status
64
-
|> should.equal(200)
65
-
response
66
-
|> testing.string_body
67
-
|> should.equal("Comment with id 123")
68
-
}
69
-
70
-
pub fn delete_comment_test() {
71
-
let request = testing.delete("/comments/123", [], "")
72
-
let response = router.handle_request(request)
73
-
response.status
74
-
|> should.equal(405)
75
-
}
+25
examples/10-working-with-files/README.md
+25
examples/10-working-with-files/README.md
···
1
+
# Wisp Example: Working with files
2
+
3
+
```sh
4
+
gleam run # Run the server
5
+
gleam test # Run the tests
6
+
```
7
+
8
+
This example shows how to accept file uploads and allow users to download files.
9
+
10
+
This example is based off of the ["working with form data" example][formdata],
11
+
so read that first. The additions are detailed here and commented in the code.
12
+
13
+
[formdata]: https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data
14
+
15
+
### `app/router` module
16
+
17
+
The `handle_request` function has been updated to upload and download files.
18
+
19
+
### `app_test` module
20
+
21
+
Tests have been added that upload and download files to verify the behaviour.
22
+
23
+
### Other files
24
+
25
+
No changes have been made to the other files.
+14
examples/10-working-with-files/gleam.toml
+14
examples/10-working-with-files/gleam.toml
···
1
+
name = "app"
2
+
version = "1.0.0"
3
+
description = "A Wisp example"
4
+
gleam = ">= 0.32.0"
5
+
6
+
[dependencies]
7
+
gleam_stdlib = "~> 0.30"
8
+
wisp = { path = "../.." }
9
+
gleam_erlang = "~> 0.23"
10
+
mist = ">= 2.0.0 and < 3.0.0"
11
+
gleam_http = "~> 3.5"
12
+
13
+
[dev-dependencies]
14
+
gleeunit = "~> 1.0"
+117
examples/10-working-with-files/src/app/router.gleam
+117
examples/10-working-with-files/src/app/router.gleam
···
1
+
import app/web
2
+
import gleam/bytes_tree
3
+
import gleam/http.{Get, Post}
4
+
import gleam/list
5
+
import gleam/result
6
+
import gleam/string_tree
7
+
import wisp.{type Request, type Response}
8
+
9
+
pub fn handle_request(req: Request) -> Response {
10
+
use req <- web.middleware(req)
11
+
12
+
case wisp.path_segments(req) {
13
+
[] -> show_home(req)
14
+
["file-from-disc"] -> handle_download_file_from_disc(req)
15
+
["file-from-memory"] -> handle_download_file_from_memory(req)
16
+
["upload-file"] -> handle_file_upload(req)
17
+
_ -> wisp.not_found()
18
+
}
19
+
}
20
+
21
+
// Notice how `enctype="multipart/form-data"` is used in the file-upload form.
22
+
// This ensure that the file is encoded appropriately for the server to read.
23
+
const html = "
24
+
<p><a href='/file-from-memory'>Download file from memory</a></p>
25
+
<p><a href='/file-from-disc'>Download file from disc</a></p>
26
+
27
+
<form method=post action='/upload-file' enctype='multipart/form-data'>
28
+
<label>Your file:
29
+
<input type='file' name='uploaded-file'>
30
+
</label>
31
+
<input type='submit' value='Submit'>
32
+
</form>
33
+
"
34
+
35
+
fn show_home(req: Request) -> Response {
36
+
use <- wisp.require_method(req, Get)
37
+
html
38
+
|> string_tree.from_string
39
+
|> wisp.html_response(200)
40
+
}
41
+
42
+
fn handle_download_file_from_memory(req: Request) -> Response {
43
+
use <- wisp.require_method(req, Get)
44
+
45
+
// In this case we have the file contents in memory as a string.
46
+
// This is good if we have just made the file, but if the file already exists
47
+
// on the disc then the approach in the next function is more efficient.
48
+
let file_contents = bytes_tree.from_string("Hello, Joe!")
49
+
50
+
wisp.ok()
51
+
|> wisp.set_header("content-type", "text/plain")
52
+
// The content-disposition header is set by this function to ensure this is
53
+
// treated as a file download. If the file was uploaded by the user then you
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
+
|> wisp.file_download_from_memory(
57
+
named: "hello.txt",
58
+
containing: file_contents,
59
+
)
60
+
}
61
+
62
+
fn handle_download_file_from_disc(req: Request) -> Response {
63
+
use <- wisp.require_method(req, Get)
64
+
65
+
// In this case the file exists on the disc.
66
+
// Here we're using the project README, but in a real application you'd
67
+
// probably have an absolute path to wherever it is you keep your files.
68
+
let file_path = "./README.md"
69
+
70
+
wisp.ok()
71
+
|> wisp.set_header("content-type", "text/markdown")
72
+
|> wisp.file_download(named: "hello.md", from: file_path)
73
+
}
74
+
75
+
fn handle_file_upload(req: Request) -> Response {
76
+
use <- wisp.require_method(req, Post)
77
+
use formdata <- wisp.require_form(req)
78
+
79
+
// The list and result module are used here to extract the values from the
80
+
// form data.
81
+
// Alternatively you could also pattern match on the list of values (they are
82
+
// sorted into alphabetical order), or use a HTML form library.
83
+
let result = {
84
+
// Note the name of the input is used to find the value.
85
+
use file <- result.try(list.key_find(formdata.files, "uploaded-file"))
86
+
87
+
// The file has been streamed to a temporary file on the disc, so there's no
88
+
// risk of large files causing memory issues.
89
+
// The `.path` field contains the path to this file, which you may choose to
90
+
// move or read using a library like `simplifile`. When the request is done the
91
+
// temporary file is deleted.
92
+
wisp.log_info("File uploaded to " <> file.path)
93
+
94
+
// File uploads may include a file name. Some clients such as curl may not
95
+
// have one, so this field may be empty.
96
+
// You should never trust this field. Just because it has a particular file
97
+
// extension does not mean it is a file of that type, and it may contain
98
+
// invalid characters. Always validate the file type and do not use this
99
+
// name as the new path for the file.
100
+
wisp.log_info("The file name is reportedly " <> file.file_name)
101
+
102
+
Ok(file.file_name)
103
+
}
104
+
105
+
// An appropriate response is returned depending on whether the form data
106
+
// could be successfully handled or not.
107
+
case result {
108
+
Ok(name) -> {
109
+
{ "<p>Thank you for your file!" <> name <> "</p>" <> html }
110
+
|> string_tree.from_string
111
+
|> wisp.html_response(200)
112
+
}
113
+
Error(_) -> {
114
+
wisp.bad_request()
115
+
}
116
+
}
117
+
}
+12
examples/10-working-with-files/src/app/web.gleam
+12
examples/10-working-with-files/src/app/web.gleam
···
1
+
import wisp
2
+
3
+
pub fn middleware(
4
+
req: wisp.Request,
5
+
handle_request: fn(wisp.Request) -> wisp.Response,
6
+
) -> wisp.Response {
7
+
let req = wisp.method_override(req)
8
+
use <- wisp.log_request(req)
9
+
use <- wisp.rescue_crashes
10
+
use req <- wisp.handle_head(req)
11
+
handle_request(req)
12
+
}
+18
examples/10-working-with-files/src/app.gleam
+18
examples/10-working-with-files/src/app.gleam
···
1
+
import app/router
2
+
import gleam/erlang/process
3
+
import mist
4
+
import wisp
5
+
import wisp/wisp_mist
6
+
7
+
pub fn main() {
8
+
wisp.configure_logger()
9
+
let secret_key_base = wisp.random_string(64)
10
+
11
+
let assert Ok(_) =
12
+
wisp_mist.handler(router.handle_request, secret_key_base)
13
+
|> mist.new
14
+
|> mist.port(8000)
15
+
|> mist.start_http
16
+
17
+
process.sleep_forever()
18
+
}
+69
examples/10-working-with-files/test/app_test.gleam
+69
examples/10-working-with-files/test/app_test.gleam
···
1
+
import gleeunit
2
+
import gleeunit/should
3
+
import gleam/string
4
+
import wisp/testing
5
+
import app/router
6
+
7
+
pub fn main() {
8
+
gleeunit.main()
9
+
}
10
+
11
+
pub fn home_test() {
12
+
let response = router.handle_request(testing.get("/", []))
13
+
14
+
response.status
15
+
|> should.equal(200)
16
+
17
+
response.headers
18
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
19
+
20
+
response
21
+
|> testing.string_body
22
+
|> string.contains("<form method")
23
+
|> should.equal(True)
24
+
}
25
+
26
+
pub fn file_from_disc_test() {
27
+
let response = router.handle_request(testing.get("/file-from-disc", []))
28
+
29
+
response.status
30
+
|> should.equal(200)
31
+
32
+
response.headers
33
+
|> should.equal([
34
+
#("content-type", "text/markdown"),
35
+
#("content-disposition", "attachment; filename=\"hello.md\""),
36
+
])
37
+
38
+
response
39
+
|> testing.string_body
40
+
|> string.starts_with("# Wisp Example: ")
41
+
|> should.equal(True)
42
+
}
43
+
44
+
pub fn file_from_memory_test() {
45
+
let response = router.handle_request(testing.get("/file-from-memory", []))
46
+
47
+
response.status
48
+
|> should.equal(200)
49
+
50
+
response.headers
51
+
|> should.equal([
52
+
#("content-type", "text/plain"),
53
+
#("content-disposition", "attachment; filename=\"hello.txt\""),
54
+
])
55
+
56
+
response
57
+
|> testing.string_body
58
+
|> should.equal("Hello, Joe!")
59
+
}
60
+
61
+
pub fn upload_file_test() {
62
+
// Oh no! What's this? There's no test here!
63
+
//
64
+
// The helper for constructing a multipart form request in tests has not yet
65
+
// been implemented. If this is something you need for your project, please
66
+
// let us know and we'll bump it up the list of priorities.
67
+
//
68
+
Nil
69
+
}
-29
examples/2-working-with-form-data/README.md
-29
examples/2-working-with-form-data/README.md
···
1
-
# Wisp Example: Working with form data
2
-
3
-
```sh
4
-
gleam run # Run the server
5
-
gleam test # Run the tests
6
-
```
7
-
8
-
This example shows how to read urlencoded and multipart formdata from a request
9
-
10
-
This example is based off of the ["Hello, World!" example][hello], and uses
11
-
concepts from the [routing example][routing] so read those first. The additions
12
-
are detailed here and commented in the code.
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
16
-
17
-
### `app/router` module
18
-
19
-
The `handle_request` function has been updated to read the form data from the
20
-
request body and make use of values from it.
21
-
22
-
### `app_test` module
23
-
24
-
Tests have been added that send requests with form data bodies and check that
25
-
the expected response is returned.
26
-
27
-
### Other files
28
-
29
-
No changes have been made to the other files.
-14
examples/2-working-with-form-data/gleam.toml
-14
examples/2-working-with-form-data/gleam.toml
···
1
-
name = "app"
2
-
version = "1.0.0"
3
-
description = "A Wisp example"
4
-
gleam = ">= 0.32.0"
5
-
6
-
[dependencies]
7
-
gleam_stdlib = "~> 0.30"
8
-
wisp = { path = "../.." }
9
-
gleam_erlang = "~> 0.23"
10
-
mist = "~> 0.14"
11
-
gleam_http = "~> 3.5"
12
-
13
-
[dev-dependencies]
14
-
gleeunit = "~> 1.0"
-69
examples/2-working-with-form-data/src/app/router.gleam
-69
examples/2-working-with-form-data/src/app/router.gleam
···
1
-
import app/web
2
-
import gleam/http.{Get, Post}
3
-
import gleam/list
4
-
import gleam/result
5
-
import gleam/string_builder
6
-
import wisp.{type Request, type Response}
7
-
8
-
pub fn handle_request(req: Request) -> Response {
9
-
use req <- web.middleware(req)
10
-
11
-
// For GET requests, show the form,
12
-
// for POST requests we use the data from the form
13
-
case req.method {
14
-
Get -> show_form()
15
-
Post -> handle_form_submission(req)
16
-
_ -> wisp.method_not_allowed(allowed: [Get, Post])
17
-
}
18
-
}
19
-
20
-
pub fn show_form() -> Response {
21
-
// In a larger application a template library or HTML form library might
22
-
// be used here instead of a string literal.
23
-
let html =
24
-
string_builder.from_string(
25
-
"<form method='post'>
26
-
<label>Title:
27
-
<input type='text' name='title'>
28
-
</label>
29
-
<label>Name:
30
-
<input type='text' name='name'>
31
-
</label>
32
-
<input type='submit' value='Submit'>
33
-
</form>",
34
-
)
35
-
wisp.ok()
36
-
|> wisp.html_body(html)
37
-
}
38
-
39
-
pub fn handle_form_submission(req: Request) -> Response {
40
-
// This middleware parses a `wisp.FormData` from the request body.
41
-
// It returns an error response if the body is not valid form data, or
42
-
// if the content-type is not `application/x-www-form-urlencoded` or
43
-
// `multipart/form-data`, or if the body is too large.
44
-
use formdata <- wisp.require_form(req)
45
-
46
-
// The list and result module are used here to extract the values from the
47
-
// form data.
48
-
// Alternatively you could also pattern match on the list of values (they are
49
-
// sorted into alphabetical order), or use a HTML form library.
50
-
let result = {
51
-
use title <- result.try(list.key_find(formdata.values, "title"))
52
-
use name <- result.try(list.key_find(formdata.values, "name"))
53
-
let greeting =
54
-
"Hi, " <> wisp.escape_html(title) <> " " <> wisp.escape_html(name) <> "!"
55
-
Ok(greeting)
56
-
}
57
-
58
-
// An appropriate response is returned depending on whether the form data
59
-
// could be successfully handled or not.
60
-
case result {
61
-
Ok(content) -> {
62
-
wisp.ok()
63
-
|> wisp.html_body(string_builder.from_string(content))
64
-
}
65
-
Error(_) -> {
66
-
wisp.bad_request()
67
-
}
68
-
}
69
-
}
-12
examples/2-working-with-form-data/src/app/web.gleam
-12
examples/2-working-with-form-data/src/app/web.gleam
···
1
-
import wisp
2
-
3
-
pub fn middleware(
4
-
req: wisp.Request,
5
-
handle_request: fn(wisp.Request) -> wisp.Response,
6
-
) -> wisp.Response {
7
-
let req = wisp.method_override(req)
8
-
use <- wisp.log_request(req)
9
-
use <- wisp.rescue_crashes
10
-
use req <- wisp.handle_head(req)
11
-
handle_request(req)
12
-
}
-17
examples/2-working-with-form-data/src/app.gleam
-17
examples/2-working-with-form-data/src/app.gleam
···
1
-
import gleam/erlang/process
2
-
import mist
3
-
import wisp
4
-
import app/router
5
-
6
-
pub fn main() {
7
-
wisp.configure_logger()
8
-
let secret_key_base = wisp.random_string(64)
9
-
10
-
let assert Ok(_) =
11
-
wisp.mist_handler(router.handle_request, secret_key_base)
12
-
|> mist.new
13
-
|> mist.port(8000)
14
-
|> mist.start_http
15
-
16
-
process.sleep_forever()
17
-
}
-63
examples/2-working-with-form-data/test/app_test.gleam
-63
examples/2-working-with-form-data/test/app_test.gleam
···
1
-
import gleeunit
2
-
import gleeunit/should
3
-
import gleam/string
4
-
import wisp/testing
5
-
import app/router
6
-
7
-
pub fn main() {
8
-
gleeunit.main()
9
-
}
10
-
11
-
pub fn view_form_test() {
12
-
let response = router.handle_request(testing.get("/", []))
13
-
14
-
response.status
15
-
|> should.equal(200)
16
-
17
-
response.headers
18
-
|> should.equal([#("content-type", "text/html")])
19
-
20
-
response
21
-
|> testing.string_body
22
-
|> string.contains("<form method='post'>")
23
-
|> should.equal(True)
24
-
}
25
-
26
-
pub fn submit_wrong_content_type_test() {
27
-
let response = router.handle_request(testing.post("/", [], ""))
28
-
29
-
response.status
30
-
|> should.equal(415)
31
-
32
-
response.headers
33
-
|> should.equal([
34
-
#("accept", "application/x-www-form-urlencoded, multipart/form-data"),
35
-
])
36
-
}
37
-
38
-
pub fn submit_missing_parameters_test() {
39
-
// The `METHOD_form` functions are used to create a request with a
40
-
// `x-www-form-urlencoded` body, with the appropriate `content-type` header.
41
-
let response =
42
-
testing.post_form("/", [], [])
43
-
|> router.handle_request()
44
-
45
-
response.status
46
-
|> should.equal(400)
47
-
}
48
-
49
-
pub fn submit_successful_test() {
50
-
let response =
51
-
testing.post_form("/", [], [#("title", "Captain"), #("name", "Caveman")])
52
-
|> router.handle_request()
53
-
54
-
response.status
55
-
|> should.equal(200)
56
-
57
-
response.headers
58
-
|> should.equal([#("content-type", "text/html")])
59
-
60
-
response
61
-
|> testing.string_body
62
-
|> should.equal("Hi, Captain Caveman!")
63
-
}
-35
examples/3-working-with-json/README.md
-35
examples/3-working-with-json/README.md
···
1
-
# Wisp Example: Working with JSON
2
-
3
-
```sh
4
-
gleam run # Run the server
5
-
gleam test # Run the tests
6
-
```
7
-
8
-
This example shows how to read JSON from a request and return JSON in the
9
-
response.
10
-
11
-
This example is based off of the ["Hello, World!" example][hello], and uses
12
-
concepts from the [routing example][routing] so read those first. The additions
13
-
are detailed here and commented in the code.
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
17
-
18
-
### `gleam.toml` file
19
-
20
-
The `gleam_json` JSON package has been added as a dependency.
21
-
22
-
### `app/router` module
23
-
24
-
The `handle_request` function has been updated to read JSON from the
25
-
request body, decode it using the Gleam standard library, and return JSON
26
-
back to the client.
27
-
28
-
### `app_test` module
29
-
30
-
Tests have been added that send requests with JSON bodies and check that the
31
-
expected response is returned.
32
-
33
-
### Other files
34
-
35
-
No changes have been made to the other files.
-14
examples/3-working-with-json/gleam.toml
-14
examples/3-working-with-json/gleam.toml
···
1
-
name = "app"
2
-
version = "1.0.0"
3
-
description = "A Wisp example"
4
-
gleam = ">= 0.32.0"
5
-
6
-
[dependencies]
7
-
gleam_stdlib = "~> 0.30"
8
-
wisp = { path = "../.." }
9
-
gleam_json = "~> 0.6"
10
-
gleam_erlang = "~> 0.23"
11
-
mist = "~> 0.14"
12
-
13
-
[dev-dependencies]
14
-
gleeunit = "~> 1.0"
-61
examples/3-working-with-json/src/app/router.gleam
-61
examples/3-working-with-json/src/app/router.gleam
···
1
-
import app/web
2
-
import gleam/dynamic.{type Dynamic}
3
-
import gleam/http.{Post}
4
-
import gleam/json
5
-
import gleam/result
6
-
import wisp.{type Request, type Response}
7
-
8
-
// This type is going to be parsed and decoded from the request body.
9
-
pub type Person {
10
-
Person(name: String, is_cool: Bool)
11
-
}
12
-
13
-
// To decode the type we need a dynamic decoder.
14
-
// See the standard library documentation for more information on decoding
15
-
// dynamic values [1].
16
-
//
17
-
// [1]: https://hexdocs.pm/gleam_stdlib/gleam/dynamic.html
18
-
fn decode_person(json: Dynamic) -> Result(Person, dynamic.DecodeErrors) {
19
-
let decoder =
20
-
dynamic.decode2(
21
-
Person,
22
-
dynamic.field("name", dynamic.string),
23
-
dynamic.field("is-cool", dynamic.bool),
24
-
)
25
-
decoder(json)
26
-
}
27
-
28
-
pub fn handle_request(req: Request) -> Response {
29
-
use req <- web.middleware(req)
30
-
use <- wisp.require_method(req, Post)
31
-
32
-
// This middleware parses a `Dynamic` value from the request body.
33
-
// It returns an error response if the body is not valid JSON, or
34
-
// if the content-type is not `application/json`, or if the body
35
-
// is too large.
36
-
use json <- wisp.require_json(req)
37
-
38
-
let result = {
39
-
// The dynamic value can be decoded into a `Person` value.
40
-
use person <- result.try(decode_person(json))
41
-
42
-
// And then a JSON response can be created from the person.
43
-
let object =
44
-
json.object([
45
-
#("name", json.string(person.name)),
46
-
#("is-cool", json.bool(person.is_cool)),
47
-
#("saved", json.bool(True)),
48
-
])
49
-
Ok(json.to_string_builder(object))
50
-
}
51
-
52
-
// An appropriate response is returned depending on whether the JSON could be
53
-
// successfully handled or not.
54
-
case result {
55
-
Ok(json) -> wisp.json_response(json, 201)
56
-
57
-
// In a real application we would probably want to return some JSON error
58
-
// object, but for this example we'll just return an empty response.
59
-
Error(_) -> wisp.unprocessable_entity()
60
-
}
61
-
}
-13
examples/3-working-with-json/src/app/web.gleam
-13
examples/3-working-with-json/src/app/web.gleam
···
1
-
import wisp
2
-
3
-
pub fn middleware(
4
-
req: wisp.Request,
5
-
handle_request: fn(wisp.Request) -> wisp.Response,
6
-
) -> wisp.Response {
7
-
let req = wisp.method_override(req)
8
-
use <- wisp.log_request(req)
9
-
use <- wisp.rescue_crashes
10
-
use req <- wisp.handle_head(req)
11
-
12
-
handle_request(req)
13
-
}
-17
examples/3-working-with-json/src/app.gleam
-17
examples/3-working-with-json/src/app.gleam
···
1
-
import gleam/erlang/process
2
-
import mist
3
-
import wisp
4
-
import app/router
5
-
6
-
pub fn main() {
7
-
wisp.configure_logger()
8
-
let secret_key_base = wisp.random_string(64)
9
-
10
-
let assert Ok(_) =
11
-
wisp.mist_handler(router.handle_request, secret_key_base)
12
-
|> mist.new
13
-
|> mist.port(8000)
14
-
|> mist.start_http
15
-
16
-
process.sleep_forever()
17
-
}
-57
examples/3-working-with-json/test/app_test.gleam
-57
examples/3-working-with-json/test/app_test.gleam
···
1
-
import app/router
2
-
import gleam/json
3
-
import gleeunit
4
-
import gleeunit/should
5
-
import wisp/testing
6
-
7
-
pub fn main() {
8
-
gleeunit.main()
9
-
}
10
-
11
-
pub fn get_test() {
12
-
let response = router.handle_request(testing.get("/", []))
13
-
14
-
response.status
15
-
|> should.equal(405)
16
-
}
17
-
18
-
pub fn submit_wrong_content_type_test() {
19
-
let response = router.handle_request(testing.post("/", [], ""))
20
-
21
-
response.status
22
-
|> should.equal(415)
23
-
24
-
response.headers
25
-
|> should.equal([#("accept", "application/json")])
26
-
}
27
-
28
-
pub fn submit_missing_parameters_test() {
29
-
let json = json.object([#("name", json.string("Joe"))])
30
-
31
-
// The `METHOD_json` functions are used to create a request with a JSON body,
32
-
// with the appropriate `content-type` header.
33
-
let response =
34
-
testing.post_json("/", [], json)
35
-
|> router.handle_request()
36
-
37
-
response.status
38
-
|> should.equal(422)
39
-
}
40
-
41
-
pub fn submit_successful_test() {
42
-
let json =
43
-
json.object([#("name", json.string("Joe")), #("is-cool", json.bool(True))])
44
-
let response =
45
-
testing.post_json("/", [], json)
46
-
|> router.handle_request()
47
-
48
-
response.status
49
-
|> should.equal(201)
50
-
51
-
response.headers
52
-
|> should.equal([#("content-type", "application/json")])
53
-
54
-
response
55
-
|> testing.string_body
56
-
|> should.equal("{\"name\":\"Joe\",\"is-cool\":true,\"saved\":true}")
57
-
}
-36
examples/4-working-with-other-formats/README.md
-36
examples/4-working-with-other-formats/README.md
···
1
-
# Wisp Example: Working with other formats
2
-
3
-
```sh
4
-
gleam run # Run the server
5
-
gleam test # Run the tests
6
-
```
7
-
8
-
This example shows how to read and return formats that do not have special
9
-
support in Wisp. In this case we'll use CSV, but the same techniques can be used
10
-
for any format.
11
-
12
-
This example is based off of the ["Hello, World!" example][hello], and uses
13
-
concepts from the [routing example][routing] so read those first. The additions
14
-
are detailed here and commented in the code.
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
18
-
19
-
### `gleam.toml` file
20
-
21
-
The `gsv` CSV package has been added as a dependency.
22
-
23
-
### `app/router` module
24
-
25
-
The `handle_request` function has been updated to read a string from the
26
-
request body, decode it using the `gsv` library, and return some CSV data
27
-
back to the client.
28
-
29
-
### `app_test` module
30
-
31
-
Tests have been added that send requests with CSV bodies and check that the
32
-
expected response is returned.
33
-
34
-
### Other files
35
-
36
-
No changes have been made to the other files.
-15
examples/4-working-with-other-formats/gleam.toml
-15
examples/4-working-with-other-formats/gleam.toml
···
1
-
name = "app"
2
-
version = "1.0.0"
3
-
description = "A Wisp example"
4
-
gleam = ">= 0.32.0"
5
-
6
-
[dependencies]
7
-
gleam_stdlib = "~> 0.30"
8
-
wisp = { path = "../.." }
9
-
gsv = "~> 1.0"
10
-
gleam_erlang = "~> 0.23"
11
-
mist = "~> 0.14"
12
-
gleam_http = "~> 3.5"
13
-
14
-
[dev-dependencies]
15
-
gleeunit = "~> 1.0"
-61
examples/4-working-with-other-formats/src/app/router.gleam
-61
examples/4-working-with-other-formats/src/app/router.gleam
···
1
-
import app/web
2
-
import gleam/int
3
-
import gleam/http.{Post}
4
-
import gleam/list
5
-
import gleam/result
6
-
import gleam/string
7
-
import gsv
8
-
import wisp.{type Request, type Response}
9
-
10
-
pub fn handle_request(req: Request) -> Response {
11
-
use req <- web.middleware(req)
12
-
use <- wisp.require_method(req, Post)
13
-
14
-
// We want to accept only CSV content, so we use this middleware to check the
15
-
// correct content type header is set, and return an error response if not.
16
-
use <- wisp.require_content_type(req, "text/csv")
17
-
18
-
// This middleware reads the body of the request and returns it as a string,
19
-
// erroring if the body is not valid UTF-8, or if the body is too large.
20
-
//
21
-
// If you want to get a bit-string and don't need specifically UTF-8 encoded
22
-
// data then the `wisp.require_bit_string_body` middleware can be used
23
-
// instead.
24
-
use body <- wisp.require_string_body(req)
25
-
26
-
// Now that we have the body we can parse and process it.
27
-
// In this case we expect it to be a CSV file with a header row, but in your
28
-
// application it could be XML, protobuf, or anything else.
29
-
let result = {
30
-
// The GSV library is used to parse the CSV.
31
-
use rows <- result.try(gsv.to_lists(body))
32
-
33
-
// Get the first row, which is the header row.
34
-
use headers <- result.try(list.first(rows))
35
-
36
-
// Define the table we want to send back to the client.
37
-
let table = [
38
-
["headers", "row-count"],
39
-
[string.join(headers, ","), int.to_string(list.length(rows) - 1)],
40
-
]
41
-
42
-
// Convert the table to CSV.
43
-
let csv = gsv.from_lists(table, ",", gsv.Unix)
44
-
45
-
Ok(csv)
46
-
}
47
-
48
-
// An appropriate response is returned depending on whether the CSV could be
49
-
// successfully handled or not.
50
-
case result {
51
-
Ok(csv) -> {
52
-
wisp.ok()
53
-
|> wisp.set_header("content-type", "text/csv")
54
-
|> wisp.string_body(csv)
55
-
}
56
-
57
-
Error(_error) -> {
58
-
wisp.unprocessable_entity()
59
-
}
60
-
}
61
-
}
-13
examples/4-working-with-other-formats/src/app/web.gleam
-13
examples/4-working-with-other-formats/src/app/web.gleam
···
1
-
import wisp
2
-
3
-
pub fn middleware(
4
-
req: wisp.Request,
5
-
handle_request: fn(wisp.Request) -> wisp.Response,
6
-
) -> wisp.Response {
7
-
let req = wisp.method_override(req)
8
-
use <- wisp.log_request(req)
9
-
use <- wisp.rescue_crashes
10
-
use req <- wisp.handle_head(req)
11
-
12
-
handle_request(req)
13
-
}
-17
examples/4-working-with-other-formats/src/app.gleam
-17
examples/4-working-with-other-formats/src/app.gleam
···
1
-
import gleam/erlang/process
2
-
import mist
3
-
import wisp
4
-
import app/router
5
-
6
-
pub fn main() {
7
-
wisp.configure_logger()
8
-
let secret_key_base = wisp.random_string(64)
9
-
10
-
let assert Ok(_) =
11
-
wisp.mist_handler(router.handle_request, secret_key_base)
12
-
|> mist.new
13
-
|> mist.port(8000)
14
-
|> mist.start_http
15
-
16
-
process.sleep_forever()
17
-
}
-44
examples/4-working-with-other-formats/test/app_test.gleam
-44
examples/4-working-with-other-formats/test/app_test.gleam
···
1
-
import app/router
2
-
import gleeunit
3
-
import gleeunit/should
4
-
import wisp/testing
5
-
6
-
pub fn main() {
7
-
gleeunit.main()
8
-
}
9
-
10
-
pub fn get_test() {
11
-
let response = router.handle_request(testing.get("/", []))
12
-
13
-
response.status
14
-
|> should.equal(405)
15
-
}
16
-
17
-
pub fn post_wrong_content_type_test() {
18
-
let response = router.handle_request(testing.post("/", [], ""))
19
-
20
-
response.status
21
-
|> should.equal(415)
22
-
23
-
response.headers
24
-
|> should.equal([#("accept", "text/csv")])
25
-
}
26
-
27
-
pub fn post_successful_test() {
28
-
let csv = "name,is-cool\nJoe,true\nJosé,true\n"
29
-
30
-
let response =
31
-
testing.post("/", [], csv)
32
-
|> testing.set_header("content-type", "text/csv")
33
-
|> router.handle_request()
34
-
35
-
response.status
36
-
|> should.equal(200)
37
-
38
-
response.headers
39
-
|> should.equal([#("content-type", "text/csv")])
40
-
41
-
response
42
-
|> testing.string_body
43
-
|> should.equal("headers,row-count\n\"name,is-cool\",2")
44
-
}
-50
examples/5-using-a-database/README.md
-50
examples/5-using-a-database/README.md
···
1
-
# Wisp Example: Using A Database
2
-
3
-
```sh
4
-
gleam run # Run the server
5
-
gleam test # Run the tests
6
-
```
7
-
8
-
This example shows how to use a database, using a `Context` type to hold the
9
-
database connection.
10
-
11
-
This example is based off of the ["working with JSON" example][json], so read
12
-
that first. The additions are detailed here and commented in the code.
13
-
14
-
[json]: https://github.com/lpil/wisp/tree/main/examples/3-working-with-json
15
-
16
-
### `gleam.toml` file
17
-
18
-
The `tiny_database` package has been added as a dependency. In a real project
19
-
you would like use a proper database such as Postgres or SQLite.
20
-
21
-
### `app/web` module
22
-
23
-
A new `Context` type has been created to hold the database connection.
24
-
25
-
### `app` module
26
-
27
-
The `main` function now starts by creating a database connection and passing it
28
-
to the handler function in a `Context` record.
29
-
30
-
### `app/router` module
31
-
32
-
The `handle_request` function has been updated to route requests to functions in
33
-
the new `app/web/people` module.
34
-
35
-
### `app/web/people` module
36
-
37
-
This module has been created to hold all the functions for working with the
38
-
"people" feature, including their request handlers.
39
-
40
-
### `app_test` module
41
-
42
-
The `with_context` function has been added to create a `Context` record with a
43
-
database connection, and to setup the database.
44
-
45
-
The tests have been updated to verify that the application saves and retrieves
46
-
the data correctly.
47
-
48
-
### Other files
49
-
50
-
No changes have been made to the other files.
-15
examples/5-using-a-database/gleam.toml
-15
examples/5-using-a-database/gleam.toml
···
1
-
name = "app"
2
-
version = "1.0.0"
3
-
description = "A Wisp example"
4
-
gleam = ">= 0.32.0"
5
-
6
-
[dependencies]
7
-
gleam_stdlib = "~> 0.30"
8
-
wisp = { path = "../.." }
9
-
gleam_json = "~> 0.6"
10
-
tiny_database = { path = "../utilities/tiny_database" }
11
-
gleam_erlang = "~> 0.23"
12
-
mist = "~> 0.14"
13
-
14
-
[dev-dependencies]
15
-
gleeunit = "~> 1.0"
-19
examples/5-using-a-database/src/app/router.gleam
-19
examples/5-using-a-database/src/app/router.gleam
···
1
-
import app/web.{type Context}
2
-
import app/web/people
3
-
import wisp.{type Request, type Response}
4
-
5
-
pub fn handle_request(req: Request, ctx: Context) -> Response {
6
-
use req <- web.middleware(req)
7
-
8
-
// A new `app/web/people` module now contains the handlers and other functions
9
-
// relating to the People feature of the application.
10
-
//
11
-
// The router module now only deals with routing, and dispatches to the
12
-
// feature modules for handling requests.
13
-
//
14
-
case wisp.path_segments(req) {
15
-
["people"] -> people.all(req, ctx)
16
-
["people", id] -> people.one(req, ctx, id)
17
-
_ -> wisp.not_found()
18
-
}
19
-
}
-144
examples/5-using-a-database/src/app/web/people.gleam
-144
examples/5-using-a-database/src/app/web/people.gleam
···
1
-
import app/web.{type Context}
2
-
import gleam/dynamic.{type Dynamic}
3
-
import gleam/http.{Get, Post}
4
-
import gleam/json
5
-
import gleam/map
6
-
import gleam/result.{try}
7
-
import tiny_database
8
-
import wisp.{type Request, type Response}
9
-
10
-
// This request handler is used for requests to `/people`.
11
-
//
12
-
pub fn all(req: Request, ctx: Context) -> Response {
13
-
// Dispatch to the appropriate handler based on the HTTP method.
14
-
case req.method {
15
-
Get -> list_people(ctx)
16
-
Post -> create_person(req, ctx)
17
-
_ -> wisp.method_not_allowed([Get, Post])
18
-
}
19
-
}
20
-
21
-
// This request handler is used for requests to `/people/:id`.
22
-
//
23
-
pub fn one(req: Request, ctx: Context, id: String) -> Response {
24
-
// Dispatch to the appropriate handler based on the HTTP method.
25
-
case req.method {
26
-
Get -> read_person(ctx, id)
27
-
_ -> wisp.method_not_allowed([Get])
28
-
}
29
-
}
30
-
31
-
pub type Person {
32
-
Person(name: String, favourite_colour: String)
33
-
}
34
-
35
-
// This handler returns a list of all the people in the database, in JSON
36
-
// format.
37
-
//
38
-
pub fn list_people(ctx: Context) -> Response {
39
-
let result = {
40
-
// Get all the ids from the database.
41
-
use ids <- try(tiny_database.list(ctx.db))
42
-
43
-
// Convert the ids into a JSON array of objects.
44
-
Ok(json.to_string_builder(json.object([
45
-
#(
46
-
"people",
47
-
json.array(ids, fn(id) { json.object([#("id", json.string(id))]) }),
48
-
),
49
-
])))
50
-
}
51
-
52
-
case result {
53
-
// When everything goes well we return a 200 response with the JSON.
54
-
Ok(json) -> wisp.json_response(json, 200)
55
-
56
-
// In a later example we will see how to return specific errors to the user
57
-
// depending on what went wrong. For now we will just return a 500 error.
58
-
Error(Nil) -> wisp.internal_server_error()
59
-
}
60
-
}
61
-
62
-
pub fn create_person(req: Request, ctx: Context) -> Response {
63
-
// Read the JSON from the request body.
64
-
use json <- wisp.require_json(req)
65
-
66
-
let result = {
67
-
// Decode the JSON into a Person record.
68
-
use person <- try(decode_person(json))
69
-
70
-
// Save the person to the database.
71
-
use id <- try(save_to_database(ctx.db, person))
72
-
73
-
// Construct a JSON payload with the id of the newly created person.
74
-
Ok(json.to_string_builder(json.object([#("id", json.string(id))])))
75
-
}
76
-
77
-
// Return an appropriate response depending on whether everything went well or
78
-
// if there was an error.
79
-
case result {
80
-
Ok(json) -> wisp.json_response(json, 201)
81
-
Error(Nil) -> wisp.unprocessable_entity()
82
-
}
83
-
}
84
-
85
-
pub fn read_person(ctx: Context, id: String) -> Response {
86
-
let result = {
87
-
// Read the person with the given id from the database.
88
-
use person <- try(read_from_database(ctx.db, id))
89
-
90
-
// Construct a JSON payload with the person's details.
91
-
Ok(json.to_string_builder(json.object([
92
-
#("id", json.string(id)),
93
-
#("name", json.string(person.name)),
94
-
#("favourite-colour", json.string(person.favourite_colour)),
95
-
])))
96
-
}
97
-
98
-
// Return an appropriate response.
99
-
case result {
100
-
Ok(json) -> wisp.json_response(json, 200)
101
-
Error(Nil) -> wisp.not_found()
102
-
}
103
-
}
104
-
105
-
fn decode_person(json: Dynamic) -> Result(Person, Nil) {
106
-
let decoder =
107
-
dynamic.decode2(
108
-
Person,
109
-
dynamic.field("name", dynamic.string),
110
-
dynamic.field("favourite-colour", dynamic.string),
111
-
)
112
-
let result = decoder(json)
113
-
114
-
// In this example we are not going to be reporting specific errors to the
115
-
// user, so we can discard the error and replace it with Nil.
116
-
result
117
-
|> result.nil_error
118
-
}
119
-
120
-
/// Save a person to the database and return the id of the newly created record.
121
-
pub fn save_to_database(
122
-
db: tiny_database.Connection,
123
-
person: Person,
124
-
) -> Result(String, Nil) {
125
-
// In a real application you might use a database client with some SQL here.
126
-
// Instead we create a simple map and save that.
127
-
let data =
128
-
map.from_list([
129
-
#("name", person.name),
130
-
#("favourite-colour", person.favourite_colour),
131
-
])
132
-
tiny_database.insert(db, data)
133
-
}
134
-
135
-
pub fn read_from_database(
136
-
db: tiny_database.Connection,
137
-
id: String,
138
-
) -> Result(Person, Nil) {
139
-
// In a real application you might use a database client with some SQL here.
140
-
use data <- try(tiny_database.read(db, id))
141
-
use name <- try(map.get(data, "name"))
142
-
use favourite_colour <- try(map.get(data, "favourite-colour"))
143
-
Ok(Person(name, favourite_colour))
144
-
}
-25
examples/5-using-a-database/src/app/web.gleam
-25
examples/5-using-a-database/src/app/web.gleam
···
1
-
import wisp
2
-
import tiny_database
3
-
4
-
// A new Context type, which holds any additional data that the request handlers
5
-
// need in addition to the request.
6
-
//
7
-
// Here it is holding a database connection, but it could hold anything else
8
-
// such as API keys, IO performing functions (so they can be swapped out in
9
-
// tests for mock implementations), configuration, and so on.
10
-
//
11
-
pub type Context {
12
-
Context(db: tiny_database.Connection)
13
-
}
14
-
15
-
pub fn middleware(
16
-
req: wisp.Request,
17
-
handle_request: fn(wisp.Request) -> wisp.Response,
18
-
) -> wisp.Response {
19
-
let req = wisp.method_override(req)
20
-
use <- wisp.log_request(req)
21
-
use <- wisp.rescue_crashes
22
-
use req <- wisp.handle_head(req)
23
-
24
-
handle_request(req)
25
-
}
-33
examples/5-using-a-database/src/app.gleam
-33
examples/5-using-a-database/src/app.gleam
···
1
-
import gleam/erlang/process
2
-
import tiny_database
3
-
import mist
4
-
import wisp
5
-
import app/router
6
-
import app/web
7
-
8
-
pub const data_directory = "tmp/data"
9
-
10
-
pub fn main() {
11
-
wisp.configure_logger()
12
-
let secret_key_base = wisp.random_string(64)
13
-
14
-
// A database creation is created here, when the program starts.
15
-
// This connection is used by all requests.
16
-
use db <- tiny_database.with_connection(data_directory)
17
-
18
-
// A context is constructed to hold the database connection.
19
-
let context = web.Context(db: db)
20
-
21
-
// The handle_request function is partially applied with the context to make
22
-
// the request handler function that only takes a request.
23
-
let handler = router.handle_request(_, context)
24
-
25
-
let assert Ok(_) =
26
-
handler
27
-
|> wisp.mist_handler(secret_key_base)
28
-
|> mist.new
29
-
|> mist.port(8000)
30
-
|> mist.start_http
31
-
32
-
process.sleep_forever()
33
-
}
-108
examples/5-using-a-database/test/app_test.gleam
-108
examples/5-using-a-database/test/app_test.gleam
···
1
-
import app
2
-
import app/router
3
-
import app/web.{type Context, Context}
4
-
import app/web/people.{Person}
5
-
import gleam/json
6
-
import gleeunit
7
-
import gleeunit/should
8
-
import tiny_database
9
-
import wisp/testing
10
-
11
-
pub fn main() {
12
-
gleeunit.main()
13
-
}
14
-
15
-
fn with_context(test: fn(Context) -> t) -> t {
16
-
// Create a new database connection for this test
17
-
use db <- tiny_database.with_connection(app.data_directory)
18
-
19
-
// Truncate the database so there is no prexisting data from previous tests
20
-
let assert Ok(_) = tiny_database.truncate(db)
21
-
let context = Context(db: db)
22
-
23
-
// Run the test with the context
24
-
test(context)
25
-
}
26
-
27
-
pub fn get_unknown_test() {
28
-
use ctx <- with_context
29
-
let request = testing.get("/", [])
30
-
let response = router.handle_request(request, ctx)
31
-
32
-
response.status
33
-
|> should.equal(404)
34
-
}
35
-
36
-
pub fn list_people_test() {
37
-
use ctx <- with_context
38
-
39
-
let response = router.handle_request(testing.get("/people", []), ctx)
40
-
response.status
41
-
|> should.equal(200)
42
-
response.headers
43
-
|> should.equal([#("content-type", "application/json")])
44
-
45
-
// Initially there are no people in the database
46
-
response
47
-
|> testing.string_body
48
-
|> should.equal("{\"people\":[]}")
49
-
50
-
// Create a new person
51
-
let assert Ok(id) = people.save_to_database(ctx.db, Person("Jane", "Red"))
52
-
53
-
// The id of the new person is listed by the API
54
-
let response = router.handle_request(testing.get("/people", []), ctx)
55
-
response
56
-
|> testing.string_body
57
-
|> should.equal("{\"people\":[{\"id\":\"" <> id <> "\"}]}")
58
-
}
59
-
60
-
pub fn create_person_test() {
61
-
use ctx <- with_context
62
-
let json =
63
-
json.object([
64
-
#("name", json.string("Lucy")),
65
-
#("favourite-colour", json.string("Pink")),
66
-
])
67
-
let request = testing.post_json("/people", [], json)
68
-
let response = router.handle_request(request, ctx)
69
-
70
-
response.status
71
-
|> should.equal(201)
72
-
73
-
// The request created a new person in the database
74
-
let assert Ok([id]) = tiny_database.list(ctx.db)
75
-
76
-
response
77
-
|> testing.string_body
78
-
|> should.equal("{\"id\":\"" <> id <> "\"}")
79
-
}
80
-
81
-
pub fn create_person_missing_parameters_test() {
82
-
use ctx <- with_context
83
-
let json = json.object([#("name", json.string("Lucy"))])
84
-
let request = testing.post_json("/people", [], json)
85
-
let response = router.handle_request(request, ctx)
86
-
87
-
response.status
88
-
|> should.equal(422)
89
-
90
-
// Nothing was created in the database
91
-
let assert Ok([]) = tiny_database.list(ctx.db)
92
-
}
93
-
94
-
pub fn read_person_test() {
95
-
use ctx <- with_context
96
-
let assert Ok(id) = people.save_to_database(ctx.db, Person("Jane", "Red"))
97
-
let request = testing.get("/people/" <> id, [])
98
-
let response = router.handle_request(request, ctx)
99
-
100
-
response.status
101
-
|> should.equal(200)
102
-
103
-
response
104
-
|> testing.string_body
105
-
|> should.equal(
106
-
"{\"id\":\"" <> id <> "\",\"name\":\"Jane\",\"favourite-colour\":\"Red\"}",
107
-
)
108
-
}
-43
examples/6-serving-static-assets/README.md
-43
examples/6-serving-static-assets/README.md
···
1
-
# Wisp Example: Serving static assets
2
-
3
-
```sh
4
-
gleam run # Run the server
5
-
gleam test # Run the tests
6
-
```
7
-
8
-
This example shows how to route requests to different handlers based on the
9
-
request path and method.
10
-
11
-
This example is based off of the ["Hello, World!" example][hello], so read that
12
-
one first. The additions are detailed here and commented in the code.
13
-
14
-
[hello]: https://github.com/lpil/wisp/tree/main/examples/1-routing
15
-
16
-
### `priv/static` directory
17
-
18
-
This directory contains the static assets that will be served by the application.
19
-
20
-
### `app/web` module
21
-
22
-
A `Context` type has been defined to hold the path to the directory containing
23
-
the static assets.
24
-
25
-
The `serve_static` middleware has been added to the middleware stack to serve
26
-
the static assets.
27
-
28
-
### `app` module
29
-
30
-
The `main` function now starts by determining the path to the static assets
31
-
directory and constructs a `Context` record to pass to the handler function.
32
-
33
-
### `app/router` module
34
-
35
-
The `handle_request` function now returns a page of HTML.
36
-
37
-
### `app_test` module
38
-
39
-
Tests have been added to ensure that the static assets are served correctly.
40
-
41
-
### Other files
42
-
43
-
No changes have been made to the other files.
-13
examples/6-serving-static-assets/gleam.toml
-13
examples/6-serving-static-assets/gleam.toml
-6
examples/6-serving-static-assets/priv/static/main.js
-6
examples/6-serving-static-assets/priv/static/main.js
-9
examples/6-serving-static-assets/priv/static/styles.css
-9
examples/6-serving-static-assets/priv/static/styles.css
-23
examples/6-serving-static-assets/src/app/router.gleam
-23
examples/6-serving-static-assets/src/app/router.gleam
···
1
-
import wisp.{type Request, type Response}
2
-
import gleam/string_builder
3
-
import gleam/http
4
-
import app/web.{type Context}
5
-
6
-
const html = "<!DOCTYPE html>
7
-
<html lang=\"en\">
8
-
<head>
9
-
<meta charset=\"utf-8\">
10
-
<title>Wisp Example</title>
11
-
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
12
-
<link rel=\"stylesheet\" href=\"/static/styles.css\">
13
-
</head>
14
-
<body>
15
-
<script src=\"/static/main.js\"></script>
16
-
</body>
17
-
</html>
18
-
"
19
-
20
-
pub fn handle_request(req: Request, ctx: Context) -> Response {
21
-
use req <- web.middleware(req, ctx)
22
-
wisp.html_response(string_builder.from_string(html), 200)
23
-
}
-19
examples/6-serving-static-assets/src/app/web.gleam
-19
examples/6-serving-static-assets/src/app/web.gleam
···
1
-
import wisp
2
-
3
-
pub type Context {
4
-
Context(static_directory: String)
5
-
}
6
-
7
-
pub fn middleware(
8
-
req: wisp.Request,
9
-
ctx: Context,
10
-
handle_request: fn(wisp.Request) -> wisp.Response,
11
-
) -> wisp.Response {
12
-
let req = wisp.method_override(req)
13
-
use <- wisp.log_request(req)
14
-
use <- wisp.rescue_crashes
15
-
use req <- wisp.handle_head(req)
16
-
use <- wisp.serve_static(req, under: "/static", from: ctx.static_directory)
17
-
18
-
handle_request(req)
19
-
}
-34
examples/6-serving-static-assets/src/app.gleam
-34
examples/6-serving-static-assets/src/app.gleam
···
1
-
import gleam/erlang/process
2
-
import mist
3
-
import wisp
4
-
import app/router
5
-
import app/web.{Context}
6
-
7
-
pub fn main() {
8
-
wisp.configure_logger()
9
-
let secret_key_base = wisp.random_string(64)
10
-
11
-
// A context is constructed holding the static directory path.
12
-
let ctx = Context(static_directory: static_directory())
13
-
14
-
// The handle_request function is partially applied with the context to make
15
-
// the request handler function that only takes a request.
16
-
let handler = router.handle_request(_, ctx)
17
-
18
-
let assert Ok(_) =
19
-
wisp.mist_handler(handler, secret_key_base)
20
-
|> mist.new
21
-
|> mist.port(8000)
22
-
|> mist.start_http
23
-
24
-
process.sleep_forever()
25
-
}
26
-
27
-
pub fn static_directory() -> String {
28
-
// The priv directory is where we store non-Gleam and non-Erlang files,
29
-
// including static assets to be served.
30
-
// This function returns an absolute path and works both in development and in
31
-
// production after compilation.
32
-
let assert Ok(priv_directory) = wisp.priv_directory("app")
33
-
priv_directory <> "/static"
34
-
}
-54
examples/6-serving-static-assets/test/app_test.gleam
-54
examples/6-serving-static-assets/test/app_test.gleam
···
1
-
import gleeunit
2
-
import gleeunit/should
3
-
import wisp/testing
4
-
import app
5
-
import app/router
6
-
import app/web.{type Context, Context}
7
-
8
-
pub fn main() {
9
-
gleeunit.main()
10
-
}
11
-
12
-
fn with_context(test: fn(Context) -> t) -> t {
13
-
// Create the context to use in tests
14
-
let context = Context(static_directory: app.static_directory())
15
-
16
-
// Run the test with the context
17
-
test(context)
18
-
}
19
-
20
-
pub fn get_home_page_test() {
21
-
use ctx <- with_context
22
-
let request = testing.get("/", [])
23
-
let response = router.handle_request(request, ctx)
24
-
25
-
response.status
26
-
|> should.equal(200)
27
-
28
-
response.headers
29
-
|> should.equal([#("content-type", "text/html")])
30
-
}
31
-
32
-
pub fn get_stylesheet_test() {
33
-
use ctx <- with_context
34
-
let request = testing.get("/static/styles.css", [])
35
-
let response = router.handle_request(request, ctx)
36
-
37
-
response.status
38
-
|> should.equal(200)
39
-
40
-
response.headers
41
-
|> should.equal([#("content-type", "text/css")])
42
-
}
43
-
44
-
pub fn get_javascript_test() {
45
-
use ctx <- with_context
46
-
let request = testing.get("/static/main.js", [])
47
-
let response = router.handle_request(request, ctx)
48
-
49
-
response.status
50
-
|> should.equal(200)
51
-
52
-
response.headers
53
-
|> should.equal([#("content-type", "text/javascript")])
54
-
}
-22
examples/7-logging/README.md
-22
examples/7-logging/README.md
···
1
-
# Wisp Example: Logging
2
-
3
-
```sh
4
-
gleam run # Run the server
5
-
gleam test # Run the tests
6
-
```
7
-
8
-
This example shows how to route requests to different handlers based on the
9
-
request path and method.
10
-
11
-
This example is based off of the ["routing" example][routing], so read that
12
-
one first. The additions are detailed here and commented in the code.
13
-
14
-
[routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing
15
-
16
-
### `app/router` module
17
-
18
-
The `handle_request` function now logs messages depending on the request.
19
-
20
-
### Other files
21
-
22
-
No changes have been made to the other files.
-13
examples/7-logging/gleam.toml
-13
examples/7-logging/gleam.toml
-41
examples/7-logging/src/app/router.gleam
-41
examples/7-logging/src/app/router.gleam
···
1
-
import wisp.{type Request, type Response}
2
-
import app/web
3
-
4
-
// Wisp has functions for logging messages using the BEAM logger.
5
-
//
6
-
// Messages can be logged at different levels. From most important to least
7
-
// important they are:
8
-
// - emergency
9
-
// - alert
10
-
// - critical
11
-
// - error
12
-
// - warning
13
-
// - notice
14
-
// - info
15
-
// - debug
16
-
//
17
-
pub fn handle_request(req: Request) -> Response {
18
-
use _req <- web.middleware(req)
19
-
20
-
case wisp.path_segments(req) {
21
-
[] -> {
22
-
wisp.log_debug("The home page")
23
-
wisp.ok()
24
-
}
25
-
26
-
["about"] -> {
27
-
wisp.log_info("They're reading about us")
28
-
wisp.ok()
29
-
}
30
-
31
-
["secret"] -> {
32
-
wisp.log_error("The secret page was found!")
33
-
wisp.ok()
34
-
}
35
-
36
-
_ -> {
37
-
wisp.log_warning("User requested a route that does not exist")
38
-
wisp.not_found()
39
-
}
40
-
}
41
-
}
-13
examples/7-logging/src/app/web.gleam
-13
examples/7-logging/src/app/web.gleam
···
1
-
import wisp
2
-
3
-
pub fn middleware(
4
-
req: wisp.Request,
5
-
handle_request: fn(wisp.Request) -> wisp.Response,
6
-
) -> wisp.Response {
7
-
let req = wisp.method_override(req)
8
-
use <- wisp.log_request(req)
9
-
use <- wisp.rescue_crashes
10
-
use req <- wisp.handle_head(req)
11
-
12
-
handle_request(req)
13
-
}
-17
examples/7-logging/src/app.gleam
-17
examples/7-logging/src/app.gleam
···
1
-
import gleam/erlang/process
2
-
import mist
3
-
import wisp
4
-
import app/router
5
-
6
-
pub fn main() {
7
-
wisp.configure_logger()
8
-
let secret_key_base = wisp.random_string(64)
9
-
10
-
let assert Ok(_) =
11
-
wisp.mist_handler(router.handle_request, secret_key_base)
12
-
|> mist.new
13
-
|> mist.port(8000)
14
-
|> mist.start_http
15
-
16
-
process.sleep_forever()
17
-
}
-16
examples/7-logging/test/app_test.gleam
-16
examples/7-logging/test/app_test.gleam
···
1
-
import gleeunit
2
-
import gleeunit/should
3
-
import wisp/testing
4
-
import app/router
5
-
6
-
pub fn main() {
7
-
gleeunit.main()
8
-
}
9
-
10
-
pub fn get_home_page_test() {
11
-
let request = testing.get("/", [])
12
-
let response = router.handle_request(request)
13
-
14
-
response.status
15
-
|> should.equal(200)
16
-
}
-36
examples/9-configuring-default-responses/README.md
-36
examples/9-configuring-default-responses/README.md
···
1
-
# Wisp Example: Working with form data
2
-
3
-
```sh
4
-
gleam run # Run the server
5
-
gleam test # Run the tests
6
-
```
7
-
8
-
Wisp has a response body value called `Empty`, which is just that: an empty
9
-
body. It can be returned by middleware functions such as `wisp.require_json`
10
-
when the request isn't suitable for request handler to run, such as if the
11
-
request body contains invalid JSON.
12
-
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
-
15
-
This example is based off of the [routing example][routing] so read that first.
16
-
The additions are detailed here and commented in the code.
17
-
18
-
[routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing
19
-
20
-
### `app/router` module
21
-
22
-
The `handle_request` function has been updated to return responses with the
23
-
`wisp.Empty` body.
24
-
25
-
### `app/web` module
26
-
27
-
The `middleware` function has been updated to return default responses when an
28
-
`wisp.Empty` response body is returned.
29
-
30
-
### `app_test` module
31
-
32
-
Tests have been added to test each of the .
33
-
34
-
### Other files
35
-
36
-
No changes have been made to the other files.
-13
examples/9-configuring-default-responses/gleam.toml
-13
examples/9-configuring-default-responses/gleam.toml
-24
examples/9-configuring-default-responses/src/app/router.gleam
-24
examples/9-configuring-default-responses/src/app/router.gleam
···
1
-
import app/web
2
-
import gleam/string_builder
3
-
import wisp.{type Request, type Response}
4
-
5
-
pub fn handle_request(req: Request) -> Response {
6
-
use req <- web.middleware(req)
7
-
8
-
case wisp.path_segments(req) {
9
-
// This request returns a non-empty body.
10
-
[] -> {
11
-
"<h1>Hello, Joe!</h1>"
12
-
|> string_builder.from_string
13
-
|> wisp.html_response(200)
14
-
}
15
-
16
-
// These routes return `wisp.Empty` bodies.
17
-
["internal-server-error"] -> wisp.internal_server_error()
18
-
["unprocessable-entity"] -> wisp.unprocessable_entity()
19
-
["method-not-allowed"] -> wisp.method_not_allowed([])
20
-
["entity-too-large"] -> wisp.entity_too_large()
21
-
["bad-request"] -> wisp.bad_request()
22
-
_ -> wisp.not_found()
23
-
}
24
-
}
-56
examples/9-configuring-default-responses/src/app/web.gleam
-56
examples/9-configuring-default-responses/src/app/web.gleam
···
1
-
import wisp
2
-
import gleam/bool
3
-
import gleam/string_builder
4
-
5
-
pub fn middleware(
6
-
req: wisp.Request,
7
-
handle_request: fn(wisp.Request) -> wisp.Response,
8
-
) -> wisp.Response {
9
-
let req = wisp.method_override(req)
10
-
use <- wisp.log_request(req)
11
-
use <- wisp.rescue_crashes
12
-
use req <- wisp.handle_head(req)
13
-
14
-
// This new middleware has been added to the stack.
15
-
// It is defined below.
16
-
use <- default_responses
17
-
18
-
handle_request(req)
19
-
}
20
-
21
-
pub fn default_responses(handle_request: fn() -> wisp.Response) -> wisp.Response {
22
-
let response = handle_request()
23
-
24
-
// The `bool.guard` function is used to return the original request if the
25
-
// body is not `wisp.Empty`.
26
-
use <- bool.guard(when: response.body != wisp.Empty, return: response)
27
-
28
-
// You can use any logic to return appropriate responses depending on what is
29
-
// best for your application.
30
-
// I'm going to match on the status code and depending on what it is add
31
-
// different HTML as the body. This is a good option for most applications.
32
-
case response.status {
33
-
404 | 405 ->
34
-
"<h1>There's nothing here</h1>"
35
-
|> string_builder.from_string
36
-
|> wisp.html_body(response, _)
37
-
38
-
400 | 422 ->
39
-
"<h1>Bad request</h1>"
40
-
|> string_builder.from_string
41
-
|> wisp.html_body(response, _)
42
-
43
-
413 ->
44
-
"<h1>Request entity too large</h1>"
45
-
|> string_builder.from_string
46
-
|> wisp.html_body(response, _)
47
-
48
-
500 ->
49
-
"<h1>Internal server error</h1>"
50
-
|> string_builder.from_string
51
-
|> wisp.html_body(response, _)
52
-
53
-
// For other status codes redirect to the home page
54
-
_ -> wisp.redirect("/")
55
-
}
56
-
}
-17
examples/9-configuring-default-responses/src/app.gleam
-17
examples/9-configuring-default-responses/src/app.gleam
···
1
-
import gleam/erlang/process
2
-
import mist
3
-
import wisp
4
-
import app/router
5
-
6
-
pub fn main() {
7
-
wisp.configure_logger()
8
-
let secret_key_base = wisp.random_string(64)
9
-
10
-
let assert Ok(_) =
11
-
wisp.mist_handler(router.handle_request, secret_key_base)
12
-
|> mist.new
13
-
|> mist.port(8000)
14
-
|> mist.start_http
15
-
16
-
process.sleep_forever()
17
-
}
-115
examples/9-configuring-default-responses/test/app_test.gleam
-115
examples/9-configuring-default-responses/test/app_test.gleam
···
1
-
import gleeunit
2
-
import gleeunit/should
3
-
import gleam/string
4
-
import wisp/testing
5
-
import app/router
6
-
7
-
pub fn main() {
8
-
gleeunit.main()
9
-
}
10
-
11
-
pub fn home_test() {
12
-
let response = router.handle_request(testing.get("/", []))
13
-
14
-
response.status
15
-
|> should.equal(200)
16
-
17
-
response.headers
18
-
|> should.equal([#("content-type", "text/html")])
19
-
20
-
let assert True =
21
-
response
22
-
|> testing.string_body
23
-
|> string.contains("<h1>Hello, Joe!</h1>")
24
-
}
25
-
26
-
pub fn internal_server_error_test() {
27
-
let response =
28
-
router.handle_request(testing.get("/internal-server-error", []))
29
-
30
-
response.status
31
-
|> should.equal(500)
32
-
33
-
response.headers
34
-
|> should.equal([#("content-type", "text/html")])
35
-
36
-
let assert True =
37
-
response
38
-
|> testing.string_body
39
-
|> string.contains("<h1>Internal server error</h1>")
40
-
}
41
-
42
-
pub fn unprocessable_entity_test() {
43
-
let response = router.handle_request(testing.get("/unprocessable-entity", []))
44
-
45
-
response.status
46
-
|> should.equal(422)
47
-
48
-
response.headers
49
-
|> should.equal([#("content-type", "text/html")])
50
-
51
-
let assert True =
52
-
response
53
-
|> testing.string_body
54
-
|> string.contains("<h1>Bad request</h1>")
55
-
}
56
-
57
-
pub fn bad_request_test() {
58
-
let response = router.handle_request(testing.get("/bad-request", []))
59
-
60
-
response.status
61
-
|> should.equal(400)
62
-
63
-
response.headers
64
-
|> should.equal([#("content-type", "text/html")])
65
-
66
-
let assert True =
67
-
response
68
-
|> testing.string_body
69
-
|> string.contains("<h1>Bad request</h1>")
70
-
}
71
-
72
-
pub fn method_not_allowed_test() {
73
-
let response = router.handle_request(testing.get("/method-not-allowed", []))
74
-
75
-
response.status
76
-
|> should.equal(405)
77
-
78
-
response.headers
79
-
|> should.equal([#("allow", ""), #("content-type", "text/html")])
80
-
81
-
let assert True =
82
-
response
83
-
|> testing.string_body
84
-
|> string.contains("<h1>There's nothing here</h1>")
85
-
}
86
-
87
-
pub fn not_found_test() {
88
-
let response = router.handle_request(testing.get("/not-found", []))
89
-
90
-
response.status
91
-
|> should.equal(404)
92
-
93
-
response.headers
94
-
|> should.equal([#("content-type", "text/html")])
95
-
96
-
let assert True =
97
-
response
98
-
|> testing.string_body
99
-
|> string.contains("<h1>There's nothing here</h1>")
100
-
}
101
-
102
-
pub fn entity_too_large_test() {
103
-
let response = router.handle_request(testing.get("/entity-too-large", []))
104
-
105
-
response.status
106
-
|> should.equal(413)
107
-
108
-
response.headers
109
-
|> should.equal([#("content-type", "text/html")])
110
-
111
-
let assert True =
112
-
response
113
-
|> testing.string_body
114
-
|> string.contains("<h1>Request entity too large</h1>")
115
-
}
+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" }
+8
-8
examples/utilities/tiny_database/src/tiny_database.gleam
+8
-8
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/map.{type Map}
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)
···
41
41
42
42
pub fn insert(
43
43
connection: Connection,
44
-
values: Map(String, String),
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
-
|> map.to_list
50
+
|> dict.to_list
51
51
|> list.map(fn(pair) { #(pair.0, json.string(pair.1)) })
52
52
let json = json.to_string(json.object(values))
53
53
use _ <- result.try(
···
60
60
pub fn read(
61
61
connection: Connection,
62
62
id: String,
63
-
) -> Result(Map(String, String), Nil) {
63
+
) -> Result(Dict(String, String), Nil) {
64
64
use data <- result.try(
65
65
simplifile.read(file_path(connection, id))
66
66
|> result.nil_error,
67
67
)
68
68
69
-
let decoder = dynamic.map(dynamic.string, dynamic.string)
69
+
let decoder = dynamic.dict(dynamic.string, dynamic.string)
70
70
71
71
use data <- result.try(
72
72
json.decode(data, decoder)
+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
-15
gleam.toml
+15
-15
gleam.toml
···
1
1
name = "wisp"
2
-
version = "0.11.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 = "~> 1.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"
11
+
exception = ">= 2.0.0 and < 3.0.0"
12
+
gleam_crypto = ">= 1.0.0 and < 2.0.0"
13
+
gleam_erlang = ">= 0.21.0 and < 2.0.0"
14
+
gleam_http = ">= 3.5.0 and < 4.0.0"
15
+
gleam_json = ">= 0.6.0 and < 3.0.0"
16
+
gleam_stdlib = ">= 0.43.0 and < 2.0.0"
17
+
mist = ">= 1.2.0 and < 4.0.0"
18
+
simplifile = ">= 2.0.0 and < 3.0.0"
19
+
marceau = ">= 1.1.0 and < 2.0.0"
20
+
logging = ">= 1.2.0 and < 2.0.0"
21
+
directories = ">= 1.0.0 and < 2.0.0"
22
22
23
23
[dev-dependencies]
24
-
gleeunit = "~> 1.0"
24
+
gleeunit = ">= 1.0.0 and < 2.0.0"
+34
-23
manifest.toml
+34
-23
manifest.toml
···
2
2
# You typically do not need to edit this file
3
3
4
4
packages = [
5
-
{ name = "exception", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "984401CFC95BCA87C391E36194D2B9E5B946467D44893FADB1CA4ACD4B7A29CE" },
6
-
{ name = "gleam_crypto", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "DE1FC4E631CA374AB29CCAEAC043EE171B86114D7DC66DD483F0A93BF0C4C6FF" },
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.34.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1FB8454D2991E9B4C0C804544D8A9AD0F6184725E20D63C3155F0AEB4230B016" },
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.9.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "C960B6CF25D4AABAB01211146E9B57E11827B9C49E4175217E0FB7EF5BCB0FF7" },
14
-
{ name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" },
15
-
{ name = "mist", version = "0.15.0", build_tools = ["gleam"], requirements = ["gleam_otp", "gleam_stdlib", "gleam_http", "glisten", "gleam_erlang"], otp_app = "mist", source = "hex", outer_checksum = "49F51DDB64D7B2832F72727CC9721C478D6B524C96EA444C601A19D01E023C03" },
16
-
{ name = "simplifile", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "AAFCF154F69B237D269FF2764890F61ABC4A7EF2A592D44D67627B99694539D9" },
17
-
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
5
+
{ name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
6
+
{ name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" },
7
+
{ name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" },
8
+
{ name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
9
+
{ name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" },
10
+
{ name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" },
11
+
{ name = "gleam_erlang", version = "0.30.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "760618870AE4A497B10C73548E6E44F43B76292A54F0207B3771CBB599C675B4" },
12
+
{ name = "gleam_http", version = "3.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "A9EE0722106FCCAB8AD3BF9D0A3EFF92BFE8561D59B83BAE96EB0BE1938D4E0F" },
13
+
{ name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" },
14
+
{ name = "gleam_otp", version = "0.14.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5A8CE8DBD01C29403390A7BD5C0A63D26F865C83173CF9708E6E827E53159C65" },
15
+
{ name = "gleam_stdlib", version = "0.43.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "69EF22E78FDCA9097CBE7DF91C05B2A8B5436826D9F66680D879182C0860A747" },
16
+
{ name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
17
+
{ name = "glisten", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "912132751031473CB38F454120124FFC96AF6B0EA33D92C9C90DB16327A2A972" },
18
+
{ name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
19
+
{ name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
20
+
{ name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" },
21
+
{ name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" },
22
+
{ name = "mist", version = "3.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "CDA1A74E768419235E16886463EC4722EFF4AB3F8D820A76EAD45D7C167D7282" },
23
+
{ name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" },
24
+
{ name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
25
+
{ name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" },
26
+
{ name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" },
18
27
]
19
28
20
29
[requirements]
21
-
exception = { version = "~> 1.0" }
22
-
gleam_crypto = { version = "~> 1.0" }
23
-
gleam_erlang = { version = "~> 0.21" }
24
-
gleam_http = { version = "~> 3.5" }
25
-
gleam_json = { version = "~> 0.6 or ~> 1.0" }
26
-
gleam_stdlib = { version = "~> 0.29 or ~> 1.0" }
27
-
gleeunit = { version = "~> 1.0" }
28
-
marceau = { version = "~> 1.1" }
29
-
mist = { version = "~> 0.13" }
30
-
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" }
-34
src/wisp/internal/logger.gleam
-34
src/wisp/internal/logger.gleam
···
1
-
import gleam/dict.{type Dict}
2
-
import gleam/erlang/atom.{type Atom}
3
-
import gleam/dynamic.{type Dynamic}
4
-
5
-
pub type LogLevel {
6
-
Emergency
7
-
Alert
8
-
Critical
9
-
Error
10
-
Warning
11
-
Notice
12
-
Info
13
-
Debug
14
-
}
15
-
16
-
type DoNotLeak
17
-
18
-
pub fn configure_logger() -> Nil {
19
-
update_primary_config(
20
-
dict.from_list([#(atom.create_from_string("level"), dynamic.from(Info))]),
21
-
)
22
-
Nil
23
-
}
24
-
25
-
@external(erlang, "logger", "update_primary_config")
26
-
fn update_primary_config(config: Dict(Atom, Dynamic)) -> DoNotLeak
27
-
28
-
pub fn log(level: LogLevel, message: String) -> Nil {
29
-
erlang_log(level, message)
30
-
Nil
31
-
}
32
-
33
-
@external(erlang, "logger", "log")
34
-
fn erlang_log(level: LogLevel, message: String) -> DoNotLeak
+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
+
}
+11
-6
src/wisp/testing.gleam
+11
-6
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
-
import wisp.{type Request, type Response, Empty, File, Text}
12
+
import wisp.{type Request, type Response, Bytes, Empty, File, Text}
13
13
14
14
/// The default secret key base used for test requests.
15
15
/// This should never be used outside of tests.
···
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
+
Bytes(bytes) -> {
232
+
let data = bytes_tree.to_bit_array(bytes)
233
+
let assert Ok(string) = bit_array.to_string(data)
234
+
string
235
+
}
231
236
File(path) -> {
232
237
let assert Ok(contents) = simplifile.read(path)
233
238
contents
···
245
250
pub fn bit_array_body(response: Response) -> BitArray {
246
251
case response.body {
247
252
Empty -> <<>>
248
-
Text(builder) ->
249
-
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))
250
255
File(path) -> {
251
256
let assert Ok(contents) = simplifile.read_bits(path)
252
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
+
}
+463
-316
src/wisp.gleam
+463
-316
src/wisp.gleam
···
1
1
import exception
2
-
import gleam/bytes_builder
3
2
import gleam/bit_array
4
3
import gleam/bool
4
+
import gleam/bytes_tree.{type BytesTree}
5
5
import gleam/crypto
6
+
import gleam/dict.{type Dict}
6
7
import gleam/dynamic.{type Dynamic}
7
8
import gleam/erlang
9
+
import gleam/erlang/atom.{type Atom}
8
10
import gleam/http.{type Method}
9
11
import gleam/http/cookie
10
12
import gleam/http/request.{type Request as HttpRequest}
···
17
19
import gleam/option.{type Option}
18
20
import gleam/result
19
21
import gleam/string
20
-
import gleam/string_builder.{type StringBuilder}
22
+
import gleam/string_tree.{type StringTree}
21
23
import gleam/uri
24
+
import logging
22
25
import marceau
23
-
import mist
24
26
import simplifile
25
-
import wisp/internal/logger
26
-
27
-
//
28
-
// Running the server
29
-
//
30
-
31
-
/// Convert a Wisp request handler into a function that can be run with the Mist
32
-
/// web server.
33
-
///
34
-
/// # Examples
35
-
///
36
-
/// ```gleam
37
-
/// pub fn main() {
38
-
/// let secret_key_base = "..."
39
-
/// let assert Ok(_) =
40
-
/// handle_request
41
-
/// |> wisp.mist_handler(secret_key_base)
42
-
/// |> mist.new
43
-
/// |> mist.port(8000)
44
-
/// |> mist.start_http
45
-
/// process.sleep_forever()
46
-
/// }
47
-
/// ```
48
-
pub fn mist_handler(
49
-
handler: fn(Request) -> Response,
50
-
secret_key_base: String,
51
-
) -> fn(HttpRequest(mist.Connection)) -> HttpResponse(mist.ResponseData) {
52
-
fn(request: HttpRequest(_)) {
53
-
let connection = make_connection(mist_body_reader(request), secret_key_base)
54
-
let request = request.set_body(request, connection)
55
-
56
-
use <- exception.defer(fn() {
57
-
let assert Ok(_) = delete_temporary_files(request)
58
-
})
59
-
60
-
let response =
61
-
request
62
-
|> handler
63
-
|> mist_response
64
-
65
-
response
66
-
}
67
-
}
68
-
69
-
fn mist_body_reader(request: HttpRequest(mist.Connection)) -> Reader {
70
-
case mist.stream(request) {
71
-
Error(_) -> fn(_) { Ok(ReadingFinished) }
72
-
Ok(stream) -> fn(size) { wrap_mist_chunk(stream(size)) }
73
-
}
74
-
}
75
-
76
-
fn wrap_mist_chunk(
77
-
chunk: Result(mist.Chunk, mist.ReadError),
78
-
) -> Result(Read, Nil) {
79
-
chunk
80
-
|> result.nil_error
81
-
|> result.map(fn(chunk) {
82
-
case chunk {
83
-
mist.Done -> ReadingFinished
84
-
mist.Chunk(data, consume) ->
85
-
Chunk(data, fn(size) { wrap_mist_chunk(consume(size)) })
86
-
}
87
-
})
88
-
}
89
-
90
-
fn mist_response(response: Response) -> HttpResponse(mist.ResponseData) {
91
-
let body = case response.body {
92
-
Empty -> mist.Bytes(bytes_builder.new())
93
-
Text(text) -> mist.Bytes(bytes_builder.from_string_builder(text))
94
-
File(path) -> mist_send_file(path)
95
-
}
96
-
response
97
-
|> response.set_body(body)
98
-
}
99
-
100
-
fn mist_send_file(path: String) -> mist.ResponseData {
101
-
case mist.send_file(path, offset: 0, limit: option.None) {
102
-
Ok(body) -> body
103
-
Error(error) -> {
104
-
log_error(string.inspect(error))
105
-
// TODO: return 500
106
-
mist.Bytes(bytes_builder.new())
107
-
}
108
-
}
109
-
}
27
+
import wisp/internal
110
28
111
29
//
112
30
// Responses
···
117
35
pub type Body {
118
36
/// A body of unicode text.
119
37
///
120
-
/// The body is represented using a `StringBuilder`. If you have a `String`
121
-
/// 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.
40
+
///
41
+
Text(StringTree)
42
+
/// A body of binary data.
122
43
///
123
-
Text(StringBuilder)
44
+
/// The body is represented using a `BytesTree`. If you have a `BitArray`
45
+
/// you can use the `bytes_tree.from_bit_array` function to convert it.
46
+
///
47
+
Bytes(BytesTree)
124
48
/// A body of the contents of a file.
125
49
///
126
50
/// This will be sent efficiently using the `send_file` function of the
···
143
67
HttpResponse(Body)
144
68
145
69
/// Create an empty response with the given status code.
146
-
///
70
+
///
147
71
/// # Examples
148
-
///
72
+
///
149
73
/// ```gleam
150
74
/// response(200)
151
75
/// // -> Response(200, [], Empty)
152
76
/// ```
153
-
///
77
+
///
154
78
pub fn response(status: Int) -> Response {
155
79
HttpResponse(status, [], Empty)
156
80
}
157
81
158
82
/// Set the body of a response.
159
-
///
83
+
///
160
84
/// # Examples
161
-
///
85
+
///
162
86
/// ```gleam
163
87
/// response(200)
164
88
/// |> set_body(File("/tmp/myfile.txt"))
165
89
/// // -> Response(200, [], File("/tmp/myfile.txt"))
166
90
/// ```
167
-
///
91
+
///
168
92
pub fn set_body(response: Response, body: Body) -> Response {
169
93
response
170
94
|> response.set_body(body)
171
95
}
172
96
97
+
/// Send a file from the disc as a file download.
98
+
///
99
+
/// The operating system `send_file` function is used to efficiently send the
100
+
/// file over the network socket without reading the entire file into memory.
101
+
///
102
+
/// The `content-disposition` header will be set to `attachment;
103
+
/// filename="name"` to ensure the file is downloaded by the browser. This is
104
+
/// especially good for files that the browser would otherwise attempt to open
105
+
/// as this can result in cross-site scripting vulnerabilities.
106
+
///
107
+
/// If you wish to not set the `content-disposition` header you could use the
108
+
/// `set_body` function with the `File` body variant.
109
+
///
110
+
/// # Examples
111
+
///
112
+
/// ```gleam
113
+
/// response(200)
114
+
/// |> file_download(named: "myfile.txt", from: "/tmp/myfile.txt")
115
+
/// // -> Response(
116
+
/// // 200,
117
+
/// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")],
118
+
/// // File("/tmp/myfile.txt"),
119
+
/// // )
120
+
/// ```
121
+
///
122
+
pub fn file_download(
123
+
response: Response,
124
+
named name: String,
125
+
from path: String,
126
+
) -> Response {
127
+
let name = uri.percent_encode(name)
128
+
response
129
+
|> response.set_header(
130
+
"content-disposition",
131
+
"attachment; filename=\"" <> name <> "\"",
132
+
)
133
+
|> response.set_body(File(path))
134
+
}
135
+
136
+
/// Send a file from memory as a file download.
137
+
///
138
+
/// If your file is already on the disc use `file_download` instead, to avoid
139
+
/// having to read the file into memory to send it.
140
+
///
141
+
/// The `content-disposition` header will be set to `attachment;
142
+
/// filename="name"` to ensure the file is downloaded by the browser. This is
143
+
/// especially good for files that the browser would otherwise attempt to open
144
+
/// as this can result in cross-site scripting vulnerabilities.
145
+
///
146
+
/// # Examples
147
+
///
148
+
/// ```gleam
149
+
/// let content = bytes_tree.from_string("Hello, Joe!")
150
+
/// response(200)
151
+
/// |> file_download_from_memory(named: "myfile.txt", containing: content)
152
+
/// // -> Response(
153
+
/// // 200,
154
+
/// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")],
155
+
/// // File("/tmp/myfile.txt"),
156
+
/// // )
157
+
/// ```
158
+
///
159
+
pub fn file_download_from_memory(
160
+
response: Response,
161
+
named name: String,
162
+
containing data: BytesTree,
163
+
) -> Response {
164
+
let name = uri.percent_encode(name)
165
+
response
166
+
|> response.set_header(
167
+
"content-disposition",
168
+
"attachment; filename=\"" <> name <> "\"",
169
+
)
170
+
|> response.set_body(Bytes(data))
171
+
}
172
+
173
173
/// Create a HTML response.
174
-
///
174
+
///
175
175
/// The body is expected to be valid HTML, though this is not validated.
176
176
/// The `content-type` header will be set to `text/html`.
177
-
///
177
+
///
178
178
/// # Examples
179
-
///
179
+
///
180
180
/// ```gleam
181
-
/// let body = string_builder.from_string("<h1>Hello, Joe!</h1>")
181
+
/// let body = string_tree.from_string("<h1>Hello, Joe!</h1>")
182
182
/// html_response(body, 200)
183
183
/// // -> Response(200, [#("content-type", "text/html")], Text(body))
184
184
/// ```
185
-
///
186
-
pub fn html_response(html: StringBuilder, status: Int) -> Response {
187
-
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
+
)
188
192
}
189
193
190
194
/// Create a JSON response.
191
-
///
195
+
///
192
196
/// The body is expected to be valid JSON, though this is not validated.
193
197
/// The `content-type` header will be set to `application/json`.
194
-
///
198
+
///
195
199
/// # Examples
196
-
///
200
+
///
197
201
/// ```gleam
198
-
/// let body = string_builder.from_string("{\"name\": \"Joe\"}")
202
+
/// let body = string_tree.from_string("{\"name\": \"Joe\"}")
199
203
/// json_response(body, 200)
200
204
/// // -> Response(200, [#("content-type", "application/json")], Text(body))
201
205
/// ```
202
-
///
203
-
pub fn json_response(json: StringBuilder, status: Int) -> Response {
204
-
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
+
)
205
213
}
206
214
207
215
/// Set the body of a response to a given HTML document, and set the
208
216
/// `content-type` header to `text/html`.
209
-
///
217
+
///
210
218
/// The body is expected to be valid HTML, though this is not validated.
211
-
///
219
+
///
212
220
/// # Examples
213
-
///
221
+
///
214
222
/// ```gleam
215
-
/// let body = string_builder.from_string("<h1>Hello, Joe!</h1>")
223
+
/// let body = string_tree.from_string("<h1>Hello, Joe!</h1>")
216
224
/// response(201)
217
225
/// |> html_body(body)
218
-
/// // -> Response(201, [#("content-type", "text/html")], Text(body))
226
+
/// // -> Response(201, [#("content-type", "text/html; charset=utf-8")], Text(body))
219
227
/// ```
220
-
///
221
-
pub fn html_body(response: Response, html: StringBuilder) -> Response {
228
+
///
229
+
pub fn html_body(response: Response, html: StringTree) -> Response {
222
230
response
223
231
|> response.set_body(Text(html))
224
-
|> response.set_header("content-type", "text/html")
232
+
|> response.set_header("content-type", "text/html; charset=utf-8")
225
233
}
226
234
227
235
/// Set the body of a response to a given JSON document, and set the
228
236
/// `content-type` header to `application/json`.
229
-
///
237
+
///
230
238
/// The body is expected to be valid JSON, though this is not validated.
231
-
///
239
+
///
232
240
/// # Examples
233
-
///
241
+
///
234
242
/// ```gleam
235
-
/// let body = string_builder.from_string("{\"name\": \"Joe\"}")
243
+
/// let body = string_tree.from_string("{\"name\": \"Joe\"}")
236
244
/// response(201)
237
245
/// |> json_body(body)
238
-
/// // -> Response(201, [#("content-type", "application/json")], Text(body))
246
+
/// // -> Response(201, [#("content-type", "application/json; charset=utf-8")], Text(body))
239
247
/// ```
240
-
///
241
-
pub fn json_body(response: Response, json: StringBuilder) -> Response {
248
+
///
249
+
pub fn json_body(response: Response, json: StringTree) -> Response {
242
250
response
243
251
|> response.set_body(Text(json))
244
-
|> response.set_header("content-type", "application/json")
252
+
|> response.set_header("content-type", "application/json; charset=utf-8")
245
253
}
246
254
247
-
/// Set the body of a response to a given string builder.
255
+
/// Set the body of a response to a given string tree.
248
256
///
249
257
/// You likely want to also set the request `content-type` header to an
250
258
/// appropriate value for the format of the content.
251
259
///
252
260
/// # Examples
253
-
///
261
+
///
254
262
/// ```gleam
255
-
/// let body = string_builder.from_string("Hello, Joe!")
263
+
/// let body = string_tree.from_string("Hello, Joe!")
256
264
/// response(201)
257
-
/// |> string_builder_body(body)
265
+
/// |> string_tree_body(body)
258
266
/// // -> Response(201, [], Text(body))
259
267
/// ```
260
-
///
261
-
pub fn string_builder_body(
262
-
response: Response,
263
-
content: StringBuilder,
264
-
) -> Response {
268
+
///
269
+
pub fn string_tree_body(response: Response, content: StringTree) -> Response {
265
270
response
266
271
|> response.set_body(Text(content))
267
272
}
268
273
269
-
/// Set the body of a response to a given string builder.
274
+
/// Set the body of a response to a given string.
270
275
///
271
276
/// You likely want to also set the request `content-type` header to an
272
277
/// appropriate value for the format of the content.
273
278
///
274
279
/// # Examples
275
-
///
280
+
///
276
281
/// ```gleam
277
-
/// let body =
282
+
/// let body =
278
283
/// response(201)
279
-
/// |> string_builder_body("Hello, Joe!")
284
+
/// |> string_body("Hello, Joe!")
280
285
/// // -> Response(
281
286
/// // 201,
282
287
/// // [],
283
-
/// // Text(string_builder.from_string("Hello, Joe"))
288
+
/// // Text(string_tree.from_string("Hello, Joe"))
284
289
/// // )
285
290
/// ```
286
-
///
291
+
///
287
292
pub fn string_body(response: Response, content: String) -> Response {
288
293
response
289
-
|> response.set_body(Text(string_builder.from_string(content)))
294
+
|> response.set_body(Text(string_tree.from_string(content)))
290
295
}
291
296
292
297
/// Escape a string so that it can be safely included in a HTML document.
···
300
305
/// escape_html("<h1>Hello, Joe!</h1>")
301
306
/// // -> "<h1>Hello, Joe!</h1>"
302
307
/// ```
303
-
///
308
+
///
304
309
pub fn escape_html(content: String) -> String {
305
-
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
319
+
}
320
+
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
+
}
306
368
}
307
369
308
-
fn do_escape_html(escaped: String, content: String) -> String {
309
-
case string.pop_grapheme(content) {
310
-
Ok(#("<", xs)) -> do_escape_html(escaped <> "<", xs)
311
-
Ok(#(">", xs)) -> do_escape_html(escaped <> ">", xs)
312
-
Ok(#("&", xs)) -> do_escape_html(escaped <> "&", xs)
313
-
Ok(#(x, xs)) -> do_escape_html(escaped <> x, xs)
314
-
Error(_) -> escaped <> content
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"
315
433
}
316
434
}
317
435
···
509
627
//
510
628
511
629
/// The connection to the client for a HTTP request.
512
-
///
630
+
///
513
631
/// The body of the request can be read from this connection using functions
514
632
/// such as `require_multipart_body`.
515
-
///
516
-
pub opaque type Connection {
517
-
Connection(
518
-
reader: Reader,
519
-
max_body_size: Int,
520
-
max_files_size: Int,
521
-
read_chunk_size: Int,
522
-
secret_key_base: String,
523
-
temporary_directory: String,
524
-
)
525
-
}
526
-
527
-
fn make_connection(body_reader: Reader, secret_key_base: String) -> Connection {
528
-
// TODO: replace `/tmp` with appropriate for the OS
529
-
let prefix = "/tmp/gleam-wisp/"
530
-
let temporary_directory = join_path(prefix, random_slug())
531
-
Connection(
532
-
reader: body_reader,
533
-
max_body_size: 8_000_000,
534
-
max_files_size: 32_000_000,
535
-
read_chunk_size: 1_000_000,
536
-
temporary_directory: temporary_directory,
537
-
secret_key_base: secret_key_base,
538
-
)
539
-
}
633
+
///
634
+
pub type Connection =
635
+
internal.Connection
540
636
541
637
type BufferedReader {
542
-
BufferedReader(reader: Reader, buffer: BitArray)
638
+
BufferedReader(reader: internal.Reader, buffer: BitArray)
543
639
}
544
640
545
641
type Quotas {
···
561
657
}
562
658
}
563
659
564
-
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) {
565
664
case reader.buffer {
566
665
<<>> -> reader.reader(chunk_size)
567
-
_ -> Ok(Chunk(reader.buffer, reader.reader))
666
+
_ -> Ok(internal.Chunk(reader.buffer, reader.reader))
568
667
}
569
668
}
570
669
571
-
type Reader =
572
-
fn(Int) -> Result(Read, Nil)
573
-
574
-
type Read {
575
-
Chunk(BitArray, next: Reader)
576
-
ReadingFinished
577
-
}
578
-
579
670
/// Set the maximum permitted size of a request body of the request in bytes.
580
671
///
581
672
/// If a body is larger than this size attempting to read the body will result
···
587
678
/// instead use the `max_files_size` limit.
588
679
///
589
680
pub fn set_max_body_size(request: Request, size: Int) -> Request {
590
-
Connection(..request.body, max_body_size: size)
681
+
internal.Connection(..request.body, max_body_size: size)
591
682
|> request.set_body(request, _)
592
683
}
593
684
594
685
/// Get the maximum permitted size of a request body of the request in bytes.
595
-
///
686
+
///
596
687
pub fn get_max_body_size(request: Request) -> Int {
597
688
request.body.max_body_size
598
689
}
599
690
600
691
/// Set the secret key base used to sign cookies and other sensitive data.
601
-
///
692
+
///
602
693
/// This key must be at least 64 bytes long and should be kept secret. Anyone
603
694
/// with this secret will be able to manipulate signed cookies and other sensitive
604
695
/// data.
605
696
///
606
697
/// # Panics
607
-
///
698
+
///
608
699
/// This function will panic if the key is less than 64 bytes long.
609
700
///
610
701
pub fn set_secret_key_base(request: Request, key: String) -> Request {
611
702
case string.byte_size(key) < 64 {
612
703
True -> panic as "Secret key base must be at least 64 bytes long"
613
704
False ->
614
-
Connection(..request.body, secret_key_base: key)
705
+
internal.Connection(..request.body, secret_key_base: key)
615
706
|> request.set_body(request, _)
616
707
}
617
708
}
618
709
619
710
/// Get the secret key base used to sign cookies and other sensitive data.
620
-
///
711
+
///
621
712
pub fn get_secret_key_base(request: Request) -> String {
622
713
request.body.secret_key_base
623
714
}
···
630
721
///
631
722
/// This limit only applies for files in a multipart body that get streamed to
632
723
/// disc. For headers and other content that gets read into memory use the
633
-
/// `max_files_size` limit.
724
+
/// `max_body_size` limit.
634
725
///
635
726
pub fn set_max_files_size(request: Request, size: Int) -> Request {
636
-
Connection(..request.body, max_files_size: size)
727
+
internal.Connection(..request.body, max_files_size: size)
637
728
|> request.set_body(request, _)
638
729
}
639
730
640
731
/// Get the maximum permitted total size of a files uploaded by a request in
641
732
/// bytes.
642
-
///
733
+
///
643
734
pub fn get_max_files_size(request: Request) -> Int {
644
735
request.body.max_files_size
645
736
}
···
653
744
/// been received from the client.
654
745
///
655
746
pub fn set_read_chunk_size(request: Request, size: Int) -> Request {
656
-
Connection(..request.body, read_chunk_size: size)
747
+
internal.Connection(..request.body, read_chunk_size: size)
657
748
|> request.set_body(request, _)
658
749
}
659
750
660
751
/// Get the size limit for each chunk of the request body when read from the
661
752
/// client.
662
-
///
753
+
///
663
754
pub fn get_read_chunk_size(request: Request) -> Int {
664
755
request.body.read_chunk_size
665
756
}
666
757
667
758
/// A convenient alias for a HTTP request with a Wisp connection as the body.
668
-
///
759
+
///
669
760
pub type Request =
670
-
HttpRequest(Connection)
761
+
HttpRequest(internal.Connection)
671
762
672
763
/// This middleware function ensures that the request has a specific HTTP
673
764
/// method, returning an empty response with status code 405: Method not allowed
674
765
/// if the method is not correct.
675
766
///
676
767
/// # Examples
677
-
///
768
+
///
678
769
/// ```gleam
679
770
/// fn handle_request(request: Request) -> Response {
680
771
/// use <- wisp.require_method(request, http.Patch)
···
695
786
696
787
// TODO: re-export once Gleam has a syntax for that
697
788
/// Return the non-empty segments of a request path.
698
-
///
789
+
///
699
790
/// # Examples
700
791
///
701
792
/// ```gleam
···
709
800
710
801
// TODO: re-export once Gleam has a syntax for that
711
802
/// Set a given header to a given value, replacing any existing value.
712
-
///
803
+
///
713
804
/// # Examples
714
805
///
715
806
/// ```gleam
···
779
870
/// return an incorrect value, depending on the underlying web server. It is the
780
871
/// responsibility of the caller to cache the body if it is needed multiple
781
872
/// times.
782
-
///
873
+
///
783
874
/// If the body is larger than the `max_body_size` limit then an empty response
784
875
/// with status code 413: Entity too large will be returned to the client.
785
-
///
876
+
///
786
877
/// If the body is found not to be valid UTF-8 then an empty response with
787
878
/// status code 400: Bad request will be returned to the client.
788
-
///
879
+
///
789
880
/// # Examples
790
881
///
791
882
/// ```gleam
···
815
906
/// return an incorrect value, depending on the underlying web server. It is the
816
907
/// responsibility of the caller to cache the body if it is needed multiple
817
908
/// times.
818
-
///
909
+
///
819
910
/// If the body is larger than the `max_body_size` limit then an empty response
820
911
/// with status code 413: Entity too large will be returned to the client.
821
-
///
912
+
///
822
913
/// # Examples
823
914
///
824
915
/// ```gleam
···
841
932
// TODO: don't always return entity to large. Other errors are possible, such as
842
933
// network errors.
843
934
/// Read the entire body of the request as a bit string.
844
-
///
935
+
///
845
936
/// You may instead wish to use the `require_bit_array_body` or the
846
937
/// `require_string_body` middleware functions instead.
847
-
///
938
+
///
848
939
/// This function does not cache the body in any way, so if you call this
849
940
/// function (or any other body reading function) more than once it may hang or
850
941
/// return an incorrect value, depending on the underlying web server. It is the
851
942
/// responsibility of the caller to cache the body if it is needed multiple
852
943
/// times.
853
-
///
944
+
///
854
945
/// If the body is larger than the `max_body_size` limit then an empty response
855
946
/// with status code 413: Entity too large will be returned to the client.
856
-
///
947
+
///
857
948
pub fn read_body_to_bitstring(request: Request) -> Result(BitArray, Nil) {
858
949
let connection = request.body
859
950
read_body_loop(
···
865
956
}
866
957
867
958
fn read_body_loop(
868
-
reader: Reader,
959
+
reader: internal.Reader,
869
960
read_chunk_size: Int,
870
961
max_body_size: Int,
871
962
accumulator: BitArray,
872
963
) -> Result(BitArray, Nil) {
873
964
use chunk <- result.try(reader(read_chunk_size))
874
965
case chunk {
875
-
ReadingFinished -> Ok(accumulator)
876
-
Chunk(chunk, next) -> {
966
+
internal.ReadingFinished -> Ok(accumulator)
967
+
internal.Chunk(chunk, next) -> {
877
968
let accumulator = bit_array.append(accumulator, chunk)
878
969
case bit_array.byte_size(accumulator) > max_body_size {
879
970
True -> Error(Nil)
···
887
978
/// A middleware which extracts form data from the body of a request that is
888
979
/// encoded as either `application/x-www-form-urlencoded` or
889
980
/// `multipart/form-data`.
890
-
///
981
+
///
891
982
/// Extracted fields are sorted into alphabetical order by key, so if you wish
892
983
/// to use pattern matching the order can be relied upon.
893
-
///
984
+
///
894
985
/// ```gleam
895
986
/// fn handle_request(request: Request) -> Response {
896
987
/// use form <- wisp.require_form(request)
···
919
1010
///
920
1011
/// If the body cannot be parsed successfully then an empty response with status
921
1012
/// code 400: Bad request will be returned to the client.
922
-
///
1013
+
///
923
1014
pub fn require_form(
924
1015
request: Request,
925
1016
next: fn(FormData) -> Response,
···
946
1037
/// Unsupported media type if the header is not the expected value
947
1038
///
948
1039
/// # Examples
949
-
///
1040
+
///
950
1041
/// ```gleam
951
1042
/// fn handle_request(request: Request) -> Response {
952
1043
/// use <- wisp.require_content_type(request, "application/json")
···
960
1051
next: fn() -> Response,
961
1052
) -> Response {
962
1053
case list.key_find(request.headers, "content-type") {
963
-
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
+
964
1063
_ -> unsupported_media_type([expected])
965
1064
}
966
1065
}
967
1066
968
1067
/// A middleware which extracts JSON from the body of a request.
969
-
///
1068
+
///
970
1069
/// ```gleam
971
1070
/// fn handle_request(request: Request) -> Response {
972
1071
/// use json <- wisp.require_json(request)
···
987
1086
///
988
1087
/// If the body cannot be parsed successfully then an empty response with status
989
1088
/// code 400: Bad request will be returned to the client.
990
-
///
1089
+
///
991
1090
pub fn require_json(request: Request, next: fn(Dynamic) -> Response) -> Response {
992
1091
use <- require_content_type(request, "application/json")
993
1092
use body <- require_string_body(request)
···
1171
1270
fn read_chunk(
1172
1271
reader: BufferedReader,
1173
1272
chunk_size: Int,
1174
-
) -> Result(#(BitArray, Reader), Response) {
1273
+
) -> Result(#(BitArray, internal.Reader), Response) {
1175
1274
buffered_read(reader, chunk_size)
1176
1275
|> result.replace_error(bad_request())
1177
1276
|> result.try(fn(chunk) {
1178
1277
case chunk {
1179
-
Chunk(chunk, next) -> Ok(#(chunk, next))
1180
-
ReadingFinished -> Error(bad_request())
1278
+
internal.Chunk(chunk, next) -> Ok(#(chunk, next))
1279
+
internal.ReadingFinished -> Error(bad_request())
1181
1280
}
1182
1281
})
1183
1282
}
···
1221
1320
}
1222
1321
1223
1322
/// Data parsed from form sent in a request's body.
1224
-
///
1323
+
///
1225
1324
pub type FormData {
1226
1325
FormData(
1227
1326
/// String values of the form's fields.
···
1260
1359
/// ```
1261
1360
///
1262
1361
pub fn rescue_crashes(handler: fn() -> Response) -> Response {
1263
-
case erlang.rescue(handler) {
1362
+
case exception.rescue(handler) {
1264
1363
Ok(response) -> response
1265
1364
Error(error) -> {
1266
-
log_error(string.inspect(error))
1365
+
let #(kind, detail) = case error {
1366
+
exception.Errored(detail) -> #(Errored, detail)
1367
+
exception.Thrown(detail) -> #(Thrown, detail)
1368
+
exception.Exited(detail) -> #(Exited, detail)
1369
+
}
1370
+
case dynamic.dict(atom.from_dynamic, Ok)(detail) {
1371
+
Ok(details) -> {
1372
+
let c = atom.create_from_string("class")
1373
+
log_error_dict(dict.insert(details, c, dynamic.from(kind)))
1374
+
Nil
1375
+
}
1376
+
Error(_) -> log_error(string.inspect(error))
1377
+
}
1267
1378
internal_server_error()
1268
1379
}
1269
1380
}
1381
+
}
1382
+
1383
+
type DoNotLeak
1384
+
1385
+
@external(erlang, "logger", "error")
1386
+
fn log_error_dict(o: Dict(Atom, Dynamic)) -> DoNotLeak
1387
+
1388
+
type ErrorKind {
1389
+
Errored
1390
+
Thrown
1391
+
Exited
1270
1392
}
1271
1393
1272
1394
// TODO: test, somehow.
···
1298
1420
response
1299
1421
}
1300
1422
1301
-
fn remove_preceeding_slashes(string: String) -> String {
1302
-
case string {
1303
-
"/" <> rest -> remove_preceeding_slashes(rest)
1304
-
_ -> string
1305
-
}
1306
-
}
1307
-
1308
-
// TODO: replace with simplifile function when it exists
1309
-
fn join_path(a: String, b: String) -> String {
1310
-
let b = remove_preceeding_slashes(b)
1311
-
case string.ends_with(a, "/") {
1312
-
True -> a <> b
1313
-
False -> a <> "/" <> b
1314
-
}
1315
-
}
1316
-
1317
1423
/// A middleware function that serves files from a directory, along with a
1318
1424
/// suitable `content-type` header for known file extensions.
1319
1425
///
···
1322
1428
///
1323
1429
/// The `under` parameter is the request path prefix that must match for the
1324
1430
/// file to be served.
1325
-
///
1431
+
///
1326
1432
/// | `under` | `from` | `request.path` | `file` |
1327
1433
/// |-----------|---------|--------------------|-------------------------|
1328
1434
/// | `/static` | `/data` | `/static/file.txt` | `/data/file.txt` |
···
1361
1467
from directory: String,
1362
1468
next handler: fn() -> Response,
1363
1469
) -> Response {
1364
-
let path = remove_preceeding_slashes(req.path)
1365
-
let prefix = remove_preceeding_slashes(prefix)
1470
+
let path = internal.remove_preceeding_slashes(req.path)
1471
+
let prefix = internal.remove_preceeding_slashes(prefix)
1366
1472
case req.method, string.starts_with(path, prefix) {
1367
1473
http.Get, True -> {
1368
1474
let path =
1369
1475
path
1370
-
|> string.drop_left(string.length(prefix))
1476
+
|> string.drop_start(string.length(prefix))
1371
1477
|> string.replace(each: "..", with: "")
1372
-
|> join_path(directory, _)
1478
+
|> internal.join_path(directory, _)
1373
1479
1374
1480
let mime_type =
1375
1481
req.path
···
1378
1484
|> result.unwrap("")
1379
1485
|> marceau.extension_to_mime_type
1380
1486
1381
-
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) {
1382
1493
Ok(True) ->
1383
1494
response.new(200)
1384
-
|> response.set_header("content-type", mime_type)
1495
+
|> response.set_header("content-type", content_type)
1385
1496
|> response.set_body(File(path))
1386
1497
_ -> handler()
1387
1498
}
···
1428
1539
1429
1540
/// Create a new temporary directory for the given request.
1430
1541
///
1431
-
/// 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
1432
1543
/// adapter then this file will be deleted for you when the request is complete.
1433
1544
/// Otherwise you will need to call the `delete_temporary_files` function
1434
1545
/// yourself.
···
1438
1549
) -> Result(String, simplifile.FileError) {
1439
1550
let directory = request.body.temporary_directory
1440
1551
use _ <- result.try(simplifile.create_directory_all(directory))
1441
-
let path = join_path(directory, random_slug())
1552
+
let path = internal.join_path(directory, internal.random_slug())
1442
1553
use _ <- result.map(simplifile.create_file(path))
1443
1554
path
1444
1555
}
1445
1556
1446
1557
/// Delete any temporary files created for the given request.
1447
1558
///
1448
-
/// 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
1449
1560
/// adapter then this file will be deleted for you when the request is complete.
1450
1561
/// Otherwise you will need to call this function yourself.
1451
1562
///
···
1469
1580
/// > erlang.priv_directory("my_app")
1470
1581
/// // -> Ok("/some/location/my_app/priv")
1471
1582
/// ```
1472
-
///
1583
+
///
1473
1584
pub const priv_directory = erlang.priv_directory
1474
1585
1475
1586
//
···
1478
1589
1479
1590
/// Configure the Erlang logger, setting the minimum log level to `info`, to be
1480
1591
/// called when your application starts.
1481
-
///
1592
+
///
1482
1593
/// You may wish to use an alternative for this such as one provided by a more
1483
1594
/// sophisticated logging library.
1484
-
///
1595
+
///
1485
1596
/// In future this function may be extended to change the output format.
1486
-
///
1597
+
///
1487
1598
pub fn configure_logger() -> Nil {
1488
-
logger.configure_logger()
1599
+
logging.configure()
1600
+
}
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))
1489
1640
}
1490
1641
1491
1642
/// Log a message to the Erlang logger with the level of `emergency`.
1492
-
///
1643
+
///
1493
1644
/// See the [Erlang logger documentation][1] for more information.
1494
-
///
1645
+
///
1495
1646
/// [1]: https://www.erlang.org/doc/man/logger
1496
-
///
1647
+
///
1497
1648
pub fn log_emergency(message: String) -> Nil {
1498
-
logger.log(logger.Emergency, message)
1649
+
logging.log(logging.Emergency, message)
1499
1650
}
1500
1651
1501
1652
/// Log a message to the Erlang logger with the level of `alert`.
1502
-
///
1653
+
///
1503
1654
/// See the [Erlang logger documentation][1] for more information.
1504
-
///
1655
+
///
1505
1656
/// [1]: https://www.erlang.org/doc/man/logger
1506
-
///
1657
+
///
1507
1658
pub fn log_alert(message: String) -> Nil {
1508
-
logger.log(logger.Alert, message)
1659
+
logging.log(logging.Alert, message)
1509
1660
}
1510
1661
1511
1662
/// Log a message to the Erlang logger with the level of `critical`.
1512
-
///
1663
+
///
1513
1664
/// See the [Erlang logger documentation][1] for more information.
1514
-
///
1665
+
///
1515
1666
/// [1]: https://www.erlang.org/doc/man/logger
1516
-
///
1667
+
///
1517
1668
pub fn log_critical(message: String) -> Nil {
1518
-
logger.log(logger.Critical, message)
1669
+
logging.log(logging.Critical, message)
1519
1670
}
1520
1671
1521
1672
/// Log a message to the Erlang logger with the level of `error`.
1522
-
///
1673
+
///
1523
1674
/// See the [Erlang logger documentation][1] for more information.
1524
-
///
1675
+
///
1525
1676
/// [1]: https://www.erlang.org/doc/man/logger
1526
-
///
1677
+
///
1527
1678
pub fn log_error(message: String) -> Nil {
1528
-
logger.log(logger.Error, message)
1679
+
logging.log(logging.Error, message)
1529
1680
}
1530
1681
1531
1682
/// Log a message to the Erlang logger with the level of `warning`.
1532
-
///
1683
+
///
1533
1684
/// See the [Erlang logger documentation][1] for more information.
1534
-
///
1685
+
///
1535
1686
/// [1]: https://www.erlang.org/doc/man/logger
1536
-
///
1687
+
///
1537
1688
pub fn log_warning(message: String) -> Nil {
1538
-
logger.log(logger.Warning, message)
1689
+
logging.log(logging.Warning, message)
1539
1690
}
1540
1691
1541
1692
/// Log a message to the Erlang logger with the level of `notice`.
1542
-
///
1693
+
///
1543
1694
/// See the [Erlang logger documentation][1] for more information.
1544
-
///
1695
+
///
1545
1696
/// [1]: https://www.erlang.org/doc/man/logger
1546
-
///
1697
+
///
1547
1698
pub fn log_notice(message: String) -> Nil {
1548
-
logger.log(logger.Notice, message)
1699
+
logging.log(logging.Notice, message)
1549
1700
}
1550
1701
1551
1702
/// Log a message to the Erlang logger with the level of `info`.
1552
-
///
1703
+
///
1553
1704
/// See the [Erlang logger documentation][1] for more information.
1554
-
///
1705
+
///
1555
1706
/// [1]: https://www.erlang.org/doc/man/logger
1556
-
///
1707
+
///
1557
1708
pub fn log_info(message: String) -> Nil {
1558
-
logger.log(logger.Info, message)
1709
+
logging.log(logging.Info, message)
1559
1710
}
1560
1711
1561
1712
/// Log a message to the Erlang logger with the level of `debug`.
1562
-
///
1713
+
///
1563
1714
/// See the [Erlang logger documentation][1] for more information.
1564
-
///
1715
+
///
1565
1716
/// [1]: https://www.erlang.org/doc/man/logger
1566
-
///
1717
+
///
1567
1718
pub fn log_debug(message: String) -> Nil {
1568
-
logger.log(logger.Debug, message)
1719
+
logging.log(logging.Debug, message)
1569
1720
}
1570
1721
1571
1722
//
···
1575
1726
/// Generate a random string of the given length.
1576
1727
///
1577
1728
pub fn random_string(length: Int) -> String {
1578
-
crypto.strong_random_bytes(length)
1579
-
|> bit_array.base64_url_encode(False)
1580
-
|> string.slice(0, length)
1729
+
internal.random_string(length)
1581
1730
}
1582
1731
1583
1732
/// Sign a message which can later be verified using the `verify_signed_message`
1584
1733
/// function to detect if the message has been tampered with.
1585
-
///
1734
+
///
1586
1735
/// Signed messages are not encrypted and can be read by anyone. They are not
1587
1736
/// suitable for storing sensitive information.
1588
-
///
1737
+
///
1589
1738
/// This function uses the secret key base from the request. If the secret
1590
1739
/// changes then the signature will no longer be verifiable.
1591
-
///
1740
+
///
1592
1741
pub fn sign_message(
1593
1742
request: Request,
1594
1743
message: BitArray,
···
1598
1747
}
1599
1748
1600
1749
/// Verify a signed message which was signed using the `sign_message` function.
1601
-
///
1750
+
///
1602
1751
/// Returns the content of the message if the signature is valid, otherwise
1603
1752
/// returns an error.
1604
-
///
1753
+
///
1605
1754
/// This function uses the secret key base from the request. If the secret
1606
1755
/// changes then the signature will no longer be verifiable.
1607
-
///
1756
+
///
1608
1757
pub fn verify_signed_message(
1609
1758
request: Request,
1610
1759
message: String,
···
1612
1761
crypto.verify_signed_message(message, <<request.body.secret_key_base:utf8>>)
1613
1762
}
1614
1763
1615
-
fn random_slug() -> String {
1616
-
random_string(16)
1617
-
}
1618
-
1619
1764
//
1620
1765
// Cookies
1621
1766
//
···
1640
1785
///
1641
1786
/// ```gleam
1642
1787
/// wisp.ok()
1643
-
/// |> wisp.set_cookie("id", "123", wisp.PlainText, 60 * 60)
1788
+
/// |> wisp.set_cookie(request, "id", "123", wisp.PlainText, 60 * 60)
1644
1789
/// ```
1645
-
///
1790
+
///
1646
1791
/// Setting a signed cookie that the client can read but not modify:
1647
-
///
1792
+
///
1648
1793
/// ```gleam
1649
1794
/// wisp.ok()
1650
-
/// |> wisp.set_cookie("id", value, wisp.Signed, 60 * 60)
1795
+
/// |> wisp.set_cookie(request, "id", value, wisp.Signed, 60 * 60)
1651
1796
/// ```
1652
1797
///
1653
1798
pub fn set_cookie(
···
1687
1832
/// for a signed cookie, then `Error(Nil)` is returned.
1688
1833
///
1689
1834
/// ```gleam
1690
-
/// wisp.get_cookie(request, "group")
1835
+
/// wisp.get_cookie(request, "group", wisp.PlainText)
1691
1836
/// // -> Ok("A")
1692
1837
/// ```
1693
1838
///
···
1714
1859
1715
1860
// TODO: chunk the body
1716
1861
/// Create a connection which will return the given body when read.
1717
-
///
1862
+
///
1718
1863
/// This function is intended for use in tests, though you probably want the
1719
1864
/// `wisp/testing` module instead.
1720
-
///
1865
+
///
1721
1866
pub fn create_canned_connection(
1722
1867
body: BitArray,
1723
1868
secret_key_base: String,
1724
-
) -> Connection {
1725
-
make_connection(
1726
-
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
+
},
1727
1874
secret_key_base,
1728
1875
)
1729
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
}
+56
-24
test/wisp_test.gleam
+56
-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
···
18
19
import wisp/testing
19
20
20
21
pub fn main() {
22
+
wisp.configure_logger()
21
23
gleeunit.main()
22
24
}
23
25
···
117
119
}
118
120
119
121
pub fn json_response_test() {
120
-
let body = string_builder.from_string("{\"one\":1,\"two\":2}")
122
+
let body = string_tree.from_string("{\"one\":1,\"two\":2}")
121
123
let response = wisp.json_response(body, 201)
122
124
response.status
123
125
|> should.equal(201)
124
126
response.headers
125
-
|> should.equal([#("content-type", "application/json")])
127
+
|> should.equal([#("content-type", "application/json; charset=utf-8")])
126
128
response
127
129
|> testing.string_body
128
130
|> should.equal("{\"one\":1,\"two\":2}")
129
131
}
130
132
131
133
pub fn html_response_test() {
132
-
let body = string_builder.from_string("Hello, world!")
134
+
let body = string_tree.from_string("Hello, world!")
133
135
let response = wisp.html_response(body, 200)
134
136
response.status
135
137
|> should.equal(200)
136
138
response.headers
137
-
|> should.equal([#("content-type", "text/html")])
139
+
|> should.equal([#("content-type", "text/html; charset=utf-8")])
138
140
response
139
141
|> testing.string_body
140
142
|> should.equal("Hello, world!")
141
143
}
142
144
143
145
pub fn html_body_test() {
144
-
let body = string_builder.from_string("Hello, world!")
146
+
let body = string_tree.from_string("Hello, world!")
145
147
let response =
146
148
wisp.method_not_allowed([http.Get])
147
149
|> wisp.html_body(body)
148
150
response.status
149
151
|> should.equal(405)
150
152
response.headers
151
-
|> should.equal([#("allow", "GET"), #("content-type", "text/html")])
153
+
|> should.equal([
154
+
#("allow", "GET"),
155
+
#("content-type", "text/html; charset=utf-8"),
156
+
])
152
157
response
153
158
|> testing.string_body
154
159
|> should.equal("Hello, world!")
···
329
334
}
330
335
331
336
pub fn rescue_crashes_error_test() {
332
-
// 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
+
333
340
{
334
341
use <- wisp.rescue_crashes
335
342
panic as "we need to crash to test the middleware"
···
358
365
response.status
359
366
|> should.equal(200)
360
367
response.headers
361
-
|> should.equal([#("content-type", "text/plain")])
368
+
|> should.equal([#("content-type", "text/plain; charset=utf-8")])
362
369
response.body
363
370
|> should.equal(wisp.File("./test/fixture.txt"))
364
371
···
369
376
response.status
370
377
|> should.equal(200)
371
378
response.headers
372
-
|> should.equal([#("content-type", "application/json")])
379
+
|> should.equal([#("content-type", "application/json; charset=utf-8")])
373
380
response.body
374
381
|> should.equal(wisp.File("./test/fixture.json"))
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"))
375
393
376
394
// Get something not handled by the static file server
377
395
let response =
···
396
414
response.status
397
415
|> should.equal(200)
398
416
response.headers
399
-
|> should.equal([#("content-type", "text/plain")])
417
+
|> should.equal([#("content-type", "text/plain; charset=utf-8")])
400
418
response.body
401
419
|> should.equal(wisp.File("./test/fixture.txt"))
402
420
}
···
412
430
response.status
413
431
|> should.equal(200)
414
432
response.headers
415
-
|> should.equal([#("content-type", "text/plain")])
433
+
|> should.equal([#("content-type", "text/plain; charset=utf-8")])
416
434
response.body
417
435
|> should.equal(wisp.File("./test/fixture.txt"))
418
436
}
···
484
502
pub fn require_content_type_test() {
485
503
{
486
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")])
487
515
use <- wisp.require_content_type(request, "text/plain")
488
516
wisp.ok()
489
517
}
···
723
751
list.key_find(request.headers, "x-original-method")
724
752
|> should.equal(header)
725
753
726
-
string_builder.from_string("Hello!")
754
+
string_tree.from_string("Hello!")
727
755
|> wisp.html_response(201)
728
756
}
729
757
···
732
760
|> handler(Error(Nil))
733
761
|> should.equal(Response(
734
762
201,
735
-
[#("content-type", "text/html")],
736
-
wisp.Text(string_builder.from_string("Hello!")),
763
+
[#("content-type", "text/html; charset=utf-8")],
764
+
wisp.Text(string_tree.from_string("Hello!")),
737
765
))
738
766
739
767
testing.get("/", [])
740
768
|> request.set_method(http.Head)
741
769
|> handler(Ok("HEAD"))
742
-
|> 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
+
))
743
775
744
776
testing.get("/", [])
745
777
|> request.set_method(http.Post)
···
865
897
|> should.equal(Response(
866
898
200,
867
899
[],
868
-
wisp.Text(string_builder.from_string("Hello, world!")),
900
+
wisp.Text(string_tree.from_string("Hello, world!")),
869
901
))
870
902
}
871
903
872
-
pub fn string_builder_body_test() {
904
+
pub fn string_tree_body_test() {
873
905
wisp.ok()
874
-
|> wisp.string_builder_body(string_builder.from_string("Hello, world!"))
906
+
|> wisp.string_tree_body(string_tree.from_string("Hello, world!"))
875
907
|> should.equal(Response(
876
908
200,
877
909
[],
878
-
wisp.Text(string_builder.from_string("Hello, world!")),
910
+
wisp.Text(string_tree.from_string("Hello, world!")),
879
911
))
880
912
}
881
913
882
914
pub fn json_body_test() {
883
915
wisp.ok()
884
-
|> wisp.json_body(string_builder.from_string("{\"one\":1,\"two\":2}"))
916
+
|> wisp.json_body(string_tree.from_string("{\"one\":1,\"two\":2}"))
885
917
|> should.equal(Response(
886
918
200,
887
-
[#("content-type", "application/json")],
888
-
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}")),
889
921
))
890
922
}
891
923