🧚 A practical web framework for Gleam

Compare changes

Choose any two refs to compare.

Changed files
+3401 -2628
.github
workflows
docs
examples
0-hello-world
00-hello-world
01-routing
02-working-with-form-data
03-working-with-json
04-working-with-other-formats
05-using-a-database
06-serving-static-assets
07-logging
08-working-with-cookies
09-configuring-default-responses
1-routing
10-working-with-files
2-working-with-form-data
3-working-with-json
4-working-with-other-formats
5-using-a-database
6-serving-static-assets
7-logging
8-working-with-cookies
9-configuring-default-responses
utilities
src
test
+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
··· 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
··· 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

This is a binary file and will not be displayed.

+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
··· 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
··· 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 - 12 - [dev-dependencies] 13 - gleeunit = "~> 1.0"
-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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 1 + function update() { 2 + document.body.innerText = new Date().toLocaleTimeString(); 3 + } 4 + 5 + setInterval(update, 1000); 6 + update();
+9
examples/06-serving-static-assets/priv/static/styles.css
··· 1 + html { 2 + display: flex; 3 + justify-content: center; 4 + align-items: center; 5 + border: 50px solid #ffaff3; 6 + height: 100vh; 7 + box-sizing: border-box; 8 + font-size: 40px; 9 + }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 + }
+32
examples/08-working-with-cookies/README.md
··· 1 + # Wisp Example: Working with cookies 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 write cookies, and how to sign cookies so 9 + they cannot be tampered with. 10 + 11 + This example is based off of the ["working with form data" example][form-data] so read that one 12 + first. The additions are detailed here and commented in the code. 13 + 14 + Signing of cookies uses the `secret_key_base` value. If this value changes then 15 + the application will not be able to verify previously signed cookies, and if 16 + someone gains access to the secret key they will be able to forge cookies. This 17 + example application generates a random string in `app.gleam`, but in a real 18 + application you will need to read this secret value from somewhere secure. 19 + 20 + [form-data]: https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data 21 + 22 + ### `app/router` module 23 + 24 + The `handle_request` function has been updated to read and write cookies. 25 + 26 + ### `app_test` module 27 + 28 + Tests have been added to test that cookies are handled correctly, and to create signed cookies for test requests. 29 + 30 + ### Other files 31 + 32 + No changes have been made to the other files.
+15
examples/08-working-with-cookies/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_crypto = "~> 1.0" 10 + gleam_erlang = "~> 0.23" 11 + mist = ">= 2.0.0 and < 3.0.0" 12 + gleam_http = "~> 3.5" 13 + 14 + [dev-dependencies] 15 + gleeunit = "~> 1.0"
+79
examples/08-working-with-cookies/src/app/router.gleam
··· 1 + import app/web 2 + import gleam/http.{Delete, Get, Post} 3 + import gleam/list 4 + import gleam/string_tree 5 + import wisp.{type Request, type Response} 6 + 7 + const cookie_name = "id" 8 + 9 + pub fn handle_request(req: Request) -> Response { 10 + use req <- web.middleware(req) 11 + 12 + case wisp.path_segments(req) { 13 + [] -> home(req) 14 + ["session"] -> session(req) 15 + _ -> wisp.not_found() 16 + } 17 + } 18 + 19 + pub fn home(req: Request) -> Response { 20 + case wisp.get_cookie(req, cookie_name, wisp.Signed) { 21 + Ok(name) -> { 22 + [ 23 + "<h1>Hello, " <> wisp.escape_html(name) <> "!</h1>", 24 + "<form action='/session?_method=DELETE' method='post'>", 25 + " <button type='submit'>Log out</button>", 26 + "</form>", 27 + ] 28 + |> string_tree.from_strings 29 + |> wisp.html_response(200) 30 + } 31 + Error(_) -> { 32 + wisp.redirect("/session") 33 + } 34 + } 35 + } 36 + 37 + pub fn session(req: Request) -> Response { 38 + case req.method { 39 + Get -> new_session() 40 + Post -> create_session(req) 41 + Delete -> destroy_session(req) 42 + _ -> wisp.method_not_allowed([Get, Post, Delete]) 43 + } 44 + } 45 + 46 + pub fn new_session() -> Response { 47 + " 48 + <form action='/session' method='post'> 49 + <label> 50 + Name: <input type='text' name='name'> 51 + </label> 52 + <button type='submit'>Log in</button> 53 + </form> 54 + " 55 + |> string_tree.from_string 56 + |> wisp.html_response(200) 57 + } 58 + 59 + pub fn destroy_session(req: Request) -> Response { 60 + let resp = wisp.redirect("/session") 61 + case wisp.get_cookie(req, cookie_name, wisp.Signed) { 62 + Ok(value) -> wisp.set_cookie(resp, req, cookie_name, value, wisp.Signed, 0) 63 + Error(_) -> resp 64 + } 65 + } 66 + 67 + pub fn create_session(req: Request) -> Response { 68 + use formdata <- wisp.require_form(req) 69 + 70 + case list.key_find(formdata.values, "name") { 71 + Ok(name) -> { 72 + wisp.redirect("/") 73 + |> wisp.set_cookie(req, cookie_name, name, wisp.Signed, 60 * 60 * 24) 74 + } 75 + Error(_) -> { 76 + wisp.redirect("/session") 77 + } 78 + } 79 + }
+12
examples/08-working-with-cookies/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/08-working-with-cookies/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 + }
+64
examples/08-working-with-cookies/test/app_test.gleam
··· 1 + import app/router 2 + import gleam/crypto 3 + import gleam/list 4 + import gleam/string 5 + import gleeunit 6 + import gleeunit/should 7 + import wisp 8 + import wisp/testing 9 + 10 + pub fn main() { 11 + gleeunit.main() 12 + } 13 + 14 + pub fn home_not_logged_in_test() { 15 + let response = router.handle_request(testing.get("/", [])) 16 + 17 + response.status 18 + |> should.equal(303) 19 + 20 + response.headers 21 + |> should.equal([#("location", "/session")]) 22 + } 23 + 24 + pub fn home_logged_in_test() { 25 + let response = 26 + testing.get("/", []) 27 + |> testing.set_cookie("id", "Tim", wisp.Signed) 28 + |> router.handle_request 29 + 30 + response.status 31 + |> should.equal(200) 32 + 33 + response 34 + |> testing.string_body 35 + |> string.contains("Hello, Tim!") 36 + |> should.equal(True) 37 + } 38 + 39 + pub fn new_session_test() { 40 + let response = router.handle_request(testing.get("/session", [])) 41 + 42 + response.status 43 + |> should.equal(200) 44 + 45 + response 46 + |> testing.string_body 47 + |> string.contains("Log in") 48 + |> should.equal(True) 49 + } 50 + 51 + pub fn create_session_test() { 52 + let request = testing.post_form("/session", [], [#("name", "Tim")]) 53 + let response = router.handle_request(request) 54 + 55 + response.status 56 + |> should.equal(303) 57 + 58 + let assert Ok(cookie) = list.key_find(response.headers, "set-cookie") 59 + 60 + let signed = wisp.sign_message(request, <<"Tim":utf8>>, crypto.Sha512) 61 + cookie 62 + |> string.starts_with("id=" <> signed) 63 + |> should.equal(True) 64 + }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 - 12 - [dev-dependencies] 13 - gleeunit = "~> 1.0"
-6
examples/6-serving-static-assets/priv/static/main.js
··· 1 - function update() { 2 - document.body.innerText = new Date().toLocaleTimeString(); 3 - } 4 - 5 - setInterval(update, 1000); 6 - update();
-9
examples/6-serving-static-assets/priv/static/styles.css
··· 1 - html { 2 - display: flex; 3 - justify-content: center; 4 - align-items: center; 5 - border: 50px solid #ffaff3; 6 - height: 100vh; 7 - box-sizing: border-box; 8 - font-size: 40px; 9 - }
-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
··· 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
··· 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
··· 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
··· 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
··· 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 - 12 - [dev-dependencies] 13 - gleeunit = "~> 1.0"
-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
··· 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
··· 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
··· 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 - }
-32
examples/8-working-with-cookies/README.md
··· 1 - # Wisp Example: Working with cookies 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 write cookies, and how to sign cookies so 9 - they cannot be tampered with. 10 - 11 - This example is based off of the [working with form data example][form-data] so read that one 12 - first. The additions are detailed here and commented in the code. 13 - 14 - Signing of cookies uses the `secret_key_base` value. If this value changes then 15 - the application will not be able to verify previously signed cookies, and if 16 - someone gains access to the secret key they will be able to forge cookies. This 17 - example application generates a random string in `app.gleam`, but in a real 18 - application you will need to read this secret value from somewhere secure. 19 - 20 - [routing]: https://github.com/lpil/wisp/tree/main/examples/2-working-with-form-data 21 - 22 - ### `app/router` module 23 - 24 - The `handle_request` function has been updated to read and write cookies. 25 - 26 - ### `app_test` module 27 - 28 - Tests have been added to test that cookies are handled correctly, and to create signed cookies for test requests. 29 - 30 - ### Other files 31 - 32 - No changes have been made to the other files.
-14
examples/8-working-with-cookies/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_crypto = "~> 1.0" 10 - gleam_erlang = "~> 0.23" 11 - mist = "~> 0.14" 12 - 13 - [dev-dependencies] 14 - gleeunit = "~> 1.0"
-79
examples/8-working-with-cookies/src/app/router.gleam
··· 1 - import app/web 2 - import gleam/http.{Delete, Get, Post} 3 - import gleam/list 4 - import gleam/string_builder 5 - import wisp.{type Request, type Response} 6 - 7 - const cookie_name = "id" 8 - 9 - pub fn handle_request(req: Request) -> Response { 10 - use req <- web.middleware(req) 11 - 12 - case wisp.path_segments(req) { 13 - [] -> home(req) 14 - ["session"] -> session(req) 15 - _ -> wisp.not_found() 16 - } 17 - } 18 - 19 - pub fn home(req: Request) -> Response { 20 - case wisp.get_cookie(req, cookie_name, wisp.Signed) { 21 - Ok(name) -> { 22 - [ 23 - "<h1>Hello, " <> wisp.escape_html(name) <> "!</h1>", 24 - "<form action='/session?_method=DELETE' method='post'>", 25 - " <button type='submit'>Log out</button>", 26 - "</form>", 27 - ] 28 - |> string_builder.from_strings 29 - |> wisp.html_response(200) 30 - } 31 - Error(_) -> { 32 - wisp.redirect("/session") 33 - } 34 - } 35 - } 36 - 37 - pub fn session(req: Request) -> Response { 38 - case req.method { 39 - Get -> new_session() 40 - Post -> create_session(req) 41 - Delete -> destroy_session(req) 42 - _ -> wisp.method_not_allowed([Get, Post, Delete]) 43 - } 44 - } 45 - 46 - pub fn new_session() -> Response { 47 - " 48 - <form action='/session' method='post'> 49 - <label> 50 - Name: <input type='text' name='name'> 51 - </label> 52 - <button type='submit'>Log in</button> 53 - </form> 54 - " 55 - |> string_builder.from_string 56 - |> wisp.html_response(200) 57 - } 58 - 59 - pub fn destroy_session(req: Request) -> Response { 60 - let resp = wisp.redirect("/session") 61 - case wisp.get_cookie(req, cookie_name, wisp.Signed) { 62 - Ok(value) -> wisp.set_cookie(resp, req, cookie_name, value, wisp.Signed, 0) 63 - Error(_) -> resp 64 - } 65 - } 66 - 67 - pub fn create_session(req: Request) -> Response { 68 - use formdata <- wisp.require_form(req) 69 - 70 - case list.key_find(formdata.values, "name") { 71 - Ok(name) -> { 72 - wisp.redirect("/") 73 - |> wisp.set_cookie(req, cookie_name, name, wisp.Signed, 60 * 60 * 24) 74 - } 75 - Error(_) -> { 76 - wisp.redirect("/session") 77 - } 78 - } 79 - }
-12
examples/8-working-with-cookies/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/8-working-with-cookies/src/app.gleam
··· 1 - import gleam/erlang/process 2 - import mist 3 - import wisp 4 - import app/router 5 - 6 - pub fn main() { 7 - wisp.configure_logger() 8 - let secret_key_base = wisp.random_string(64) 9 - 10 - let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 - |> mist.new 13 - |> mist.port(8000) 14 - |> mist.start_http 15 - 16 - process.sleep_forever() 17 - }
-64
examples/8-working-with-cookies/test/app_test.gleam
··· 1 - import app/router 2 - import gleam/crypto 3 - import gleam/list 4 - import gleam/string 5 - import gleeunit 6 - import gleeunit/should 7 - import wisp 8 - import wisp/testing 9 - 10 - pub fn main() { 11 - gleeunit.main() 12 - } 13 - 14 - pub fn home_not_logged_in_test() { 15 - let response = router.handle_request(testing.get("/", [])) 16 - 17 - response.status 18 - |> should.equal(303) 19 - 20 - response.headers 21 - |> should.equal([#("location", "/session")]) 22 - } 23 - 24 - pub fn home_logged_in_test() { 25 - let response = 26 - testing.get("/", []) 27 - |> testing.set_cookie("id", "Tim", wisp.Signed) 28 - |> router.handle_request 29 - 30 - response.status 31 - |> should.equal(200) 32 - 33 - response 34 - |> testing.string_body 35 - |> string.contains("Hello, Tim!") 36 - |> should.equal(True) 37 - } 38 - 39 - pub fn new_session_test() { 40 - let response = router.handle_request(testing.get("/session", [])) 41 - 42 - response.status 43 - |> should.equal(200) 44 - 45 - response 46 - |> testing.string_body 47 - |> string.contains("Log in") 48 - |> should.equal(True) 49 - } 50 - 51 - pub fn create_session_test() { 52 - let request = testing.post_form("/session", [], [#("name", "Tim")]) 53 - let response = router.handle_request(request) 54 - 55 - response.status 56 - |> should.equal(303) 57 - 58 - let assert Ok(cookie) = list.key_find(response.headers, "set-cookie") 59 - 60 - let signed = wisp.sign_message(request, <<"Tim":utf8>>, crypto.Sha512) 61 - cookie 62 - |> string.starts_with("id=" <> signed) 63 - |> should.equal(True) 64 - }
-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
··· 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 = "~> 0.14" 10 - gleam_erlang = "~> 0.23" 11 - 12 - [dev-dependencies] 13 - gleeunit = "~> 1.0"
-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
··· 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
··· 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
··· 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
··· 6 6 7 7 [dependencies] 8 8 gleam_stdlib = "~> 0.30" 9 - simplifile = "~> 1.0" 10 - ids = "~> 0.8" 9 + simplifile = "~> 2.0" 11 10 gleam_json = "~> 0.6" 11 + youid = ">= 1.1.0 and < 2.0.0" 12 12 13 13 [dev-dependencies] 14 14 gleeunit = "~> 1.0"
+9 -8
examples/utilities/tiny_database/manifest.toml
··· 2 2 # You typically do not need to edit this file 3 3 4 4 packages = [ 5 - { name = "gleam_erlang", version = "0.23.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "C21CFB816C114784E669FFF4BBF433535EEA9960FA2F216209B8691E87156B96" }, 5 + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, 6 + { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, 7 + { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, 6 8 { name = "gleam_json", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB405BD93A8828BCD870463DE29375E7B2D252D9D124C109E5B618AAC00B86FC" }, 7 - { name = "gleam_otp", version = "0.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_erlang"], otp_app = "gleam_otp", source = "hex", outer_checksum = "18EF8242A5E54BA92F717C7222F03B3228AEE00D1F286D4C56C3E8C18AA2588E" }, 8 - { name = "gleam_stdlib", version = "0.32.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "ABF00CDCCB66FABBCE351A50060964C4ACE798F95A0D78622C8A7DC838792577" }, 9 - { name = "gleeunit", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D3682ED8C5F9CAE1C928F2506DE91625588CC752495988CBE0F5653A42A6F334" }, 10 - { name = "ids", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib", "gleam_otp"], otp_app = "ids", source = "hex", outer_checksum = "912A21722E07E68117B92863D05B15BE97E5AEF4ECF47C2F567CECCD5A4F5739" }, 11 - { name = "simplifile", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0BD6F0E7DA1A7E11D18B8AD48453225CAFCA4C8CFB4513D217B372D2866C501C" }, 9 + { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, 10 + { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, 11 + { name = "simplifile", version = "2.0.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "95219227A43FCFE62C6E494F413A1D56FF953B68FE420698612E3D89A1EFE029" }, 12 12 { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, 13 + { name = "youid", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_stdlib"], otp_app = "youid", source = "hex", outer_checksum = "15BF3E8173C8741930E23D22071CD55AE203B6E43B9E0C6C9E7D9F116808E418" }, 13 14 ] 14 15 15 16 [requirements] 16 17 gleam_json = { version = "~> 0.6" } 17 18 gleam_stdlib = { version = "~> 0.30" } 18 19 gleeunit = { version = "~> 1.0" } 19 - ids = { version = "~> 0.8" } 20 - simplifile = { version = "~> 1.0" } 20 + simplifile = { version = "~> 2.0" } 21 + youid = { version = ">= 1.1.0 and < 2.0.0" }
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 /// // -> "&lt;h1&gt;Hello, Joe!&lt;/h1&gt;" 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 = [<<"&lt;":utf8>>, ..acc] 347 + do_escape_html(rest, skip + 1, original, acc) 348 + } 349 + 350 + <<">":utf8, rest:bits>> -> { 351 + let acc = [<<"&gt;":utf8>>, ..acc] 352 + do_escape_html(rest, skip + 1, original, acc) 353 + } 354 + 355 + <<"&":utf8, rest:bits>> -> { 356 + let acc = [<<"&amp;":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 <> "&lt;", xs) 311 - Ok(#(">", xs)) -> do_escape_html(escaped <> "&gt;", xs) 312 - Ok(#("&", xs)) -> do_escape_html(escaped <> "&amp;", 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 = [<<"&lt;":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 = [<<"&gt;":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 = [<<"&amp;":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 }
+4
src/wisp_ffi.erl
··· 1 + -module(wisp_ffi). 2 + -export([coerce/1]). 3 + 4 + coerce(X) -> X.
test/fixture.dat

This is a binary file and will not be displayed.

+3 -3
test/wisp/testing_test.gleam
··· 2 2 import gleam/http/response 3 3 import gleam/json 4 4 import gleam/option.{None, Some} 5 - import gleam/string_builder 5 + import gleam/string_tree 6 6 import gleeunit/should 7 7 import wisp 8 8 import wisp/testing ··· 502 502 503 503 pub fn string_body_text_test() { 504 504 wisp.ok() 505 - |> response.set_body(wisp.Text(string_builder.from_string("Hello, Joe!"))) 505 + |> response.set_body(wisp.Text(string_tree.from_string("Hello, Joe!"))) 506 506 |> testing.string_body 507 507 |> should.equal("Hello, Joe!") 508 508 } ··· 523 523 524 524 pub fn bit_array_body_text_test() { 525 525 wisp.ok() 526 - |> response.set_body(wisp.Text(string_builder.from_string("Hello, Joe!"))) 526 + |> response.set_body(wisp.Text(string_tree.from_string("Hello, Joe!"))) 527 527 |> testing.bit_array_body 528 528 |> should.equal(<<"Hello, Joe!":utf8>>) 529 529 }
+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