🧚 A practical web framework for Gleam

Compare changes

Choose any two refs to compare.

Changed files
+961 -548
.github
workflows
docs
examples
00-hello-world
01-routing
02-working-with-form-data
03-working-with-json
04-working-with-other-formats
05-using-a-database
06-serving-static-assets
07-logging
08-working-with-cookies
09-configuring-default-responses
10-working-with-files
utilities
src
test
+28 -20
.github/workflows/ci.yml
··· 13 13 - uses: actions/checkout@v4 14 14 - uses: erlef/setup-beam@v1 15 15 with: 16 - otp-version: "26.0" 17 - gleam-version: "0.34.0-rc2" 16 + otp-version: "27.0" 17 + gleam-version: "1.4.0" 18 18 rebar3-version: "3" 19 19 # elixir-version: "1.14.2" 20 20 - run: gleam format --check src test 21 21 - run: gleam deps download 22 22 - run: gleam test 23 23 24 - - name: "Example: 0-hello-world" 24 + - name: "Example: 00-hello-world" 25 25 run: gleam test 26 - working-directory: examples/0-hello-world 26 + working-directory: examples/00-hello-world 27 27 28 - - name: "Example: 1-routing" 28 + - name: "Example: 01-routing" 29 29 run: gleam test 30 - working-directory: examples/1-routing 30 + working-directory: examples/01-routing 31 31 32 - - name: "Example: 2-working-with-form-data" 32 + - name: "Example: 02-working-with-form-data" 33 33 run: gleam test 34 - working-directory: examples/2-working-with-form-data 34 + working-directory: examples/02-working-with-form-data 35 35 36 - - name: "Example: 3-working-with-json" 36 + - name: "Example: 03-working-with-json" 37 37 run: gleam test 38 - working-directory: examples/3-working-with-json 38 + working-directory: examples/03-working-with-json 39 39 40 - - name: "Example: 4-working-with-other-formats" 40 + - name: "Example: 04-working-with-other-formats" 41 41 run: gleam test 42 - working-directory: examples/4-working-with-other-formats 42 + working-directory: examples/04-working-with-other-formats 43 43 44 - - name: "Example: 5-using-a-database" 44 + - name: "Example: 05-using-a-database" 45 45 run: gleam test 46 - working-directory: examples/5-using-a-database 46 + working-directory: examples/05-using-a-database 47 47 48 - - name: "Example: 6-serving-static-assets" 48 + - name: "Example: 06-serving-static-assets" 49 49 run: gleam test 50 - working-directory: examples/6-serving-static-assets 50 + working-directory: examples/06-serving-static-assets 51 51 52 - - name: "Example: 7-logging" 52 + - name: "Example: 07-logging" 53 53 run: gleam test 54 - working-directory: examples/7-logging 54 + working-directory: examples/07-logging 55 55 56 - - name: "Example: 8-working-with-cookies" 56 + - name: "Example: 08-working-with-cookies" 57 57 run: gleam test 58 - working-directory: examples/8-working-with-cookies 58 + working-directory: examples/08-working-with-cookies 59 + 60 + - name: "Example: 09-configuring-default-responses" 61 + run: gleam test 62 + working-directory: examples/09-configuring-default-responses 63 + 64 + - name: "Example: 10-working-with-files" 65 + run: gleam test 66 + working-directory: examples/10-working-with-files
+40 -1
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 - ## Unreleased 3 + ## v1.3.0 - 2024-11-21 4 + 5 + - Updated for `gleam_stdlib` v0.43.0. 6 + 7 + ## v1.2.0 - 2024-10-09 8 + 9 + - The requirement for `gleam_json` has been relaxed to < 3.0.0. 10 + - The requirement for `mist` has been relaxed to < 4.0.0. 11 + - The Gleam version requirement has been corrected to `>= 1.1.0` from the 12 + previously inaccurate `">= 0.32.0`. 13 + 14 + ## v1.1.0 - 2024-08-23 15 + 16 + - Rather than using `/tmp`, the platform-specific temporary directory is 17 + detected used. 18 + 19 + ## v1.0.0 - 2024-08-21 20 + 21 + - The Mist web server related functions have been moved to the `wisp_mist` 22 + module. 23 + - The `wisp` module gains the `set_logger_level` function and `LogLevel` type. 24 + 25 + ## v0.16.0 - 2024-07-13 26 + 27 + - HTML and JSON body functions now include `charset=utf-8` in the content-type 28 + header. 29 + - The `require_content_type` function now handles additional attributes 30 + correctly. 31 + 32 + ## v0.15.0 - 2024-05-12 33 + 34 + - The `mist` version constraint has been increased to >= 1.2.0. 35 + - The `simplifile` version constraint has been increased to >= 2.0.0. 36 + - The `escape_html` function in the `wisp` module has been optimised. 37 + 38 + ## v0.14.0 - 2024-03-28 39 + 40 + - The `mist` version constraint has been relaxed to permit 0.x or 1.x versions. 41 + 42 + ## v0.13.0 - 2024-03-23 4 43 5 44 - The `wisp` module gains the `file_download_from_memory` and `file_download` 6 45 functions.
+5 -5
README.md
··· 17 17 connection or user session. 18 18 19 19 ```gleam 20 - import wisp.{Request, Response} 20 + import wisp.{type Request, type Response} 21 21 22 22 pub type Context { 23 23 Context(secret: String) ··· 41 41 such as images and CSS. 42 42 43 43 ```gleam 44 - import wisp.{Request, Response} 44 + import wisp.{type Request, type Response} 45 45 46 46 pub fn handle_request(request: Request) -> Response { 47 - use <- wisp.log_request 47 + use <- wisp.log_request(request) 48 48 use <- wisp.serve_static(request, under: "/static", from: "/public") 49 49 wisp.ok() 50 50 } ··· 55 55 The Wisp examples are a good place to start. They cover various scenarios and 56 56 include comments and tests. 57 57 58 - - [Hello, World!](https://github.com/lpil/wisp/tree/main/examples/0-hello-world) 58 + - [Hello, World!](https://github.com/lpil/wisp/tree/main/examples/00-hello-world) 59 59 - [Routing](https://github.com/lpil/wisp/tree/main/examples/01-routing) 60 60 - [Working with form data](https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data) 61 61 - [Working with JSON](https://github.com/lpil/wisp/tree/main/examples/03-working-with-json) ··· 65 65 - [Logging](https://github.com/lpil/wisp/tree/main/examples/07-logging) 66 66 - [Working with cookies](https://github.com/lpil/wisp/tree/main/examples/08-working-with-cookies) 67 67 - [Configuring default responses](https://github.com/lpil/wisp/tree/main/examples/09-configuring-default-responses) 68 - - [Working with files](https://github.com/lpil/wisp/tree/main/examples/09-working-with-files) 68 + - [Working with files](https://github.com/lpil/wisp/tree/main/examples/10-working-with-files) 69 69 70 70 API documentation is available on [HexDocs](https://hexdocs.pm/wisp/). 71 71
docs/images/cover.png

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>
+2 -1
examples/00-hello-world/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = "~> 0.14" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 + 11 12 12 13 [dev-dependencies] 13 14 gleeunit = "~> 1.0"
+3 -3
examples/00-hello-world/src/app/router.gleam
··· 1 - import wisp.{type Request, type Response} 2 - import gleam/string_builder 3 1 import app/web 2 + import gleam/string_tree 3 + import wisp.{type Request, type Response} 4 4 5 5 /// The HTTP request handler- your application! 6 6 /// ··· 9 9 use _req <- web.middleware(req) 10 10 11 11 // Later we'll use templates, but for now a string will do. 12 - let body = string_builder.from_string("<h1>Hello, Joe!</h1>") 12 + let body = string_tree.from_string("<h1>Hello, Joe!</h1>") 13 13 14 14 // Return a 200 OK response with the body and a HTML content type. 15 15 wisp.html_response(body, 200)
+3 -2
examples/00-hello-world/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 // This sets the logger to print INFO level logs, and other sensible defaults ··· 14 15 15 16 // Start the Mist web server. 16 17 let assert Ok(_) = 17 - wisp.mist_handler(router.handle_request, secret_key_base) 18 + wisp_mist.handler(router.handle_request, secret_key_base) 18 19 |> mist.new 19 20 |> mist.port(8000) 20 21 |> mist.start_http
+1 -1
examples/00-hello-world/test/app_test.gleam
··· 14 14 |> should.equal(200) 15 15 16 16 response.headers 17 - |> should.equal([#("content-type", "text/html")]) 17 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 18 18 19 19 response 20 20 |> testing.string_body
+1 -1
examples/01-routing/README.md
··· 11 11 This example is based off of the ["Hello, World!" example][hello], so read that 12 12 one first. The additions are detailed here and commented in the code. 13 13 14 - [hello]: https://github.com/lpil/wisp/tree/main/examples/0-hello-world 14 + [hello]: https://github.com/lpil/wisp/tree/main/examples/00-hello-world 15 15 16 16 ### `app/router` module 17 17
+1 -1
examples/01-routing/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = "~> 0.14" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 11 gleam_http = "~> 3.5" 12 12 13 13 [dev-dependencies]
+8 -8
examples/01-routing/src/app/router.gleam
··· 1 - import wisp.{type Request, type Response} 2 - import gleam/string_builder 3 - import gleam/http.{Get, Post} 4 1 import app/web 2 + import gleam/http.{Get, Post} 3 + import gleam/string_tree 4 + import wisp.{type Request, type Response} 5 5 6 6 pub fn handle_request(req: Request) -> Response { 7 - use _req <- web.middleware(req) 7 + use req <- web.middleware(req) 8 8 9 9 // Wisp doesn't have a special router abstraction, instead we recommend using 10 10 // regular old pattern matching. This is faster than a router, is type safe, ··· 31 31 // used to return a 405: Method Not Allowed response for all other methods. 32 32 use <- wisp.require_method(req, Get) 33 33 34 - let html = string_builder.from_string("Hello, Joe!") 34 + let html = string_tree.from_string("Hello, Joe!") 35 35 wisp.ok() 36 36 |> wisp.html_body(html) 37 37 } ··· 48 48 49 49 fn list_comments() -> Response { 50 50 // In a later example we'll show how to read from a database. 51 - let html = string_builder.from_string("Comments!") 51 + let html = string_tree.from_string("Comments!") 52 52 wisp.ok() 53 53 |> wisp.html_body(html) 54 54 } 55 55 56 56 fn create_comment(_req: Request) -> Response { 57 57 // In a later example we'll show how to parse data from the request body. 58 - let html = string_builder.from_string("Created") 58 + let html = string_tree.from_string("Created") 59 59 wisp.created() 60 60 |> wisp.html_body(html) 61 61 } ··· 66 66 // The `id` path parameter has been passed to this function, so we could use 67 67 // it to look up a comment in a database. 68 68 // For now we'll just include in the response body. 69 - let html = string_builder.from_string("Comment with id " <> id) 69 + let html = string_tree.from_string("Comment with id " <> id) 70 70 wisp.ok() 71 71 |> wisp.html_body(html) 72 72 }
+3 -2
examples/01-routing/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+1 -1
examples/01-routing/test/app_test.gleam
··· 15 15 |> should.equal(200) 16 16 17 17 response.headers 18 - |> should.equal([#("content-type", "text/html")]) 18 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 19 19 20 20 response 21 21 |> testing.string_body
+2 -2
examples/02-working-with-form-data/README.md
··· 11 11 concepts from the [routing example][routing] so read those first. The additions 12 12 are detailed here and commented in the code. 13 13 14 - [hello]: https://github.com/lpil/wisp/tree/main/examples/0-hello-world 15 - [routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing 14 + [hello]: https://github.com/lpil/wisp/tree/main/examples/00-hello-world 15 + [routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing 16 16 17 17 ### `app/router` module 18 18
+1 -1
examples/02-working-with-form-data/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = "~> 0.14" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 11 gleam_http = "~> 3.5" 12 12 13 13 [dev-dependencies]
+3 -3
examples/02-working-with-form-data/src/app/router.gleam
··· 2 2 import gleam/http.{Get, Post} 3 3 import gleam/list 4 4 import gleam/result 5 - import gleam/string_builder 5 + import gleam/string_tree 6 6 import wisp.{type Request, type Response} 7 7 8 8 pub fn handle_request(req: Request) -> Response { ··· 21 21 // In a larger application a template library or HTML form library might 22 22 // be used here instead of a string literal. 23 23 let html = 24 - string_builder.from_string( 24 + string_tree.from_string( 25 25 "<form method='post'> 26 26 <label>Title: 27 27 <input type='text' name='title'> ··· 60 60 case result { 61 61 Ok(content) -> { 62 62 wisp.ok() 63 - |> wisp.html_body(string_builder.from_string(content)) 63 + |> wisp.html_body(string_tree.from_string(content)) 64 64 } 65 65 Error(_) -> { 66 66 wisp.bad_request()
+3 -2
examples/02-working-with-form-data/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+2 -2
examples/02-working-with-form-data/test/app_test.gleam
··· 15 15 |> should.equal(200) 16 16 17 17 response.headers 18 - |> should.equal([#("content-type", "text/html")]) 18 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 19 19 20 20 response 21 21 |> testing.string_body ··· 55 55 |> should.equal(200) 56 56 57 57 response.headers 58 - |> should.equal([#("content-type", "text/html")]) 58 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 59 59 60 60 response 61 61 |> testing.string_body
+2 -2
examples/03-working-with-json/README.md
··· 12 12 concepts from the [routing example][routing] so read those first. The additions 13 13 are detailed here and commented in the code. 14 14 15 - [hello]: https://github.com/lpil/wisp/tree/main/examples/0-hello-world 16 - [routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing 15 + [hello]: https://github.com/lpil/wisp/tree/main/examples/00-hello-world 16 + [routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing 17 17 18 18 ### `gleam.toml` file 19 19
+1 -1
examples/03-working-with-json/gleam.toml
··· 8 8 wisp = { path = "../.." } 9 9 gleam_json = "~> 0.6" 10 10 gleam_erlang = "~> 0.23" 11 - mist = "~> 0.14" 11 + mist = ">= 2.0.0 and < 3.0.0" 12 12 gleam_http = "~> 3.5" 13 13 14 14 [dev-dependencies]
+3 -2
examples/03-working-with-json/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+1 -1
examples/03-working-with-json/test/app_test.gleam
··· 49 49 |> should.equal(201) 50 50 51 51 response.headers 52 - |> should.equal([#("content-type", "application/json")]) 52 + |> should.equal([#("content-type", "application/json; charset=utf-8")]) 53 53 54 54 response 55 55 |> testing.string_body
+2 -2
examples/04-working-with-other-formats/README.md
··· 13 13 concepts from the [routing example][routing] so read those first. The additions 14 14 are detailed here and commented in the code. 15 15 16 - [hello]: https://github.com/lpil/wisp/tree/main/examples/0-hello-world 17 - [routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing 16 + [hello]: https://github.com/lpil/wisp/tree/main/examples/00-hello-world 17 + [routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing 18 18 19 19 ### `gleam.toml` file 20 20
+1 -1
examples/04-working-with-other-formats/gleam.toml
··· 8 8 wisp = { path = "../.." } 9 9 gsv = "~> 1.0" 10 10 gleam_erlang = "~> 0.23" 11 - mist = "~> 0.14" 11 + mist = ">= 2.0.0 and < 3.0.0" 12 12 gleam_http = "~> 3.5" 13 13 14 14 [dev-dependencies]
+3 -2
examples/04-working-with-other-formats/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+2 -2
examples/05-using-a-database/README.md
··· 1 - # Wisp Example: Using A Database 1 + # Wisp Example: Using a database 2 2 3 3 ```sh 4 4 gleam run # Run the server ··· 11 11 This example is based off of the ["working with JSON" example][json], so read 12 12 that first. The additions are detailed here and commented in the code. 13 13 14 - [json]: https://github.com/lpil/wisp/tree/main/examples/3-working-with-json 14 + [json]: https://github.com/lpil/wisp/tree/main/examples/03-working-with-json 15 15 16 16 ### `gleam.toml` file 17 17
+1 -1
examples/05-using-a-database/gleam.toml
··· 9 9 gleam_json = "~> 0.6" 10 10 tiny_database = { path = "../utilities/tiny_database" } 11 11 gleam_erlang = "~> 0.23" 12 - mist = "~> 0.14" 12 + mist = ">= 2.0.0 and < 3.0.0" 13 13 gleam_http = "~> 3.5" 14 14 15 15 [dev-dependencies]
+2 -2
examples/05-using-a-database/src/app/web/people.gleam
··· 1 1 import app/web.{type Context} 2 + import gleam/dict 2 3 import gleam/dynamic.{type Dynamic} 3 4 import gleam/http.{Get, Post} 4 5 import gleam/json 5 - import gleam/dict 6 6 import gleam/result.{try} 7 7 import tiny_database 8 8 import wisp.{type Request, type Response} ··· 122 122 // In this example we are not going to be reporting specific errors to the 123 123 // user, so we can discard the error and replace it with Nil. 124 124 result 125 - |> result.nil_error 125 + |> result.replace_error(Nil) 126 126 } 127 127 128 128 /// Save a person to the database and return the id of the newly created record.
+5 -4
examples/05-using-a-database/src/app.gleam
··· 1 + import app/router 2 + import app/web 1 3 import gleam/erlang/process 2 - import tiny_database 3 4 import mist 5 + import tiny_database 4 6 import wisp 5 - import app/router 6 - import app/web 7 + import wisp/wisp_mist 7 8 8 9 pub const data_directory = "tmp/data" 9 10 ··· 24 25 25 26 let assert Ok(_) = 26 27 handler 27 - |> wisp.mist_handler(secret_key_base) 28 + |> wisp_mist.handler(secret_key_base) 28 29 |> mist.new 29 30 |> mist.port(8000) 30 31 |> mist.start_http
+1 -1
examples/05-using-a-database/test/app_test.gleam
··· 40 40 response.status 41 41 |> should.equal(200) 42 42 response.headers 43 - |> should.equal([#("content-type", "application/json")]) 43 + |> should.equal([#("content-type", "application/json; charset=utf-8")]) 44 44 45 45 // Initially there are no people in the database 46 46 response
+4 -3
examples/06-serving-static-assets/README.md
··· 5 5 gleam test # Run the tests 6 6 ``` 7 7 8 - This example shows how to route requests to different handlers based on the 9 - request path and method. 8 + This example shows how to serve static assets. In this case we'll serve 9 + a CSS file for page styling and a JavaScript file for updating the content 10 + of the HTML page, but the same techniques can also be used for other file types. 10 11 11 12 This example is based off of the ["Hello, World!" example][hello], so read that 12 13 one first. The additions are detailed here and commented in the code. 13 14 14 - [hello]: https://github.com/lpil/wisp/tree/main/examples/1-routing 15 + [hello]: https://github.com/lpil/wisp/tree/main/examples/01-routing 15 16 16 17 ### `priv/static` directory 17 18
+1 -1
examples/06-serving-static-assets/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = "~> 0.14" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 11 gleam_http = "~> 3.5" 12 12 13 13 [dev-dependencies]
+3 -3
examples/06-serving-static-assets/src/app/router.gleam
··· 1 - import wisp.{type Request, type Response} 2 - import gleam/string_builder 3 1 import app/web.{type Context} 2 + import gleam/string_tree 3 + import wisp.{type Request, type Response} 4 4 5 5 const html = "<!DOCTYPE html> 6 6 <html lang=\"en\"> ··· 18 18 19 19 pub fn handle_request(req: Request, ctx: Context) -> Response { 20 20 use _req <- web.middleware(req, ctx) 21 - wisp.html_response(string_builder.from_string(html), 200) 21 + wisp.html_response(string_tree.from_string(html), 200) 22 22 }
+4 -3
examples/06-serving-static-assets/src/app.gleam
··· 1 + import app/router 2 + import app/web.{Context} 1 3 import gleam/erlang/process 2 4 import mist 3 5 import wisp 4 - import app/router 5 - import app/web.{Context} 6 + import wisp/wisp_mist 6 7 7 8 pub fn main() { 8 9 wisp.configure_logger() ··· 16 17 let handler = router.handle_request(_, ctx) 17 18 18 19 let assert Ok(_) = 19 - wisp.mist_handler(handler, secret_key_base) 20 + wisp_mist.handler(handler, secret_key_base) 20 21 |> mist.new 21 22 |> mist.port(8000) 22 23 |> mist.start_http
+6 -6
examples/06-serving-static-assets/test/app_test.gleam
··· 1 + import app 2 + import app/router 3 + import app/web.{type Context, Context} 1 4 import gleeunit 2 5 import gleeunit/should 3 6 import wisp/testing 4 - import app 5 - import app/router 6 - import app/web.{type Context, Context} 7 7 8 8 pub fn main() { 9 9 gleeunit.main() ··· 26 26 |> should.equal(200) 27 27 28 28 response.headers 29 - |> should.equal([#("content-type", "text/html")]) 29 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 30 30 } 31 31 32 32 pub fn get_stylesheet_test() { ··· 38 38 |> should.equal(200) 39 39 40 40 response.headers 41 - |> should.equal([#("content-type", "text/css")]) 41 + |> should.equal([#("content-type", "text/css; charset=utf-8")]) 42 42 } 43 43 44 44 pub fn get_javascript_test() { ··· 50 50 |> should.equal(200) 51 51 52 52 response.headers 53 - |> should.equal([#("content-type", "text/javascript")]) 53 + |> should.equal([#("content-type", "text/javascript; charset=utf-8")]) 54 54 }
+2 -3
examples/07-logging/README.md
··· 5 5 gleam test # Run the tests 6 6 ``` 7 7 8 - This example shows how to route requests to different handlers based on the 9 - request path and method. 8 + This example shows how to log messages using the BEAM logger. 10 9 11 10 This example is based off of the ["routing" example][routing], so read that 12 11 one first. The additions are detailed here and commented in the code. 13 12 14 - [routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing 13 + [routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing 15 14 16 15 ### `app/router` module 17 16
+1 -1
examples/07-logging/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = "~> 0.14" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 11 12 12 [dev-dependencies] 13 13 gleeunit = "~> 1.0"
+3 -2
examples/07-logging/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+2 -2
examples/08-working-with-cookies/README.md
··· 8 8 This example shows how to read and write cookies, and how to sign cookies so 9 9 they cannot be tampered with. 10 10 11 - This example is based off of the [working with form data example][form-data] so read that one 11 + This example is based off of the ["working with form data" example][form-data] so read that one 12 12 first. The additions are detailed here and commented in the code. 13 13 14 14 Signing of cookies uses the `secret_key_base` value. If this value changes then ··· 17 17 example application generates a random string in `app.gleam`, but in a real 18 18 application you will need to read this secret value from somewhere secure. 19 19 20 - [routing]: https://github.com/lpil/wisp/tree/main/examples/2-working-with-form-data 20 + [form-data]: https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data 21 21 22 22 ### `app/router` module 23 23
+1 -1
examples/08-working-with-cookies/gleam.toml
··· 8 8 wisp = { path = "../.." } 9 9 gleam_crypto = "~> 1.0" 10 10 gleam_erlang = "~> 0.23" 11 - mist = "~> 0.14" 11 + mist = ">= 2.0.0 and < 3.0.0" 12 12 gleam_http = "~> 3.5" 13 13 14 14 [dev-dependencies]
+3 -3
examples/08-working-with-cookies/src/app/router.gleam
··· 1 1 import app/web 2 2 import gleam/http.{Delete, Get, Post} 3 3 import gleam/list 4 - import gleam/string_builder 4 + import gleam/string_tree 5 5 import wisp.{type Request, type Response} 6 6 7 7 const cookie_name = "id" ··· 25 25 " <button type='submit'>Log out</button>", 26 26 "</form>", 27 27 ] 28 - |> string_builder.from_strings 28 + |> string_tree.from_strings 29 29 |> wisp.html_response(200) 30 30 } 31 31 Error(_) -> { ··· 52 52 <button type='submit'>Log in</button> 53 53 </form> 54 54 " 55 - |> string_builder.from_string 55 + |> string_tree.from_string 56 56 |> wisp.html_response(200) 57 57 } 58 58
+3 -2
examples/08-working-with-cookies/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+3 -3
examples/09-configuring-default-responses/README.md
··· 1 - # Wisp Example: Working with form data 1 + # Wisp Example: Configuring default responses 2 2 3 3 ```sh 4 4 gleam run # Run the server ··· 12 12 13 13 You likely want your application to return a generic error page rather than an empty body, and this example shows how to do that. 14 14 15 - This example is based off of the [routing example][routing] so read that first. 15 + This example is based off of the ["routing" example][routing] so read that first. 16 16 The additions are detailed here and commented in the code. 17 17 18 - [routing]: https://github.com/lpil/wisp/tree/main/examples/1-routing 18 + [routing]: https://github.com/lpil/wisp/tree/main/examples/01-routing 19 19 20 20 ### `app/router` module 21 21
+1 -1
examples/09-configuring-default-responses/gleam.toml
··· 6 6 [dependencies] 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 - mist = "~> 0.14" 9 + mist = ">= 2.0.0 and < 3.0.0" 10 10 gleam_erlang = "~> 0.23" 11 11 12 12 [dev-dependencies]
+2 -2
examples/09-configuring-default-responses/src/app/router.gleam
··· 1 1 import app/web 2 - import gleam/string_builder 2 + import gleam/string_tree 3 3 import wisp.{type Request, type Response} 4 4 5 5 pub fn handle_request(req: Request) -> Response { ··· 9 9 // This request returns a non-empty body. 10 10 [] -> { 11 11 "<h1>Hello, Joe!</h1>" 12 - |> string_builder.from_string 12 + |> string_tree.from_string 13 13 |> wisp.html_response(200) 14 14 } 15 15
+6 -6
examples/09-configuring-default-responses/src/app/web.gleam
··· 1 - import wisp 2 1 import gleam/bool 3 - import gleam/string_builder 2 + import gleam/string_tree 3 + import wisp 4 4 5 5 pub fn middleware( 6 6 req: wisp.Request, ··· 32 32 case response.status { 33 33 404 | 405 -> 34 34 "<h1>There's nothing here</h1>" 35 - |> string_builder.from_string 35 + |> string_tree.from_string 36 36 |> wisp.html_body(response, _) 37 37 38 38 400 | 422 -> 39 39 "<h1>Bad request</h1>" 40 - |> string_builder.from_string 40 + |> string_tree.from_string 41 41 |> wisp.html_body(response, _) 42 42 43 43 413 -> 44 44 "<h1>Request entity too large</h1>" 45 - |> string_builder.from_string 45 + |> string_tree.from_string 46 46 |> wisp.html_body(response, _) 47 47 48 48 500 -> 49 49 "<h1>Internal server error</h1>" 50 - |> string_builder.from_string 50 + |> string_tree.from_string 51 51 |> wisp.html_body(response, _) 52 52 53 53 // For other status codes redirect to the home page
+3 -2
examples/09-configuring-default-responses/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+7 -7
examples/09-configuring-default-responses/test/app_test.gleam
··· 15 15 |> should.equal(200) 16 16 17 17 response.headers 18 - |> should.equal([#("content-type", "text/html")]) 18 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 19 19 20 20 let assert True = 21 21 response ··· 31 31 |> should.equal(500) 32 32 33 33 response.headers 34 - |> should.equal([#("content-type", "text/html")]) 34 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 35 35 36 36 let assert True = 37 37 response ··· 46 46 |> should.equal(422) 47 47 48 48 response.headers 49 - |> should.equal([#("content-type", "text/html")]) 49 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 50 50 51 51 let assert True = 52 52 response ··· 61 61 |> should.equal(400) 62 62 63 63 response.headers 64 - |> should.equal([#("content-type", "text/html")]) 64 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 65 65 66 66 let assert True = 67 67 response ··· 76 76 |> should.equal(405) 77 77 78 78 response.headers 79 - |> should.equal([#("allow", ""), #("content-type", "text/html")]) 79 + |> should.equal([#("allow", ""), #("content-type", "text/html; charset=utf-8")]) 80 80 81 81 let assert True = 82 82 response ··· 91 91 |> should.equal(404) 92 92 93 93 response.headers 94 - |> should.equal([#("content-type", "text/html")]) 94 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 95 95 96 96 let assert True = 97 97 response ··· 106 106 |> should.equal(413) 107 107 108 108 response.headers 109 - |> should.equal([#("content-type", "text/html")]) 109 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 110 110 111 111 let assert True = 112 112 response
+2 -3
examples/10-working-with-files/README.md
··· 7 7 8 8 This example shows how to accept file uploads and allow users to download files. 9 9 10 - This example is based off of the ["Working with form data" example][formdata], 10 + This example is based off of the ["working with form data" example][formdata], 11 11 so read that first. The additions are detailed here and commented in the code. 12 12 13 13 [formdata]: https://github.com/lpil/wisp/tree/main/examples/02-working-with-form-data ··· 18 18 19 19 ### `app_test` module 20 20 21 - Tests have been added that send requests with form data bodies and check that 22 - the expected response is returned. 21 + Tests have been added that upload and download files to verify the behaviour. 23 22 24 23 ### Other files 25 24
+1 -1
examples/10-working-with-files/gleam.toml
··· 7 7 gleam_stdlib = "~> 0.30" 8 8 wisp = { path = "../.." } 9 9 gleam_erlang = "~> 0.23" 10 - mist = "~> 0.14" 10 + mist = ">= 2.0.0 and < 3.0.0" 11 11 gleam_http = "~> 3.5" 12 12 13 13 [dev-dependencies]
+8 -8
examples/10-working-with-files/src/app/router.gleam
··· 1 1 import app/web 2 + import gleam/bytes_tree 2 3 import gleam/http.{Get, Post} 3 4 import gleam/list 4 5 import gleam/result 5 - import gleam/string_builder 6 - import gleam/bytes_builder 6 + import gleam/string_tree 7 7 import wisp.{type Request, type Response} 8 8 9 9 pub fn handle_request(req: Request) -> Response { ··· 35 35 fn show_home(req: Request) -> Response { 36 36 use <- wisp.require_method(req, Get) 37 37 html 38 - |> string_builder.from_string 38 + |> string_tree.from_string 39 39 |> wisp.html_response(200) 40 40 } 41 41 ··· 45 45 // In this case we have the file contents in memory as a string. 46 46 // This is good if we have just made the file, but if the file already exists 47 47 // on the disc then the approach in the next function is more efficient. 48 - let file_contents = bytes_builder.from_string("Hello, Joe!") 48 + let file_contents = bytes_tree.from_string("Hello, Joe!") 49 49 50 50 wisp.ok() 51 51 |> wisp.set_header("content-type", "text/plain") 52 52 // The content-disposition header is set by this function to ensure this is 53 53 // treated as a file download. If the file was uploaded by the user then you 54 - // want to ensure that this header is ste as otherwise the browser may try to 55 - // display the file, which could enable in cross-site scripting attacks. 54 + // want to ensure that this header is set as otherwise the browser may try to 55 + // display the file, which could enable cross-site scripting attacks. 56 56 |> wisp.file_download_from_memory( 57 57 named: "hello.txt", 58 58 containing: file_contents, ··· 63 63 use <- wisp.require_method(req, Get) 64 64 65 65 // In this case the file exists on the disc. 66 - // Here's we're using the project README, but in a real application you'd 66 + // Here we're using the project README, but in a real application you'd 67 67 // probably have an absolute path to wherever it is you keep your files. 68 68 let file_path = "./README.md" 69 69 ··· 107 107 case result { 108 108 Ok(name) -> { 109 109 { "<p>Thank you for your file!" <> name <> "</p>" <> html } 110 - |> string_builder.from_string 110 + |> string_tree.from_string 111 111 |> wisp.html_response(200) 112 112 } 113 113 Error(_) -> {
+3 -2
examples/10-working-with-files/src/app.gleam
··· 1 + import app/router 1 2 import gleam/erlang/process 2 3 import mist 3 4 import wisp 4 - import app/router 5 + import wisp/wisp_mist 5 6 6 7 pub fn main() { 7 8 wisp.configure_logger() 8 9 let secret_key_base = wisp.random_string(64) 9 10 10 11 let assert Ok(_) = 11 - wisp.mist_handler(router.handle_request, secret_key_base) 12 + wisp_mist.handler(router.handle_request, secret_key_base) 12 13 |> mist.new 13 14 |> mist.port(8000) 14 15 |> mist.start_http
+1 -1
examples/10-working-with-files/test/app_test.gleam
··· 15 15 |> should.equal(200) 16 16 17 17 response.headers 18 - |> should.equal([#("content-type", "text/html")]) 18 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 19 19 20 20 response 21 21 |> testing.string_body
+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" }
+4 -4
examples/utilities/tiny_database/src/tiny_database.gleam
··· 1 - import gleam/result 1 + import gleam/dict.{type Dict} 2 2 import gleam/dynamic 3 - import ids/nanoid 4 3 import gleam/json 5 4 import gleam/list 6 - import gleam/dict.{type Dict} 5 + import gleam/result 7 6 import simplifile 7 + import youid/uuid 8 8 9 9 pub opaque type Connection { 10 10 Connection(root: String) ··· 44 44 values: Dict(String, String), 45 45 ) -> Result(String, Nil) { 46 46 let assert Ok(_) = simplifile.create_directory_all(connection.root) 47 - let id = nanoid.generate() 47 + let id = uuid.v4_string() 48 48 let values = 49 49 values 50 50 |> dict.to_list
+2 -2
examples/utilities/tiny_database/test/tiny_database_test.gleam
··· 1 + import gleam/dict 1 2 import gleeunit 2 3 import gleeunit/should 3 4 import tiny_database 4 - import gleam/map 5 5 6 6 pub fn main() { 7 7 gleeunit.main() ··· 10 10 pub fn insert_read_test() { 11 11 let connection = tiny_database.connect("tmp/data") 12 12 13 - let data = map.from_list([#("name", "Alice"), #("profession", "Programmer")]) 13 + let data = dict.from_list([#("name", "Alice"), #("profession", "Programmer")]) 14 14 15 15 let assert Ok(Nil) = tiny_database.truncate(connection) 16 16 let assert Ok([]) = tiny_database.list(connection)
+15 -16
gleam.toml
··· 1 1 name = "wisp" 2 - version = "0.12.0" 3 - gleam = ">= 0.32.0" 2 + version = "1.3.0" 3 + gleam = ">= 1.4.0" 4 4 description = "A practical web framework for Gleam" 5 5 licences = ["Apache-2.0"] 6 6 7 7 repository = { type = "github", user = "gleam-wisp", repo = "wisp" } 8 - links = [ 9 - { title = "Sponsor", href = "https://github.com/sponsors/lpil" }, 10 - ] 8 + links = [{ title = "Sponsor", href = "https://github.com/sponsors/lpil" }] 11 9 12 10 [dependencies] 13 - exception = "~> 2.0" 14 - gleam_crypto = "~> 1.0" 15 - gleam_erlang = "~> 0.21" 16 - gleam_http = "~> 3.5" 17 - gleam_json = "~> 0.6 or ~> 1.0" 18 - gleam_stdlib = "~> 0.29 or ~> 1.0" 19 - mist = "~> 0.13" 20 - simplifile = "~> 1.4" 21 - marceau = "~> 1.1" 22 - logging = "~> 1.0" 11 + exception = ">= 2.0.0 and < 3.0.0" 12 + gleam_crypto = ">= 1.0.0 and < 2.0.0" 13 + gleam_erlang = ">= 0.21.0 and < 2.0.0" 14 + gleam_http = ">= 3.5.0 and < 4.0.0" 15 + gleam_json = ">= 0.6.0 and < 3.0.0" 16 + gleam_stdlib = ">= 0.43.0 and < 2.0.0" 17 + mist = ">= 1.2.0 and < 4.0.0" 18 + simplifile = ">= 2.0.0 and < 3.0.0" 19 + marceau = ">= 1.1.0 and < 2.0.0" 20 + logging = ">= 1.2.0 and < 2.0.0" 21 + directories = ">= 1.0.0 and < 2.0.0" 23 22 24 23 [dev-dependencies] 25 - gleeunit = "~> 1.0" 24 + gleeunit = ">= 1.0.0 and < 2.0.0"
+33 -24
manifest.toml
··· 2 2 # You typically do not need to edit this file 3 3 4 4 packages = [ 5 + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, 6 + { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, 7 + { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, 5 8 { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, 6 - { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, 7 - { name = "gleam_erlang", version = "0.24.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "26BDB52E61889F56A291CB34167315780EE4AA20961917314446542C90D1C1A0" }, 8 - { name = "gleam_http", version = "3.5.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "C2FC3322203B16F897C1818D9810F5DEFCE347F0751F3B44421E1261277A7373" }, 9 - { name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" }, 10 - { name = "gleam_otp", version = "0.9.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5FADBBEC5ECF3F8B6BE91101D432758503192AE2ADBAD5602158977341489F71" }, 11 - { name = "gleam_stdlib", version = "0.35.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5443EEB74708454B65650FEBBB1EF5175057D1DEC62AEA9D7C6D96F41DA79152" }, 12 - { name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" }, 13 - { name = "glisten", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "73BC09C8487C2FFC0963BFAB33ED2F0D636FDFA43B966E65C1251CBAB8458099" }, 14 - { name = "logging", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "82C112ED9B6C30C1772A6FE2613B94B13F62EA35F5869A2630D13948D297BD39" }, 15 - { name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" }, 16 - { name = "mist", version = "0.17.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten"], otp_app = "mist", source = "hex", outer_checksum = "DA8ACEE52C1E4892A75181B3166A4876D8CBC69D555E4770250BC84C80F75524" }, 17 - { name = "simplifile", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "AAFCF154F69B237D269FF2764890F61ABC4A7EF2A592D44D67627B99694539D9" }, 18 - { name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" }, 9 + { name = "filepath", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "67A6D15FB39EEB69DD31F8C145BB5A421790581BD6AA14B33D64D5A55DBD6587" }, 10 + { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, 11 + { name = "gleam_erlang", version = "0.30.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "760618870AE4A497B10C73548E6E44F43B76292A54F0207B3771CBB599C675B4" }, 12 + { name = "gleam_http", version = "3.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "A9EE0722106FCCAB8AD3BF9D0A3EFF92BFE8561D59B83BAE96EB0BE1938D4E0F" }, 13 + { name = "gleam_json", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "0A57FB5666E695FD2BEE74C0428A98B0FC11A395D2C7B4CDF5E22C5DD32C74C6" }, 14 + { name = "gleam_otp", version = "0.14.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "5A8CE8DBD01C29403390A7BD5C0A63D26F865C83173CF9708E6E827E53159C65" }, 15 + { name = "gleam_stdlib", version = "0.43.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "69EF22E78FDCA9097CBE7DF91C05B2A8B5436826D9F66680D879182C0860A747" }, 16 + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, 17 + { name = "glisten", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "912132751031473CB38F454120124FFC96AF6B0EA33D92C9C90DB16327A2A972" }, 18 + { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, 19 + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 20 + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 21 + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 22 + { name = "mist", version = "3.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "CDA1A74E768419235E16886463EC4722EFF4AB3F8D820A76EAD45D7C167D7282" }, 23 + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, 24 + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, 25 + { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, 26 + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 19 27 ] 20 28 21 29 [requirements] 22 - exception = { version = "~> 2.0" } 23 - gleam_crypto = { version = "~> 1.0" } 24 - gleam_erlang = { version = "~> 0.21" } 25 - gleam_http = { version = "~> 3.5" } 26 - gleam_json = { version = "~> 0.6 or ~> 1.0" } 27 - gleam_stdlib = { version = "~> 0.29 or ~> 1.0" } 28 - gleeunit = { version = "~> 1.0" } 29 - logging = { version = "~> 1.0" } 30 - marceau = { version = "~> 1.1" } 31 - mist = { version = "~> 0.13" } 32 - simplifile = { version = "~> 1.4" } 30 + directories = { version = ">= 1.0.0 and < 2.0.0" } 31 + exception = { version = ">= 2.0.0 and < 3.0.0" } 32 + gleam_crypto = { version = ">= 1.0.0 and < 2.0.0" } 33 + gleam_erlang = { version = ">= 0.21.0 and < 2.0.0" } 34 + gleam_http = { version = ">= 3.5.0 and < 4.0.0" } 35 + gleam_json = { version = ">= 0.6.0 and < 3.0.0" } 36 + gleam_stdlib = { version = ">= 0.43.0 and < 2.0.0" } 37 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 38 + logging = { version = ">= 1.2.0 and < 2.0.0" } 39 + marceau = { version = ">= 1.1.0 and < 2.0.0" } 40 + mist = { version = ">= 1.2.0 and < 4.0.0" } 41 + simplifile = { version = ">= 2.0.0 and < 3.0.0" }
+90
src/wisp/internal.gleam
··· 1 + import directories 2 + import gleam/bit_array 3 + import gleam/crypto 4 + import gleam/string 5 + 6 + // HELPERS 7 + 8 + // 9 + // Requests 10 + // 11 + 12 + /// The connection to the client for a HTTP request. 13 + /// 14 + /// The body of the request can be read from this connection using functions 15 + /// such as `require_multipart_body`. 16 + /// 17 + pub type Connection { 18 + Connection( 19 + reader: Reader, 20 + max_body_size: Int, 21 + max_files_size: Int, 22 + read_chunk_size: Int, 23 + secret_key_base: String, 24 + temporary_directory: String, 25 + ) 26 + } 27 + 28 + pub fn make_connection( 29 + body_reader: Reader, 30 + secret_key_base: String, 31 + ) -> Connection { 32 + // Fallback to current working directory when no valid tmp directory exists 33 + let prefix = case directories.tmp_dir() { 34 + Ok(tmp_dir) -> tmp_dir <> "/gleam-wisp/" 35 + Error(_) -> "./tmp/" 36 + } 37 + let temporary_directory = join_path(prefix, random_slug()) 38 + Connection( 39 + reader: body_reader, 40 + max_body_size: 8_000_000, 41 + max_files_size: 32_000_000, 42 + read_chunk_size: 1_000_000, 43 + temporary_directory: temporary_directory, 44 + secret_key_base: secret_key_base, 45 + ) 46 + } 47 + 48 + type Reader = 49 + fn(Int) -> Result(Read, Nil) 50 + 51 + pub type Read { 52 + Chunk(BitArray, next: Reader) 53 + ReadingFinished 54 + } 55 + 56 + // 57 + // Middleware Helpers 58 + // 59 + 60 + pub fn remove_preceeding_slashes(string: String) -> String { 61 + case string { 62 + "/" <> rest -> remove_preceeding_slashes(rest) 63 + _ -> string 64 + } 65 + } 66 + 67 + // TODO: replace with simplifile function when it exists 68 + pub fn join_path(a: String, b: String) -> String { 69 + let b = remove_preceeding_slashes(b) 70 + case string.ends_with(a, "/") { 71 + True -> a <> b 72 + False -> a <> "/" <> b 73 + } 74 + } 75 + 76 + // 77 + // Cryptography 78 + // 79 + 80 + /// Generate a random string of the given length. 81 + /// 82 + pub fn random_string(length: Int) -> String { 83 + crypto.strong_random_bytes(length) 84 + |> bit_array.base64_url_encode(False) 85 + |> string.slice(0, length) 86 + } 87 + 88 + pub fn random_slug() -> String { 89 + random_string(16) 90 + }
+6 -7
src/wisp/testing.gleam
··· 1 1 import gleam/bit_array 2 - import gleam/bytes_builder 2 + import gleam/bytes_tree 3 3 import gleam/crypto 4 4 import gleam/http 5 5 import gleam/http/request 6 6 import gleam/json.{type Json} 7 7 import gleam/option.{None, Some} 8 8 import gleam/string 9 - import gleam/string_builder 9 + import gleam/string_tree 10 10 import gleam/uri 11 11 import simplifile 12 12 import wisp.{type Request, type Response, Bytes, Empty, File, Text} ··· 227 227 pub fn string_body(response: Response) -> String { 228 228 case response.body { 229 229 Empty -> "" 230 - Text(builder) -> string_builder.to_string(builder) 230 + Text(tree) -> string_tree.to_string(tree) 231 231 Bytes(bytes) -> { 232 - let data = bytes_builder.to_bit_array(bytes) 232 + let data = bytes_tree.to_bit_array(bytes) 233 233 let assert Ok(string) = bit_array.to_string(data) 234 234 string 235 235 } ··· 250 250 pub fn bit_array_body(response: Response) -> BitArray { 251 251 case response.body { 252 252 Empty -> <<>> 253 - Bytes(builder) -> bytes_builder.to_bit_array(builder) 254 - Text(builder) -> 255 - bytes_builder.to_bit_array(bytes_builder.from_string_builder(builder)) 253 + Bytes(tree) -> bytes_tree.to_bit_array(tree) 254 + Text(tree) -> bytes_tree.to_bit_array(bytes_tree.from_string_tree(tree)) 256 255 File(path) -> { 257 256 let assert Ok(contents) = simplifile.read_bits(path) 258 257 contents
+96
src/wisp/wisp_mist.gleam
··· 1 + import exception 2 + import gleam/bytes_tree 3 + import gleam/http/request.{type Request as HttpRequest} 4 + import gleam/http/response.{type Response as HttpResponse} 5 + import gleam/option 6 + import gleam/result 7 + import gleam/string 8 + import mist 9 + import wisp 10 + import wisp/internal 11 + 12 + // 13 + // Running the server 14 + // 15 + 16 + /// Convert a Wisp request handler into a function that can be run with the Mist 17 + /// web server. 18 + /// 19 + /// # Examples 20 + /// 21 + /// ```gleam 22 + /// pub fn main() { 23 + /// let secret_key_base = "..." 24 + /// let assert Ok(_) = 25 + /// handle_request 26 + /// |> wisp_mist.handler(secret_key_base) 27 + /// |> mist.new 28 + /// |> mist.port(8000) 29 + /// |> mist.start_http 30 + /// process.sleep_forever() 31 + /// } 32 + /// ``` 33 + pub fn handler( 34 + handler: fn(wisp.Request) -> wisp.Response, 35 + secret_key_base: String, 36 + ) -> fn(HttpRequest(mist.Connection)) -> HttpResponse(mist.ResponseData) { 37 + fn(request: HttpRequest(_)) { 38 + let connection = 39 + internal.make_connection(mist_body_reader(request), secret_key_base) 40 + let request = request.set_body(request, connection) 41 + 42 + use <- exception.defer(fn() { 43 + let assert Ok(_) = wisp.delete_temporary_files(request) 44 + }) 45 + 46 + let response = 47 + request 48 + |> handler 49 + |> mist_response 50 + 51 + response 52 + } 53 + } 54 + 55 + fn mist_body_reader(request: HttpRequest(mist.Connection)) -> internal.Reader { 56 + case mist.stream(request) { 57 + Error(_) -> fn(_) { Ok(internal.ReadingFinished) } 58 + Ok(stream) -> fn(size) { wrap_mist_chunk(stream(size)) } 59 + } 60 + } 61 + 62 + fn wrap_mist_chunk( 63 + chunk: Result(mist.Chunk, mist.ReadError), 64 + ) -> Result(internal.Read, Nil) { 65 + chunk 66 + |> result.replace_error(Nil) 67 + |> result.map(fn(chunk) { 68 + case chunk { 69 + mist.Done -> internal.ReadingFinished 70 + mist.Chunk(data, consume) -> 71 + internal.Chunk(data, fn(size) { wrap_mist_chunk(consume(size)) }) 72 + } 73 + }) 74 + } 75 + 76 + fn mist_response(response: wisp.Response) -> HttpResponse(mist.ResponseData) { 77 + let body = case response.body { 78 + wisp.Empty -> mist.Bytes(bytes_tree.new()) 79 + wisp.Text(text) -> mist.Bytes(bytes_tree.from_string_tree(text)) 80 + wisp.Bytes(bytes) -> mist.Bytes(bytes) 81 + wisp.File(path) -> mist_send_file(path) 82 + } 83 + response 84 + |> response.set_body(body) 85 + } 86 + 87 + fn mist_send_file(path: String) -> mist.ResponseData { 88 + case mist.send_file(path, offset: 0, limit: option.None) { 89 + Ok(body) -> body 90 + Error(error) -> { 91 + wisp.log_error(string.inspect(error)) 92 + // TODO: return 500 93 + mist.Bytes(bytes_tree.new()) 94 + } 95 + } 96 + }
+354 -314
src/wisp.gleam
··· 1 1 import exception 2 - import gleam/bytes_builder.{type BytesBuilder} 3 2 import gleam/bit_array 4 3 import gleam/bool 5 - import gleam/dict.{type Dict} 4 + import gleam/bytes_tree.{type BytesTree} 6 5 import gleam/crypto 6 + import gleam/dict.{type Dict} 7 7 import gleam/dynamic.{type Dynamic} 8 8 import gleam/erlang 9 + import gleam/erlang/atom.{type Atom} 9 10 import gleam/http.{type Method} 10 11 import gleam/http/cookie 11 12 import gleam/http/request.{type Request as HttpRequest} ··· 13 14 type Response as HttpResponse, Response as HttpResponse, 14 15 } 15 16 import gleam/int 16 - import gleam/erlang/atom.{type Atom} 17 17 import gleam/json 18 18 import gleam/list 19 19 import gleam/option.{type Option} 20 20 import gleam/result 21 21 import gleam/string 22 - import gleam/string_builder.{type StringBuilder} 22 + import gleam/string_tree.{type StringTree} 23 23 import gleam/uri 24 24 import logging 25 25 import marceau 26 - import mist 27 26 import simplifile 28 - 29 - // 30 - // Running the server 31 - // 32 - 33 - /// Convert a Wisp request handler into a function that can be run with the Mist 34 - /// web server. 35 - /// 36 - /// # Examples 37 - /// 38 - /// ```gleam 39 - /// pub fn main() { 40 - /// let secret_key_base = "..." 41 - /// let assert Ok(_) = 42 - /// handle_request 43 - /// |> wisp.mist_handler(secret_key_base) 44 - /// |> mist.new 45 - /// |> mist.port(8000) 46 - /// |> mist.start_http 47 - /// process.sleep_forever() 48 - /// } 49 - /// ``` 50 - pub fn mist_handler( 51 - handler: fn(Request) -> Response, 52 - secret_key_base: String, 53 - ) -> fn(HttpRequest(mist.Connection)) -> HttpResponse(mist.ResponseData) { 54 - fn(request: HttpRequest(_)) { 55 - let connection = make_connection(mist_body_reader(request), secret_key_base) 56 - let request = request.set_body(request, connection) 57 - 58 - use <- exception.defer(fn() { 59 - let assert Ok(_) = delete_temporary_files(request) 60 - }) 61 - 62 - let response = 63 - request 64 - |> handler 65 - |> mist_response 66 - 67 - response 68 - } 69 - } 70 - 71 - fn mist_body_reader(request: HttpRequest(mist.Connection)) -> Reader { 72 - case mist.stream(request) { 73 - Error(_) -> fn(_) { Ok(ReadingFinished) } 74 - Ok(stream) -> fn(size) { wrap_mist_chunk(stream(size)) } 75 - } 76 - } 77 - 78 - fn wrap_mist_chunk( 79 - chunk: Result(mist.Chunk, mist.ReadError), 80 - ) -> Result(Read, Nil) { 81 - chunk 82 - |> result.nil_error 83 - |> result.map(fn(chunk) { 84 - case chunk { 85 - mist.Done -> ReadingFinished 86 - mist.Chunk(data, consume) -> 87 - Chunk(data, fn(size) { wrap_mist_chunk(consume(size)) }) 88 - } 89 - }) 90 - } 91 - 92 - fn mist_response(response: Response) -> HttpResponse(mist.ResponseData) { 93 - let body = case response.body { 94 - Empty -> mist.Bytes(bytes_builder.new()) 95 - Text(text) -> mist.Bytes(bytes_builder.from_string_builder(text)) 96 - Bytes(bytes) -> mist.Bytes(bytes) 97 - File(path) -> mist_send_file(path) 98 - } 99 - response 100 - |> response.set_body(body) 101 - } 102 - 103 - fn mist_send_file(path: String) -> mist.ResponseData { 104 - case mist.send_file(path, offset: 0, limit: option.None) { 105 - Ok(body) -> body 106 - Error(error) -> { 107 - log_error(string.inspect(error)) 108 - // TODO: return 500 109 - mist.Bytes(bytes_builder.new()) 110 - } 111 - } 112 - } 27 + import wisp/internal 113 28 114 29 // 115 30 // Responses ··· 120 35 pub type Body { 121 36 /// A body of unicode text. 122 37 /// 123 - /// The body is represented using a `StringBuilder`. If you have a `String` 124 - /// you can use the `string_builder.from_string` function to convert it. 38 + /// The body is represented using a `StringTree`. If you have a `String` 39 + /// you can use the `string_tree.from_string` function to convert it. 125 40 /// 126 - Text(StringBuilder) 41 + Text(StringTree) 127 42 /// A body of binary data. 128 43 /// 129 - /// The body is represented using a `StringBuilder`. If you have a `String` 130 - /// you can use the `string_builder.from_string` function to convert it. 44 + /// The body is represented using a `BytesTree`. If you have a `BitArray` 45 + /// you can use the `bytes_tree.from_bit_array` function to convert it. 131 46 /// 132 - Bytes(BytesBuilder) 47 + Bytes(BytesTree) 133 48 /// A body of the contents of a file. 134 49 /// 135 50 /// This will be sent efficiently using the `send_file` function of the ··· 152 67 HttpResponse(Body) 153 68 154 69 /// Create an empty response with the given status code. 155 - /// 70 + /// 156 71 /// # Examples 157 - /// 72 + /// 158 73 /// ```gleam 159 74 /// response(200) 160 75 /// // -> Response(200, [], Empty) 161 76 /// ``` 162 - /// 77 + /// 163 78 pub fn response(status: Int) -> Response { 164 79 HttpResponse(status, [], Empty) 165 80 } 166 81 167 82 /// Set the body of a response. 168 - /// 83 + /// 169 84 /// # Examples 170 - /// 85 + /// 171 86 /// ```gleam 172 87 /// response(200) 173 88 /// |> set_body(File("/tmp/myfile.txt")) 174 89 /// // -> Response(200, [], File("/tmp/myfile.txt")) 175 90 /// ``` 176 - /// 91 + /// 177 92 pub fn set_body(response: Response, body: Body) -> Response { 178 93 response 179 94 |> response.set_body(body) ··· 193 108 /// `set_body` function with the `File` body variant. 194 109 /// 195 110 /// # Examples 196 - /// 111 + /// 197 112 /// ```gleam 198 113 /// response(200) 199 114 /// |> file_download(named: "myfile.txt", from: "/tmp/myfile.txt") ··· 229 144 /// as this can result in cross-site scripting vulnerabilities. 230 145 /// 231 146 /// # Examples 232 - /// 147 + /// 233 148 /// ```gleam 149 + /// let content = bytes_tree.from_string("Hello, Joe!") 234 150 /// response(200) 235 - /// |> file_download_from_memory(named: "myfile.txt", containing: "Hello, Joe!") 151 + /// |> file_download_from_memory(named: "myfile.txt", containing: content) 236 152 /// // -> Response( 237 153 /// // 200, 238 154 /// // [#("content-disposition", "attachment; filename=\"myfile.txt\"")], ··· 243 159 pub fn file_download_from_memory( 244 160 response: Response, 245 161 named name: String, 246 - containing data: BytesBuilder, 162 + containing data: BytesTree, 247 163 ) -> Response { 248 164 let name = uri.percent_encode(name) 249 165 response ··· 255 171 } 256 172 257 173 /// Create a HTML response. 258 - /// 174 + /// 259 175 /// The body is expected to be valid HTML, though this is not validated. 260 176 /// The `content-type` header will be set to `text/html`. 261 - /// 177 + /// 262 178 /// # Examples 263 - /// 179 + /// 264 180 /// ```gleam 265 - /// let body = string_builder.from_string("<h1>Hello, Joe!</h1>") 181 + /// let body = string_tree.from_string("<h1>Hello, Joe!</h1>") 266 182 /// html_response(body, 200) 267 183 /// // -> Response(200, [#("content-type", "text/html")], Text(body)) 268 184 /// ``` 269 - /// 270 - pub fn html_response(html: StringBuilder, status: Int) -> Response { 271 - HttpResponse(status, [#("content-type", "text/html")], Text(html)) 185 + /// 186 + pub fn html_response(html: StringTree, status: Int) -> Response { 187 + HttpResponse( 188 + status, 189 + [#("content-type", "text/html; charset=utf-8")], 190 + Text(html), 191 + ) 272 192 } 273 193 274 194 /// Create a JSON response. 275 - /// 195 + /// 276 196 /// The body is expected to be valid JSON, though this is not validated. 277 197 /// The `content-type` header will be set to `application/json`. 278 - /// 198 + /// 279 199 /// # Examples 280 - /// 200 + /// 281 201 /// ```gleam 282 - /// let body = string_builder.from_string("{\"name\": \"Joe\"}") 202 + /// let body = string_tree.from_string("{\"name\": \"Joe\"}") 283 203 /// json_response(body, 200) 284 204 /// // -> Response(200, [#("content-type", "application/json")], Text(body)) 285 205 /// ``` 286 - /// 287 - pub fn json_response(json: StringBuilder, status: Int) -> Response { 288 - HttpResponse(status, [#("content-type", "application/json")], Text(json)) 206 + /// 207 + pub fn json_response(json: StringTree, status: Int) -> Response { 208 + HttpResponse( 209 + status, 210 + [#("content-type", "application/json; charset=utf-8")], 211 + Text(json), 212 + ) 289 213 } 290 214 291 215 /// Set the body of a response to a given HTML document, and set the 292 216 /// `content-type` header to `text/html`. 293 - /// 217 + /// 294 218 /// The body is expected to be valid HTML, though this is not validated. 295 - /// 219 + /// 296 220 /// # Examples 297 - /// 221 + /// 298 222 /// ```gleam 299 - /// let body = string_builder.from_string("<h1>Hello, Joe!</h1>") 223 + /// let body = string_tree.from_string("<h1>Hello, Joe!</h1>") 300 224 /// response(201) 301 225 /// |> html_body(body) 302 - /// // -> Response(201, [#("content-type", "text/html")], Text(body)) 226 + /// // -> Response(201, [#("content-type", "text/html; charset=utf-8")], Text(body)) 303 227 /// ``` 304 - /// 305 - pub fn html_body(response: Response, html: StringBuilder) -> Response { 228 + /// 229 + pub fn html_body(response: Response, html: StringTree) -> Response { 306 230 response 307 231 |> response.set_body(Text(html)) 308 - |> response.set_header("content-type", "text/html") 232 + |> response.set_header("content-type", "text/html; charset=utf-8") 309 233 } 310 234 311 235 /// Set the body of a response to a given JSON document, and set the 312 236 /// `content-type` header to `application/json`. 313 - /// 237 + /// 314 238 /// The body is expected to be valid JSON, though this is not validated. 315 - /// 239 + /// 316 240 /// # Examples 317 - /// 241 + /// 318 242 /// ```gleam 319 - /// let body = string_builder.from_string("{\"name\": \"Joe\"}") 243 + /// let body = string_tree.from_string("{\"name\": \"Joe\"}") 320 244 /// response(201) 321 245 /// |> json_body(body) 322 - /// // -> Response(201, [#("content-type", "application/json")], Text(body)) 246 + /// // -> Response(201, [#("content-type", "application/json; charset=utf-8")], Text(body)) 323 247 /// ``` 324 - /// 325 - pub fn json_body(response: Response, json: StringBuilder) -> Response { 248 + /// 249 + pub fn json_body(response: Response, json: StringTree) -> Response { 326 250 response 327 251 |> response.set_body(Text(json)) 328 - |> response.set_header("content-type", "application/json") 252 + |> response.set_header("content-type", "application/json; charset=utf-8") 329 253 } 330 254 331 - /// Set the body of a response to a given string builder. 255 + /// Set the body of a response to a given string tree. 332 256 /// 333 257 /// You likely want to also set the request `content-type` header to an 334 258 /// appropriate value for the format of the content. 335 259 /// 336 260 /// # Examples 337 - /// 261 + /// 338 262 /// ```gleam 339 - /// let body = string_builder.from_string("Hello, Joe!") 263 + /// let body = string_tree.from_string("Hello, Joe!") 340 264 /// response(201) 341 - /// |> string_builder_body(body) 265 + /// |> string_tree_body(body) 342 266 /// // -> Response(201, [], Text(body)) 343 267 /// ``` 344 - /// 345 - pub fn string_builder_body( 346 - response: Response, 347 - content: StringBuilder, 348 - ) -> Response { 268 + /// 269 + pub fn string_tree_body(response: Response, content: StringTree) -> Response { 349 270 response 350 271 |> response.set_body(Text(content)) 351 272 } 352 273 353 - /// Set the body of a response to a given string builder. 274 + /// Set the body of a response to a given string. 354 275 /// 355 276 /// You likely want to also set the request `content-type` header to an 356 277 /// appropriate value for the format of the content. 357 278 /// 358 279 /// # Examples 359 - /// 280 + /// 360 281 /// ```gleam 361 - /// let body = 282 + /// let body = 362 283 /// response(201) 363 - /// |> string_builder_body("Hello, Joe!") 284 + /// |> string_body("Hello, Joe!") 364 285 /// // -> Response( 365 286 /// // 201, 366 287 /// // [], 367 - /// // Text(string_builder.from_string("Hello, Joe")) 288 + /// // Text(string_tree.from_string("Hello, Joe")) 368 289 /// // ) 369 290 /// ``` 370 - /// 291 + /// 371 292 pub fn string_body(response: Response, content: String) -> Response { 372 293 response 373 - |> response.set_body(Text(string_builder.from_string(content))) 294 + |> response.set_body(Text(string_tree.from_string(content))) 374 295 } 375 296 376 297 /// Escape a string so that it can be safely included in a HTML document. ··· 384 305 /// escape_html("<h1>Hello, Joe!</h1>") 385 306 /// // -> "&lt;h1&gt;Hello, Joe!&lt;/h1&gt;" 386 307 /// ``` 387 - /// 308 + /// 388 309 pub fn escape_html(content: String) -> String { 389 - do_escape_html("", content) 310 + let bits = <<content:utf8>> 311 + let acc = do_escape_html(bits, 0, bits, []) 312 + 313 + list.reverse(acc) 314 + |> bit_array.concat 315 + // We know the bit array produced by `do_escape_html` is still a valid utf8 316 + // string so we coerce it without passing through the validation steps of 317 + // `bit_array.to_string`. 318 + |> coerce_bit_array_to_string 390 319 } 391 320 392 - fn do_escape_html(escaped: String, content: String) -> String { 393 - case string.pop_grapheme(content) { 394 - Ok(#("<", xs)) -> do_escape_html(escaped <> "&lt;", xs) 395 - Ok(#(">", xs)) -> do_escape_html(escaped <> "&gt;", xs) 396 - Ok(#("&", xs)) -> do_escape_html(escaped <> "&amp;", xs) 397 - Ok(#(x, xs)) -> do_escape_html(escaped <> x, xs) 398 - Error(_) -> escaped <> content 321 + @external(erlang, "wisp_ffi", "coerce") 322 + fn coerce_bit_array_to_string(bit_array: BitArray) -> String 323 + 324 + // A possible way to escape chars would be to split the string into graphemes, 325 + // traverse those one by one and accumulate them back into a string escaping 326 + // ">", "<", etc. as we see them. 327 + // 328 + // However, we can be a lot more performant by working directly on the 329 + // `BitArray` representing a Gleam UTF-8 String. 330 + // This means that, instead of popping a grapheme at a time, we can work 331 + // directly on BitArray slices: this has the big advantage of making sure we 332 + // share as much as possible with the original string without having to build 333 + // a new one from scratch. 334 + // 335 + fn do_escape_html( 336 + bin: BitArray, 337 + skip: Int, 338 + original: BitArray, 339 + acc: List(BitArray), 340 + ) -> List(BitArray) { 341 + case bin { 342 + // If we find a char to escape we just advance the `skip` counter so that 343 + // it will be ignored in the following slice, then we append the escaped 344 + // version to the accumulator. 345 + <<"<":utf8, rest:bits>> -> { 346 + let acc = [<<"&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 + } 368 + } 369 + 370 + fn do_escape_html_regular( 371 + bin: BitArray, 372 + skip: Int, 373 + original: BitArray, 374 + acc: List(BitArray), 375 + len: Int, 376 + ) -> List(BitArray) { 377 + // Remember, if we're here it means we've found a char that doesn't need to be 378 + // escaped, so what we want to do is advance the `len` counter until we reach 379 + // a char that _does_ need to be escaped and take the slice going from 380 + // `skip` with size `len`. 381 + // 382 + // Imagine we're escaping this string: "abc<def&ghi" and we've reached 'd': 383 + // ``` 384 + // abc<def&ghi 385 + // ^ `skip` points here 386 + // ``` 387 + // We're going to be increasing `len` until we reach the '&': 388 + // ``` 389 + // abc<def&ghi 390 + // ^^^ len will be 3 when we reach the '&' that needs escaping 391 + // ``` 392 + // So we take the slice corresponding to "def". 393 + // 394 + case bin { 395 + // If we reach a char that has to be escaped we append the slice starting 396 + // from `skip` with size `len` and the escaped char. 397 + // This is what allows us to share as much of the original string as 398 + // possible: we only allocate a new BitArray for the escaped chars, 399 + // everything else is just a slice of the original String. 400 + <<"<":utf8, rest:bits>> -> { 401 + let assert Ok(slice) = bit_array.slice(original, skip, len) 402 + let acc = [<<"&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" 399 433 } 400 434 } 401 435 ··· 593 627 // 594 628 595 629 /// The connection to the client for a HTTP request. 596 - /// 630 + /// 597 631 /// The body of the request can be read from this connection using functions 598 632 /// such as `require_multipart_body`. 599 - /// 600 - pub opaque type Connection { 601 - Connection( 602 - reader: Reader, 603 - max_body_size: Int, 604 - max_files_size: Int, 605 - read_chunk_size: Int, 606 - secret_key_base: String, 607 - temporary_directory: String, 608 - ) 609 - } 610 - 611 - fn make_connection(body_reader: Reader, secret_key_base: String) -> Connection { 612 - // TODO: replace `/tmp` with appropriate for the OS 613 - let prefix = "/tmp/gleam-wisp/" 614 - let temporary_directory = join_path(prefix, random_slug()) 615 - Connection( 616 - reader: body_reader, 617 - max_body_size: 8_000_000, 618 - max_files_size: 32_000_000, 619 - read_chunk_size: 1_000_000, 620 - temporary_directory: temporary_directory, 621 - secret_key_base: secret_key_base, 622 - ) 623 - } 633 + /// 634 + pub type Connection = 635 + internal.Connection 624 636 625 637 type BufferedReader { 626 - BufferedReader(reader: Reader, buffer: BitArray) 638 + BufferedReader(reader: internal.Reader, buffer: BitArray) 627 639 } 628 640 629 641 type Quotas { ··· 645 657 } 646 658 } 647 659 648 - fn buffered_read(reader: BufferedReader, chunk_size: Int) -> Result(Read, Nil) { 660 + fn buffered_read( 661 + reader: BufferedReader, 662 + chunk_size: Int, 663 + ) -> Result(internal.Read, Nil) { 649 664 case reader.buffer { 650 665 <<>> -> reader.reader(chunk_size) 651 - _ -> Ok(Chunk(reader.buffer, reader.reader)) 666 + _ -> Ok(internal.Chunk(reader.buffer, reader.reader)) 652 667 } 653 - } 654 - 655 - type Reader = 656 - fn(Int) -> Result(Read, Nil) 657 - 658 - type Read { 659 - Chunk(BitArray, next: Reader) 660 - ReadingFinished 661 668 } 662 669 663 670 /// Set the maximum permitted size of a request body of the request in bytes. ··· 671 678 /// instead use the `max_files_size` limit. 672 679 /// 673 680 pub fn set_max_body_size(request: Request, size: Int) -> Request { 674 - Connection(..request.body, max_body_size: size) 681 + internal.Connection(..request.body, max_body_size: size) 675 682 |> request.set_body(request, _) 676 683 } 677 684 678 685 /// Get the maximum permitted size of a request body of the request in bytes. 679 - /// 686 + /// 680 687 pub fn get_max_body_size(request: Request) -> Int { 681 688 request.body.max_body_size 682 689 } 683 690 684 691 /// Set the secret key base used to sign cookies and other sensitive data. 685 - /// 692 + /// 686 693 /// This key must be at least 64 bytes long and should be kept secret. Anyone 687 694 /// with this secret will be able to manipulate signed cookies and other sensitive 688 695 /// data. 689 696 /// 690 697 /// # Panics 691 - /// 698 + /// 692 699 /// This function will panic if the key is less than 64 bytes long. 693 700 /// 694 701 pub fn set_secret_key_base(request: Request, key: String) -> Request { 695 702 case string.byte_size(key) < 64 { 696 703 True -> panic as "Secret key base must be at least 64 bytes long" 697 704 False -> 698 - Connection(..request.body, secret_key_base: key) 705 + internal.Connection(..request.body, secret_key_base: key) 699 706 |> request.set_body(request, _) 700 707 } 701 708 } 702 709 703 710 /// Get the secret key base used to sign cookies and other sensitive data. 704 - /// 711 + /// 705 712 pub fn get_secret_key_base(request: Request) -> String { 706 713 request.body.secret_key_base 707 714 } ··· 714 721 /// 715 722 /// This limit only applies for files in a multipart body that get streamed to 716 723 /// disc. For headers and other content that gets read into memory use the 717 - /// `max_files_size` limit. 724 + /// `max_body_size` limit. 718 725 /// 719 726 pub fn set_max_files_size(request: Request, size: Int) -> Request { 720 - Connection(..request.body, max_files_size: size) 727 + internal.Connection(..request.body, max_files_size: size) 721 728 |> request.set_body(request, _) 722 729 } 723 730 724 731 /// Get the maximum permitted total size of a files uploaded by a request in 725 732 /// bytes. 726 - /// 733 + /// 727 734 pub fn get_max_files_size(request: Request) -> Int { 728 735 request.body.max_files_size 729 736 } ··· 737 744 /// been received from the client. 738 745 /// 739 746 pub fn set_read_chunk_size(request: Request, size: Int) -> Request { 740 - Connection(..request.body, read_chunk_size: size) 747 + internal.Connection(..request.body, read_chunk_size: size) 741 748 |> request.set_body(request, _) 742 749 } 743 750 744 751 /// Get the size limit for each chunk of the request body when read from the 745 752 /// client. 746 - /// 753 + /// 747 754 pub fn get_read_chunk_size(request: Request) -> Int { 748 755 request.body.read_chunk_size 749 756 } 750 757 751 758 /// A convenient alias for a HTTP request with a Wisp connection as the body. 752 - /// 759 + /// 753 760 pub type Request = 754 - HttpRequest(Connection) 761 + HttpRequest(internal.Connection) 755 762 756 763 /// This middleware function ensures that the request has a specific HTTP 757 764 /// method, returning an empty response with status code 405: Method not allowed 758 765 /// if the method is not correct. 759 766 /// 760 767 /// # Examples 761 - /// 768 + /// 762 769 /// ```gleam 763 770 /// fn handle_request(request: Request) -> Response { 764 771 /// use <- wisp.require_method(request, http.Patch) ··· 779 786 780 787 // TODO: re-export once Gleam has a syntax for that 781 788 /// Return the non-empty segments of a request path. 782 - /// 789 + /// 783 790 /// # Examples 784 791 /// 785 792 /// ```gleam ··· 793 800 794 801 // TODO: re-export once Gleam has a syntax for that 795 802 /// Set a given header to a given value, replacing any existing value. 796 - /// 803 + /// 797 804 /// # Examples 798 805 /// 799 806 /// ```gleam ··· 863 870 /// return an incorrect value, depending on the underlying web server. It is the 864 871 /// responsibility of the caller to cache the body if it is needed multiple 865 872 /// times. 866 - /// 873 + /// 867 874 /// If the body is larger than the `max_body_size` limit then an empty response 868 875 /// with status code 413: Entity too large will be returned to the client. 869 - /// 876 + /// 870 877 /// If the body is found not to be valid UTF-8 then an empty response with 871 878 /// status code 400: Bad request will be returned to the client. 872 - /// 879 + /// 873 880 /// # Examples 874 881 /// 875 882 /// ```gleam ··· 899 906 /// return an incorrect value, depending on the underlying web server. It is the 900 907 /// responsibility of the caller to cache the body if it is needed multiple 901 908 /// times. 902 - /// 909 + /// 903 910 /// If the body is larger than the `max_body_size` limit then an empty response 904 911 /// with status code 413: Entity too large will be returned to the client. 905 - /// 912 + /// 906 913 /// # Examples 907 914 /// 908 915 /// ```gleam ··· 925 932 // TODO: don't always return entity to large. Other errors are possible, such as 926 933 // network errors. 927 934 /// Read the entire body of the request as a bit string. 928 - /// 935 + /// 929 936 /// You may instead wish to use the `require_bit_array_body` or the 930 937 /// `require_string_body` middleware functions instead. 931 - /// 938 + /// 932 939 /// This function does not cache the body in any way, so if you call this 933 940 /// function (or any other body reading function) more than once it may hang or 934 941 /// return an incorrect value, depending on the underlying web server. It is the 935 942 /// responsibility of the caller to cache the body if it is needed multiple 936 943 /// times. 937 - /// 944 + /// 938 945 /// If the body is larger than the `max_body_size` limit then an empty response 939 946 /// with status code 413: Entity too large will be returned to the client. 940 - /// 947 + /// 941 948 pub fn read_body_to_bitstring(request: Request) -> Result(BitArray, Nil) { 942 949 let connection = request.body 943 950 read_body_loop( ··· 949 956 } 950 957 951 958 fn read_body_loop( 952 - reader: Reader, 959 + reader: internal.Reader, 953 960 read_chunk_size: Int, 954 961 max_body_size: Int, 955 962 accumulator: BitArray, 956 963 ) -> Result(BitArray, Nil) { 957 964 use chunk <- result.try(reader(read_chunk_size)) 958 965 case chunk { 959 - ReadingFinished -> Ok(accumulator) 960 - Chunk(chunk, next) -> { 966 + internal.ReadingFinished -> Ok(accumulator) 967 + internal.Chunk(chunk, next) -> { 961 968 let accumulator = bit_array.append(accumulator, chunk) 962 969 case bit_array.byte_size(accumulator) > max_body_size { 963 970 True -> Error(Nil) ··· 971 978 /// A middleware which extracts form data from the body of a request that is 972 979 /// encoded as either `application/x-www-form-urlencoded` or 973 980 /// `multipart/form-data`. 974 - /// 981 + /// 975 982 /// Extracted fields are sorted into alphabetical order by key, so if you wish 976 983 /// to use pattern matching the order can be relied upon. 977 - /// 984 + /// 978 985 /// ```gleam 979 986 /// fn handle_request(request: Request) -> Response { 980 987 /// use form <- wisp.require_form(request) ··· 1003 1010 /// 1004 1011 /// If the body cannot be parsed successfully then an empty response with status 1005 1012 /// code 400: Bad request will be returned to the client. 1006 - /// 1013 + /// 1007 1014 pub fn require_form( 1008 1015 request: Request, 1009 1016 next: fn(FormData) -> Response, ··· 1030 1037 /// Unsupported media type if the header is not the expected value 1031 1038 /// 1032 1039 /// # Examples 1033 - /// 1040 + /// 1034 1041 /// ```gleam 1035 1042 /// fn handle_request(request: Request) -> Response { 1036 1043 /// use <- wisp.require_content_type(request, "application/json") ··· 1044 1051 next: fn() -> Response, 1045 1052 ) -> Response { 1046 1053 case list.key_find(request.headers, "content-type") { 1047 - Ok(content_type) if content_type == expected -> next() 1054 + Ok(content_type) -> 1055 + // This header may have further such as `; charset=utf-8`, so discard 1056 + // that if it exists. 1057 + case string.split_once(content_type, ";") { 1058 + Ok(#(content_type, _)) if content_type == expected -> next() 1059 + _ if content_type == expected -> next() 1060 + _ -> unsupported_media_type([expected]) 1061 + } 1062 + 1048 1063 _ -> unsupported_media_type([expected]) 1049 1064 } 1050 1065 } 1051 1066 1052 1067 /// A middleware which extracts JSON from the body of a request. 1053 - /// 1068 + /// 1054 1069 /// ```gleam 1055 1070 /// fn handle_request(request: Request) -> Response { 1056 1071 /// use json <- wisp.require_json(request) ··· 1071 1086 /// 1072 1087 /// If the body cannot be parsed successfully then an empty response with status 1073 1088 /// code 400: Bad request will be returned to the client. 1074 - /// 1089 + /// 1075 1090 pub fn require_json(request: Request, next: fn(Dynamic) -> Response) -> Response { 1076 1091 use <- require_content_type(request, "application/json") 1077 1092 use body <- require_string_body(request) ··· 1255 1270 fn read_chunk( 1256 1271 reader: BufferedReader, 1257 1272 chunk_size: Int, 1258 - ) -> Result(#(BitArray, Reader), Response) { 1273 + ) -> Result(#(BitArray, internal.Reader), Response) { 1259 1274 buffered_read(reader, chunk_size) 1260 1275 |> result.replace_error(bad_request()) 1261 1276 |> result.try(fn(chunk) { 1262 1277 case chunk { 1263 - Chunk(chunk, next) -> Ok(#(chunk, next)) 1264 - ReadingFinished -> Error(bad_request()) 1278 + internal.Chunk(chunk, next) -> Ok(#(chunk, next)) 1279 + internal.ReadingFinished -> Error(bad_request()) 1265 1280 } 1266 1281 }) 1267 1282 } ··· 1305 1320 } 1306 1321 1307 1322 /// Data parsed from form sent in a request's body. 1308 - /// 1323 + /// 1309 1324 pub type FormData { 1310 1325 FormData( 1311 1326 /// String values of the form's fields. ··· 1405 1420 response 1406 1421 } 1407 1422 1408 - fn remove_preceeding_slashes(string: String) -> String { 1409 - case string { 1410 - "/" <> rest -> remove_preceeding_slashes(rest) 1411 - _ -> string 1412 - } 1413 - } 1414 - 1415 - // TODO: replace with simplifile function when it exists 1416 - fn join_path(a: String, b: String) -> String { 1417 - let b = remove_preceeding_slashes(b) 1418 - case string.ends_with(a, "/") { 1419 - True -> a <> b 1420 - False -> a <> "/" <> b 1421 - } 1422 - } 1423 - 1424 1423 /// A middleware function that serves files from a directory, along with a 1425 1424 /// suitable `content-type` header for known file extensions. 1426 1425 /// ··· 1429 1428 /// 1430 1429 /// The `under` parameter is the request path prefix that must match for the 1431 1430 /// file to be served. 1432 - /// 1431 + /// 1433 1432 /// | `under` | `from` | `request.path` | `file` | 1434 1433 /// |-----------|---------|--------------------|-------------------------| 1435 1434 /// | `/static` | `/data` | `/static/file.txt` | `/data/file.txt` | ··· 1468 1467 from directory: String, 1469 1468 next handler: fn() -> Response, 1470 1469 ) -> Response { 1471 - let path = remove_preceeding_slashes(req.path) 1472 - let prefix = remove_preceeding_slashes(prefix) 1470 + let path = internal.remove_preceeding_slashes(req.path) 1471 + let prefix = internal.remove_preceeding_slashes(prefix) 1473 1472 case req.method, string.starts_with(path, prefix) { 1474 1473 http.Get, True -> { 1475 1474 let path = 1476 1475 path 1477 - |> string.drop_left(string.length(prefix)) 1476 + |> string.drop_start(string.length(prefix)) 1478 1477 |> string.replace(each: "..", with: "") 1479 - |> join_path(directory, _) 1478 + |> internal.join_path(directory, _) 1480 1479 1481 1480 let mime_type = 1482 1481 req.path ··· 1485 1484 |> result.unwrap("") 1486 1485 |> marceau.extension_to_mime_type 1487 1486 1488 - case simplifile.verify_is_file(path) { 1487 + let content_type = case mime_type { 1488 + "application/json" | "text/" <> _ -> mime_type <> "; charset=utf-8" 1489 + _ -> mime_type 1490 + } 1491 + 1492 + case simplifile.is_file(path) { 1489 1493 Ok(True) -> 1490 1494 response.new(200) 1491 - |> response.set_header("content-type", mime_type) 1495 + |> response.set_header("content-type", content_type) 1492 1496 |> response.set_body(File(path)) 1493 1497 _ -> handler() 1494 1498 } ··· 1535 1539 1536 1540 /// Create a new temporary directory for the given request. 1537 1541 /// 1538 - /// If you are using the `mist_handler` function or another compliant web server 1542 + /// If you are using the Mist adapter or another compliant web server 1539 1543 /// adapter then this file will be deleted for you when the request is complete. 1540 1544 /// Otherwise you will need to call the `delete_temporary_files` function 1541 1545 /// yourself. ··· 1545 1549 ) -> Result(String, simplifile.FileError) { 1546 1550 let directory = request.body.temporary_directory 1547 1551 use _ <- result.try(simplifile.create_directory_all(directory)) 1548 - let path = join_path(directory, random_slug()) 1552 + let path = internal.join_path(directory, internal.random_slug()) 1549 1553 use _ <- result.map(simplifile.create_file(path)) 1550 1554 path 1551 1555 } 1552 1556 1553 1557 /// Delete any temporary files created for the given request. 1554 1558 /// 1555 - /// If you are using the `mist_handler` function or another compliant web server 1559 + /// If you are using the Mist adapter or another compliant web server 1556 1560 /// adapter then this file will be deleted for you when the request is complete. 1557 1561 /// Otherwise you will need to call this function yourself. 1558 1562 /// ··· 1576 1580 /// > erlang.priv_directory("my_app") 1577 1581 /// // -> Ok("/some/location/my_app/priv") 1578 1582 /// ``` 1579 - /// 1583 + /// 1580 1584 pub const priv_directory = erlang.priv_directory 1581 1585 1582 1586 // ··· 1585 1589 1586 1590 /// Configure the Erlang logger, setting the minimum log level to `info`, to be 1587 1591 /// called when your application starts. 1588 - /// 1592 + /// 1589 1593 /// You may wish to use an alternative for this such as one provided by a more 1590 1594 /// sophisticated logging library. 1591 - /// 1595 + /// 1592 1596 /// In future this function may be extended to change the output format. 1593 - /// 1597 + /// 1594 1598 pub fn configure_logger() -> Nil { 1595 1599 logging.configure() 1596 1600 } 1597 1601 1602 + /// Type to set the log level of the Erlang's logger 1603 + /// 1604 + /// See the [Erlang logger documentation][1] for more information. 1605 + /// 1606 + /// [1]: https://www.erlang.org/doc/man/logger 1607 + /// 1608 + pub type LogLevel { 1609 + EmergencyLevel 1610 + AlertLevel 1611 + CriticalLevel 1612 + ErrorLevel 1613 + WarningLevel 1614 + NoticeLevel 1615 + InfoLevel 1616 + DebugLevel 1617 + } 1618 + 1619 + fn log_level_to_logging_log_level(log_level: LogLevel) -> logging.LogLevel { 1620 + case log_level { 1621 + EmergencyLevel -> logging.Emergency 1622 + AlertLevel -> logging.Alert 1623 + CriticalLevel -> logging.Critical 1624 + ErrorLevel -> logging.Error 1625 + WarningLevel -> logging.Warning 1626 + NoticeLevel -> logging.Notice 1627 + InfoLevel -> logging.Info 1628 + DebugLevel -> logging.Debug 1629 + } 1630 + } 1631 + 1632 + /// Set the log level of the Erlang logger to `log_level`. 1633 + /// 1634 + /// See the [Erlang logger documentation][1] for more information. 1635 + /// 1636 + /// [1]: https://www.erlang.org/doc/man/logger 1637 + /// 1638 + pub fn set_logger_level(log_level: LogLevel) -> Nil { 1639 + logging.set_level(log_level_to_logging_log_level(log_level)) 1640 + } 1641 + 1598 1642 /// Log a message to the Erlang logger with the level of `emergency`. 1599 - /// 1643 + /// 1600 1644 /// See the [Erlang logger documentation][1] for more information. 1601 - /// 1645 + /// 1602 1646 /// [1]: https://www.erlang.org/doc/man/logger 1603 - /// 1647 + /// 1604 1648 pub fn log_emergency(message: String) -> Nil { 1605 1649 logging.log(logging.Emergency, message) 1606 1650 } 1607 1651 1608 1652 /// Log a message to the Erlang logger with the level of `alert`. 1609 - /// 1653 + /// 1610 1654 /// See the [Erlang logger documentation][1] for more information. 1611 - /// 1655 + /// 1612 1656 /// [1]: https://www.erlang.org/doc/man/logger 1613 - /// 1657 + /// 1614 1658 pub fn log_alert(message: String) -> Nil { 1615 1659 logging.log(logging.Alert, message) 1616 1660 } 1617 1661 1618 1662 /// Log a message to the Erlang logger with the level of `critical`. 1619 - /// 1663 + /// 1620 1664 /// See the [Erlang logger documentation][1] for more information. 1621 - /// 1665 + /// 1622 1666 /// [1]: https://www.erlang.org/doc/man/logger 1623 - /// 1667 + /// 1624 1668 pub fn log_critical(message: String) -> Nil { 1625 1669 logging.log(logging.Critical, message) 1626 1670 } 1627 1671 1628 1672 /// Log a message to the Erlang logger with the level of `error`. 1629 - /// 1673 + /// 1630 1674 /// See the [Erlang logger documentation][1] for more information. 1631 - /// 1675 + /// 1632 1676 /// [1]: https://www.erlang.org/doc/man/logger 1633 - /// 1677 + /// 1634 1678 pub fn log_error(message: String) -> Nil { 1635 1679 logging.log(logging.Error, message) 1636 1680 } 1637 1681 1638 1682 /// Log a message to the Erlang logger with the level of `warning`. 1639 - /// 1683 + /// 1640 1684 /// See the [Erlang logger documentation][1] for more information. 1641 - /// 1685 + /// 1642 1686 /// [1]: https://www.erlang.org/doc/man/logger 1643 - /// 1687 + /// 1644 1688 pub fn log_warning(message: String) -> Nil { 1645 1689 logging.log(logging.Warning, message) 1646 1690 } 1647 1691 1648 1692 /// Log a message to the Erlang logger with the level of `notice`. 1649 - /// 1693 + /// 1650 1694 /// See the [Erlang logger documentation][1] for more information. 1651 - /// 1695 + /// 1652 1696 /// [1]: https://www.erlang.org/doc/man/logger 1653 - /// 1697 + /// 1654 1698 pub fn log_notice(message: String) -> Nil { 1655 1699 logging.log(logging.Notice, message) 1656 1700 } 1657 1701 1658 1702 /// Log a message to the Erlang logger with the level of `info`. 1659 - /// 1703 + /// 1660 1704 /// See the [Erlang logger documentation][1] for more information. 1661 - /// 1705 + /// 1662 1706 /// [1]: https://www.erlang.org/doc/man/logger 1663 - /// 1707 + /// 1664 1708 pub fn log_info(message: String) -> Nil { 1665 1709 logging.log(logging.Info, message) 1666 1710 } 1667 1711 1668 1712 /// Log a message to the Erlang logger with the level of `debug`. 1669 - /// 1713 + /// 1670 1714 /// See the [Erlang logger documentation][1] for more information. 1671 - /// 1715 + /// 1672 1716 /// [1]: https://www.erlang.org/doc/man/logger 1673 - /// 1717 + /// 1674 1718 pub fn log_debug(message: String) -> Nil { 1675 1719 logging.log(logging.Debug, message) 1676 1720 } ··· 1682 1726 /// Generate a random string of the given length. 1683 1727 /// 1684 1728 pub fn random_string(length: Int) -> String { 1685 - crypto.strong_random_bytes(length) 1686 - |> bit_array.base64_url_encode(False) 1687 - |> string.slice(0, length) 1729 + internal.random_string(length) 1688 1730 } 1689 1731 1690 1732 /// Sign a message which can later be verified using the `verify_signed_message` 1691 1733 /// function to detect if the message has been tampered with. 1692 - /// 1734 + /// 1693 1735 /// Signed messages are not encrypted and can be read by anyone. They are not 1694 1736 /// suitable for storing sensitive information. 1695 - /// 1737 + /// 1696 1738 /// This function uses the secret key base from the request. If the secret 1697 1739 /// changes then the signature will no longer be verifiable. 1698 - /// 1740 + /// 1699 1741 pub fn sign_message( 1700 1742 request: Request, 1701 1743 message: BitArray, ··· 1705 1747 } 1706 1748 1707 1749 /// Verify a signed message which was signed using the `sign_message` function. 1708 - /// 1750 + /// 1709 1751 /// Returns the content of the message if the signature is valid, otherwise 1710 1752 /// returns an error. 1711 - /// 1753 + /// 1712 1754 /// This function uses the secret key base from the request. If the secret 1713 1755 /// changes then the signature will no longer be verifiable. 1714 - /// 1756 + /// 1715 1757 pub fn verify_signed_message( 1716 1758 request: Request, 1717 1759 message: String, 1718 1760 ) -> Result(BitArray, Nil) { 1719 1761 crypto.verify_signed_message(message, <<request.body.secret_key_base:utf8>>) 1720 - } 1721 - 1722 - fn random_slug() -> String { 1723 - random_string(16) 1724 1762 } 1725 1763 1726 1764 // ··· 1747 1785 /// 1748 1786 /// ```gleam 1749 1787 /// wisp.ok() 1750 - /// |> wisp.set_cookie("id", "123", wisp.PlainText, 60 * 60) 1788 + /// |> wisp.set_cookie(request, "id", "123", wisp.PlainText, 60 * 60) 1751 1789 /// ``` 1752 - /// 1790 + /// 1753 1791 /// Setting a signed cookie that the client can read but not modify: 1754 - /// 1792 + /// 1755 1793 /// ```gleam 1756 1794 /// wisp.ok() 1757 - /// |> wisp.set_cookie("id", value, wisp.Signed, 60 * 60) 1795 + /// |> wisp.set_cookie(request, "id", value, wisp.Signed, 60 * 60) 1758 1796 /// ``` 1759 1797 /// 1760 1798 pub fn set_cookie( ··· 1794 1832 /// for a signed cookie, then `Error(Nil)` is returned. 1795 1833 /// 1796 1834 /// ```gleam 1797 - /// wisp.get_cookie(request, "group") 1835 + /// wisp.get_cookie(request, "group", wisp.PlainText) 1798 1836 /// // -> Ok("A") 1799 1837 /// ``` 1800 1838 /// ··· 1821 1859 1822 1860 // TODO: chunk the body 1823 1861 /// Create a connection which will return the given body when read. 1824 - /// 1862 + /// 1825 1863 /// This function is intended for use in tests, though you probably want the 1826 1864 /// `wisp/testing` module instead. 1827 - /// 1865 + /// 1828 1866 pub fn create_canned_connection( 1829 1867 body: BitArray, 1830 1868 secret_key_base: String, 1831 - ) -> Connection { 1832 - make_connection( 1833 - fn(_size) { Ok(Chunk(body, fn(_size) { Ok(ReadingFinished) })) }, 1869 + ) -> internal.Connection { 1870 + internal.make_connection( 1871 + fn(_size) { 1872 + Ok(internal.Chunk(body, fn(_size) { Ok(internal.ReadingFinished) })) 1873 + }, 1834 1874 secret_key_base, 1835 1875 ) 1836 1876 }
+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 }
+55 -24
test/wisp_test.gleam
··· 1 + import exception 1 2 import gleam/bit_array 2 3 import gleam/crypto 4 + import gleam/dict 3 5 import gleam/dynamic.{type Dynamic} 4 6 import gleam/erlang 5 7 import gleam/http ··· 7 9 import gleam/http/response.{Response} 8 10 import gleam/int 9 11 import gleam/list 10 - import gleam/dict 11 12 import gleam/set 12 13 import gleam/string 13 - import gleam/string_builder 14 + import gleam/string_tree 14 15 import gleeunit 15 16 import gleeunit/should 16 17 import simplifile ··· 118 119 } 119 120 120 121 pub fn json_response_test() { 121 - let body = string_builder.from_string("{\"one\":1,\"two\":2}") 122 + let body = string_tree.from_string("{\"one\":1,\"two\":2}") 122 123 let response = wisp.json_response(body, 201) 123 124 response.status 124 125 |> should.equal(201) 125 126 response.headers 126 - |> should.equal([#("content-type", "application/json")]) 127 + |> should.equal([#("content-type", "application/json; charset=utf-8")]) 127 128 response 128 129 |> testing.string_body 129 130 |> should.equal("{\"one\":1,\"two\":2}") 130 131 } 131 132 132 133 pub fn html_response_test() { 133 - let body = string_builder.from_string("Hello, world!") 134 + let body = string_tree.from_string("Hello, world!") 134 135 let response = wisp.html_response(body, 200) 135 136 response.status 136 137 |> should.equal(200) 137 138 response.headers 138 - |> should.equal([#("content-type", "text/html")]) 139 + |> should.equal([#("content-type", "text/html; charset=utf-8")]) 139 140 response 140 141 |> testing.string_body 141 142 |> should.equal("Hello, world!") 142 143 } 143 144 144 145 pub fn html_body_test() { 145 - let body = string_builder.from_string("Hello, world!") 146 + let body = string_tree.from_string("Hello, world!") 146 147 let response = 147 148 wisp.method_not_allowed([http.Get]) 148 149 |> wisp.html_body(body) 149 150 response.status 150 151 |> should.equal(405) 151 152 response.headers 152 - |> should.equal([#("allow", "GET"), #("content-type", "text/html")]) 153 + |> should.equal([ 154 + #("allow", "GET"), 155 + #("content-type", "text/html; charset=utf-8"), 156 + ]) 153 157 response 154 158 |> testing.string_body 155 159 |> should.equal("Hello, world!") ··· 330 334 } 331 335 332 336 pub fn rescue_crashes_error_test() { 333 - // TODO: Determine how to silence the logger for this test. 337 + wisp.set_logger_level(wisp.CriticalLevel) 338 + use <- exception.defer(fn() { wisp.set_logger_level(wisp.InfoLevel) }) 339 + 334 340 { 335 341 use <- wisp.rescue_crashes 336 342 panic as "we need to crash to test the middleware" ··· 359 365 response.status 360 366 |> should.equal(200) 361 367 response.headers 362 - |> should.equal([#("content-type", "text/plain")]) 368 + |> should.equal([#("content-type", "text/plain; charset=utf-8")]) 363 369 response.body 364 370 |> should.equal(wisp.File("./test/fixture.txt")) 365 371 ··· 370 376 response.status 371 377 |> should.equal(200) 372 378 response.headers 373 - |> should.equal([#("content-type", "application/json")]) 379 + |> should.equal([#("content-type", "application/json; charset=utf-8")]) 374 380 response.body 375 381 |> should.equal(wisp.File("./test/fixture.json")) 376 382 383 + // Get some other file 384 + let response = 385 + testing.get("/stuff/test/fixture.dat", []) 386 + |> handler 387 + response.status 388 + |> should.equal(200) 389 + response.headers 390 + |> should.equal([#("content-type", "application/octet-stream")]) 391 + response.body 392 + |> should.equal(wisp.File("./test/fixture.dat")) 393 + 377 394 // Get something not handled by the static file server 378 395 let response = 379 396 testing.get("/stuff/this-does-not-exist", []) ··· 397 414 response.status 398 415 |> should.equal(200) 399 416 response.headers 400 - |> should.equal([#("content-type", "text/plain")]) 417 + |> should.equal([#("content-type", "text/plain; charset=utf-8")]) 401 418 response.body 402 419 |> should.equal(wisp.File("./test/fixture.txt")) 403 420 } ··· 413 430 response.status 414 431 |> should.equal(200) 415 432 response.headers 416 - |> should.equal([#("content-type", "text/plain")]) 433 + |> should.equal([#("content-type", "text/plain; charset=utf-8")]) 417 434 response.body 418 435 |> should.equal(wisp.File("./test/fixture.txt")) 419 436 } ··· 485 502 pub fn require_content_type_test() { 486 503 { 487 504 let request = testing.get("/", [#("content-type", "text/plain")]) 505 + use <- wisp.require_content_type(request, "text/plain") 506 + wisp.ok() 507 + } 508 + |> should.equal(wisp.ok()) 509 + } 510 + 511 + pub fn require_content_type_charset_test() { 512 + { 513 + let request = 514 + testing.get("/", [#("content-type", "text/plain; charset=utf-8")]) 488 515 use <- wisp.require_content_type(request, "text/plain") 489 516 wisp.ok() 490 517 } ··· 724 751 list.key_find(request.headers, "x-original-method") 725 752 |> should.equal(header) 726 753 727 - string_builder.from_string("Hello!") 754 + string_tree.from_string("Hello!") 728 755 |> wisp.html_response(201) 729 756 } 730 757 ··· 733 760 |> handler(Error(Nil)) 734 761 |> should.equal(Response( 735 762 201, 736 - [#("content-type", "text/html")], 737 - wisp.Text(string_builder.from_string("Hello!")), 763 + [#("content-type", "text/html; charset=utf-8")], 764 + wisp.Text(string_tree.from_string("Hello!")), 738 765 )) 739 766 740 767 testing.get("/", []) 741 768 |> request.set_method(http.Head) 742 769 |> handler(Ok("HEAD")) 743 - |> should.equal(Response(201, [#("content-type", "text/html")], wisp.Empty)) 770 + |> should.equal(Response( 771 + 201, 772 + [#("content-type", "text/html; charset=utf-8")], 773 + wisp.Empty, 774 + )) 744 775 745 776 testing.get("/", []) 746 777 |> request.set_method(http.Post) ··· 866 897 |> should.equal(Response( 867 898 200, 868 899 [], 869 - wisp.Text(string_builder.from_string("Hello, world!")), 900 + wisp.Text(string_tree.from_string("Hello, world!")), 870 901 )) 871 902 } 872 903 873 - pub fn string_builder_body_test() { 904 + pub fn string_tree_body_test() { 874 905 wisp.ok() 875 - |> wisp.string_builder_body(string_builder.from_string("Hello, world!")) 906 + |> wisp.string_tree_body(string_tree.from_string("Hello, world!")) 876 907 |> should.equal(Response( 877 908 200, 878 909 [], 879 - wisp.Text(string_builder.from_string("Hello, world!")), 910 + wisp.Text(string_tree.from_string("Hello, world!")), 880 911 )) 881 912 } 882 913 883 914 pub fn json_body_test() { 884 915 wisp.ok() 885 - |> wisp.json_body(string_builder.from_string("{\"one\":1,\"two\":2}")) 916 + |> wisp.json_body(string_tree.from_string("{\"one\":1,\"two\":2}")) 886 917 |> should.equal(Response( 887 918 200, 888 - [#("content-type", "application/json")], 889 - wisp.Text(string_builder.from_string("{\"one\":1,\"two\":2}")), 919 + [#("content-type", "application/json; charset=utf-8")], 920 + wisp.Text(string_tree.from_string("{\"one\":1,\"two\":2}")), 890 921 )) 891 922 } 892 923