🧚 A practical web framework for Gleam

Optimise html_escape

authored by giacomocavalieri.me and committed by Louis Pilfold 618e2332 754bd38e

Changed files
+224 -105
src
+220 -105
src/wisp.gleam
··· 152 152 HttpResponse(Body) 153 153 154 154 /// Create an empty response with the given status code. 155 - /// 155 + /// 156 156 /// # Examples 157 - /// 157 + /// 158 158 /// ```gleam 159 159 /// response(200) 160 160 /// // -> Response(200, [], Empty) 161 161 /// ``` 162 - /// 162 + /// 163 163 pub fn response(status: Int) -> Response { 164 164 HttpResponse(status, [], Empty) 165 165 } 166 166 167 167 /// Set the body of a response. 168 - /// 168 + /// 169 169 /// # Examples 170 - /// 170 + /// 171 171 /// ```gleam 172 172 /// response(200) 173 173 /// |> set_body(File("/tmp/myfile.txt")) 174 174 /// // -> Response(200, [], File("/tmp/myfile.txt")) 175 175 /// ``` 176 - /// 176 + /// 177 177 pub fn set_body(response: Response, body: Body) -> Response { 178 178 response 179 179 |> response.set_body(body) ··· 193 193 /// `set_body` function with the `File` body variant. 194 194 /// 195 195 /// # Examples 196 - /// 196 + /// 197 197 /// ```gleam 198 198 /// response(200) 199 199 /// |> file_download(named: "myfile.txt", from: "/tmp/myfile.txt") ··· 229 229 /// as this can result in cross-site scripting vulnerabilities. 230 230 /// 231 231 /// # Examples 232 - /// 232 + /// 233 233 /// ```gleam 234 234 /// response(200) 235 235 /// |> file_download_from_memory(named: "myfile.txt", containing: "Hello, Joe!") ··· 255 255 } 256 256 257 257 /// Create a HTML response. 258 - /// 258 + /// 259 259 /// The body is expected to be valid HTML, though this is not validated. 260 260 /// The `content-type` header will be set to `text/html`. 261 - /// 261 + /// 262 262 /// # Examples 263 - /// 263 + /// 264 264 /// ```gleam 265 265 /// let body = string_builder.from_string("<h1>Hello, Joe!</h1>") 266 266 /// html_response(body, 200) 267 267 /// // -> Response(200, [#("content-type", "text/html")], Text(body)) 268 268 /// ``` 269 - /// 269 + /// 270 270 pub fn html_response(html: StringBuilder, status: Int) -> Response { 271 271 HttpResponse(status, [#("content-type", "text/html")], Text(html)) 272 272 } 273 273 274 274 /// Create a JSON response. 275 - /// 275 + /// 276 276 /// The body is expected to be valid JSON, though this is not validated. 277 277 /// The `content-type` header will be set to `application/json`. 278 - /// 278 + /// 279 279 /// # Examples 280 - /// 280 + /// 281 281 /// ```gleam 282 282 /// let body = string_builder.from_string("{\"name\": \"Joe\"}") 283 283 /// json_response(body, 200) 284 284 /// // -> Response(200, [#("content-type", "application/json")], Text(body)) 285 285 /// ``` 286 - /// 286 + /// 287 287 pub fn json_response(json: StringBuilder, status: Int) -> Response { 288 288 HttpResponse(status, [#("content-type", "application/json")], Text(json)) 289 289 } 290 290 291 291 /// Set the body of a response to a given HTML document, and set the 292 292 /// `content-type` header to `text/html`. 293 - /// 293 + /// 294 294 /// The body is expected to be valid HTML, though this is not validated. 295 - /// 295 + /// 296 296 /// # Examples 297 - /// 297 + /// 298 298 /// ```gleam 299 299 /// let body = string_builder.from_string("<h1>Hello, Joe!</h1>") 300 300 /// response(201) 301 301 /// |> html_body(body) 302 302 /// // -> Response(201, [#("content-type", "text/html")], Text(body)) 303 303 /// ``` 304 - /// 304 + /// 305 305 pub fn html_body(response: Response, html: StringBuilder) -> Response { 306 306 response 307 307 |> response.set_body(Text(html)) ··· 310 310 311 311 /// Set the body of a response to a given JSON document, and set the 312 312 /// `content-type` header to `application/json`. 313 - /// 313 + /// 314 314 /// The body is expected to be valid JSON, though this is not validated. 315 - /// 315 + /// 316 316 /// # Examples 317 - /// 317 + /// 318 318 /// ```gleam 319 319 /// let body = string_builder.from_string("{\"name\": \"Joe\"}") 320 320 /// response(201) 321 321 /// |> json_body(body) 322 322 /// // -> Response(201, [#("content-type", "application/json")], Text(body)) 323 323 /// ``` 324 - /// 324 + /// 325 325 pub fn json_body(response: Response, json: StringBuilder) -> Response { 326 326 response 327 327 |> response.set_body(Text(json)) ··· 334 334 /// appropriate value for the format of the content. 335 335 /// 336 336 /// # Examples 337 - /// 337 + /// 338 338 /// ```gleam 339 339 /// let body = string_builder.from_string("Hello, Joe!") 340 340 /// response(201) 341 341 /// |> string_builder_body(body) 342 342 /// // -> Response(201, [], Text(body)) 343 343 /// ``` 344 - /// 344 + /// 345 345 pub fn string_builder_body( 346 346 response: Response, 347 347 content: StringBuilder, ··· 356 356 /// appropriate value for the format of the content. 357 357 /// 358 358 /// # Examples 359 - /// 359 + /// 360 360 /// ```gleam 361 - /// let body = 361 + /// let body = 362 362 /// response(201) 363 363 /// |> string_body("Hello, Joe!") 364 364 /// // -> Response( ··· 367 367 /// // Text(string_builder.from_string("Hello, Joe")) 368 368 /// // ) 369 369 /// ``` 370 - /// 370 + /// 371 371 pub fn string_body(response: Response, content: String) -> Response { 372 372 response 373 373 |> response.set_body(Text(string_builder.from_string(content))) ··· 384 384 /// escape_html("<h1>Hello, Joe!</h1>") 385 385 /// // -> "&lt;h1&gt;Hello, Joe!&lt;/h1&gt;" 386 386 /// ``` 387 - /// 387 + /// 388 388 pub fn escape_html(content: String) -> String { 389 - do_escape_html("", content) 389 + let bits = <<content:utf8>> 390 + let acc = do_escape_html(bits, 0, bits, []) 391 + 392 + list.reverse(acc) 393 + |> bit_array.concat 394 + // We know the bit array produced by `do_escape_html` is still a valid utf8 395 + // string so we coerce it without passing through the validation steps of 396 + // `bit_array.to_string`. 397 + |> coerce_bit_array_to_string 398 + } 399 + 400 + @external(erlang, "wisp_ffi", "coerce") 401 + fn coerce_bit_array_to_string(bit_array: BitArray) -> String 402 + 403 + // A possible way to escape chars would be to split the string into graphemes, 404 + // traverse those one by one and accumulate them back into a string escaping 405 + // ">", "<", etc. as we see them. 406 + // 407 + // However, we can be a lot more performant by working directly on the 408 + // `BitArray` representing a Gleam UTF-8 String. 409 + // This means that, instead of popping a grapheme at a time, we can work 410 + // directly on BitArray slices: this has the big advantage of making sure we 411 + // share as much as possible with the original string without having to build 412 + // a new one from scratch. 413 + // 414 + @target(erlang) 415 + fn do_escape_html( 416 + bin: BitArray, 417 + skip: Int, 418 + original: BitArray, 419 + acc: List(BitArray), 420 + ) -> List(BitArray) { 421 + case bin { 422 + // If we find a char to escape we just advance the `skip` counter so that 423 + // it will be ignored in the following slice, then we append the escaped 424 + // version to the accumulator. 425 + <<"<":utf8, rest:bits>> -> { 426 + let acc = [<<"&lt;":utf8>>, ..acc] 427 + do_escape_html(rest, skip + 1, original, acc) 428 + } 429 + 430 + <<">":utf8, rest:bits>> -> { 431 + let acc = [<<"&gt;":utf8>>, ..acc] 432 + do_escape_html(rest, skip + 1, original, acc) 433 + } 434 + 435 + <<"&":utf8, rest:bits>> -> { 436 + let acc = [<<"&amp;":utf8>>, ..acc] 437 + do_escape_html(rest, skip + 1, original, acc) 438 + } 439 + 440 + // For any other bit that doesn't need to be escaped we go into an inner 441 + // loop, consuming as much "non-escapable" chars as possible. 442 + <<_char, rest:bits>> -> do_escape_html_regular(rest, skip, original, acc, 1) 443 + 444 + <<>> -> acc 445 + 446 + _ -> panic as "non byte aligned string, all strings should be byte aligned" 447 + } 390 448 } 391 449 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 450 + @target(erlang) 451 + fn do_escape_html_regular( 452 + bin: BitArray, 453 + skip: Int, 454 + original: BitArray, 455 + acc: List(BitArray), 456 + len: Int, 457 + ) -> List(BitArray) { 458 + // Remember, if we're here it means we've found a char that doesn't need to be 459 + // escaped, so what we want to do is advance the `len` counter until we reach 460 + // a char that _does_ need to be escaped and take the slice going from 461 + // `skip` with size `len`. 462 + // 463 + // Imagine we're escaping this string: "abc<def&ghi" and we've reached 'd': 464 + // ``` 465 + // abc<def&ghi 466 + // ^ `skip` points here 467 + // ``` 468 + // We're going to be increasing `len` until we reach the '&': 469 + // ``` 470 + // abc<def&ghi 471 + // ^^^ len will be 3 when we reach the '&' that needs escaping 472 + // ``` 473 + // So we take the slice corresponding to "def". 474 + // 475 + case bin { 476 + // If we reach a char that has to be escaped we append the slice starting 477 + // from `skip` with size `len` and the escaped char. 478 + // This is what allows us to share as much of the original string as 479 + // possible: we only allocate a new BitArray for the escaped chars, 480 + // everything else is just a slice of the original String. 481 + <<"<":utf8, rest:bits>> -> { 482 + let assert Ok(slice) = bit_array.slice(original, skip, len) 483 + let acc = [<<"&lt;":utf8>>, slice, ..acc] 484 + do_escape_html(rest, skip + len + 1, original, acc) 485 + } 486 + 487 + <<">":utf8, rest:bits>> -> { 488 + let assert Ok(slice) = bit_array.slice(original, skip, len) 489 + let acc = [<<"&gt;":utf8>>, slice, ..acc] 490 + do_escape_html(rest, skip + len + 1, original, acc) 491 + } 492 + 493 + <<"&":utf8, rest:bits>> -> { 494 + let assert Ok(slice) = bit_array.slice(original, skip, len) 495 + let acc = [<<"&amp;":utf8>>, slice, ..acc] 496 + do_escape_html(rest, skip + len + 1, original, acc) 497 + } 498 + 499 + // If a char doesn't need escaping we keep increasing the length of the 500 + // slice we're going to take. 501 + <<_char, rest:bits>> -> 502 + do_escape_html_regular(rest, skip, original, acc, len + 1) 503 + 504 + <<>> -> 505 + case skip { 506 + 0 -> [original] 507 + _ -> { 508 + let assert Ok(slice) = bit_array.slice(original, skip, len) 509 + [slice, ..acc] 510 + } 511 + } 512 + 513 + _ -> panic as "non byte aligned string, all strings should be byte aligned" 399 514 } 400 515 } 401 516 ··· 593 708 // 594 709 595 710 /// The connection to the client for a HTTP request. 596 - /// 711 + /// 597 712 /// The body of the request can be read from this connection using functions 598 713 /// such as `require_multipart_body`. 599 - /// 714 + /// 600 715 pub opaque type Connection { 601 716 Connection( 602 717 reader: Reader, ··· 676 791 } 677 792 678 793 /// Get the maximum permitted size of a request body of the request in bytes. 679 - /// 794 + /// 680 795 pub fn get_max_body_size(request: Request) -> Int { 681 796 request.body.max_body_size 682 797 } 683 798 684 799 /// Set the secret key base used to sign cookies and other sensitive data. 685 - /// 800 + /// 686 801 /// This key must be at least 64 bytes long and should be kept secret. Anyone 687 802 /// with this secret will be able to manipulate signed cookies and other sensitive 688 803 /// data. 689 804 /// 690 805 /// # Panics 691 - /// 806 + /// 692 807 /// This function will panic if the key is less than 64 bytes long. 693 808 /// 694 809 pub fn set_secret_key_base(request: Request, key: String) -> Request { ··· 701 816 } 702 817 703 818 /// Get the secret key base used to sign cookies and other sensitive data. 704 - /// 819 + /// 705 820 pub fn get_secret_key_base(request: Request) -> String { 706 821 request.body.secret_key_base 707 822 } ··· 723 838 724 839 /// Get the maximum permitted total size of a files uploaded by a request in 725 840 /// bytes. 726 - /// 841 + /// 727 842 pub fn get_max_files_size(request: Request) -> Int { 728 843 request.body.max_files_size 729 844 } ··· 743 858 744 859 /// Get the size limit for each chunk of the request body when read from the 745 860 /// client. 746 - /// 861 + /// 747 862 pub fn get_read_chunk_size(request: Request) -> Int { 748 863 request.body.read_chunk_size 749 864 } 750 865 751 866 /// A convenient alias for a HTTP request with a Wisp connection as the body. 752 - /// 867 + /// 753 868 pub type Request = 754 869 HttpRequest(Connection) 755 870 ··· 758 873 /// if the method is not correct. 759 874 /// 760 875 /// # Examples 761 - /// 876 + /// 762 877 /// ```gleam 763 878 /// fn handle_request(request: Request) -> Response { 764 879 /// use <- wisp.require_method(request, http.Patch) ··· 779 894 780 895 // TODO: re-export once Gleam has a syntax for that 781 896 /// Return the non-empty segments of a request path. 782 - /// 897 + /// 783 898 /// # Examples 784 899 /// 785 900 /// ```gleam ··· 793 908 794 909 // TODO: re-export once Gleam has a syntax for that 795 910 /// Set a given header to a given value, replacing any existing value. 796 - /// 911 + /// 797 912 /// # Examples 798 913 /// 799 914 /// ```gleam ··· 863 978 /// return an incorrect value, depending on the underlying web server. It is the 864 979 /// responsibility of the caller to cache the body if it is needed multiple 865 980 /// times. 866 - /// 981 + /// 867 982 /// If the body is larger than the `max_body_size` limit then an empty response 868 983 /// with status code 413: Entity too large will be returned to the client. 869 - /// 984 + /// 870 985 /// If the body is found not to be valid UTF-8 then an empty response with 871 986 /// status code 400: Bad request will be returned to the client. 872 - /// 987 + /// 873 988 /// # Examples 874 989 /// 875 990 /// ```gleam ··· 899 1014 /// return an incorrect value, depending on the underlying web server. It is the 900 1015 /// responsibility of the caller to cache the body if it is needed multiple 901 1016 /// times. 902 - /// 1017 + /// 903 1018 /// If the body is larger than the `max_body_size` limit then an empty response 904 1019 /// with status code 413: Entity too large will be returned to the client. 905 - /// 1020 + /// 906 1021 /// # Examples 907 1022 /// 908 1023 /// ```gleam ··· 925 1040 // TODO: don't always return entity to large. Other errors are possible, such as 926 1041 // network errors. 927 1042 /// Read the entire body of the request as a bit string. 928 - /// 1043 + /// 929 1044 /// You may instead wish to use the `require_bit_array_body` or the 930 1045 /// `require_string_body` middleware functions instead. 931 - /// 1046 + /// 932 1047 /// This function does not cache the body in any way, so if you call this 933 1048 /// function (or any other body reading function) more than once it may hang or 934 1049 /// return an incorrect value, depending on the underlying web server. It is the 935 1050 /// responsibility of the caller to cache the body if it is needed multiple 936 1051 /// times. 937 - /// 1052 + /// 938 1053 /// If the body is larger than the `max_body_size` limit then an empty response 939 1054 /// with status code 413: Entity too large will be returned to the client. 940 - /// 1055 + /// 941 1056 pub fn read_body_to_bitstring(request: Request) -> Result(BitArray, Nil) { 942 1057 let connection = request.body 943 1058 read_body_loop( ··· 971 1086 /// A middleware which extracts form data from the body of a request that is 972 1087 /// encoded as either `application/x-www-form-urlencoded` or 973 1088 /// `multipart/form-data`. 974 - /// 1089 + /// 975 1090 /// Extracted fields are sorted into alphabetical order by key, so if you wish 976 1091 /// to use pattern matching the order can be relied upon. 977 - /// 1092 + /// 978 1093 /// ```gleam 979 1094 /// fn handle_request(request: Request) -> Response { 980 1095 /// use form <- wisp.require_form(request) ··· 1003 1118 /// 1004 1119 /// If the body cannot be parsed successfully then an empty response with status 1005 1120 /// code 400: Bad request will be returned to the client. 1006 - /// 1121 + /// 1007 1122 pub fn require_form( 1008 1123 request: Request, 1009 1124 next: fn(FormData) -> Response, ··· 1030 1145 /// Unsupported media type if the header is not the expected value 1031 1146 /// 1032 1147 /// # Examples 1033 - /// 1148 + /// 1034 1149 /// ```gleam 1035 1150 /// fn handle_request(request: Request) -> Response { 1036 1151 /// use <- wisp.require_content_type(request, "application/json") ··· 1050 1165 } 1051 1166 1052 1167 /// A middleware which extracts JSON from the body of a request. 1053 - /// 1168 + /// 1054 1169 /// ```gleam 1055 1170 /// fn handle_request(request: Request) -> Response { 1056 1171 /// use json <- wisp.require_json(request) ··· 1071 1186 /// 1072 1187 /// If the body cannot be parsed successfully then an empty response with status 1073 1188 /// code 400: Bad request will be returned to the client. 1074 - /// 1189 + /// 1075 1190 pub fn require_json(request: Request, next: fn(Dynamic) -> Response) -> Response { 1076 1191 use <- require_content_type(request, "application/json") 1077 1192 use body <- require_string_body(request) ··· 1305 1420 } 1306 1421 1307 1422 /// Data parsed from form sent in a request's body. 1308 - /// 1423 + /// 1309 1424 pub type FormData { 1310 1425 FormData( 1311 1426 /// String values of the form's fields. ··· 1429 1544 /// 1430 1545 /// The `under` parameter is the request path prefix that must match for the 1431 1546 /// file to be served. 1432 - /// 1547 + /// 1433 1548 /// | `under` | `from` | `request.path` | `file` | 1434 1549 /// |-----------|---------|--------------------|-------------------------| 1435 1550 /// | `/static` | `/data` | `/static/file.txt` | `/data/file.txt` | ··· 1576 1691 /// > erlang.priv_directory("my_app") 1577 1692 /// // -> Ok("/some/location/my_app/priv") 1578 1693 /// ``` 1579 - /// 1694 + /// 1580 1695 pub const priv_directory = erlang.priv_directory 1581 1696 1582 1697 // ··· 1585 1700 1586 1701 /// Configure the Erlang logger, setting the minimum log level to `info`, to be 1587 1702 /// called when your application starts. 1588 - /// 1703 + /// 1589 1704 /// You may wish to use an alternative for this such as one provided by a more 1590 1705 /// sophisticated logging library. 1591 - /// 1706 + /// 1592 1707 /// In future this function may be extended to change the output format. 1593 - /// 1708 + /// 1594 1709 pub fn configure_logger() -> Nil { 1595 1710 logging.configure() 1596 1711 } 1597 1712 1598 1713 /// Log a message to the Erlang logger with the level of `emergency`. 1599 - /// 1714 + /// 1600 1715 /// See the [Erlang logger documentation][1] for more information. 1601 - /// 1716 + /// 1602 1717 /// [1]: https://www.erlang.org/doc/man/logger 1603 - /// 1718 + /// 1604 1719 pub fn log_emergency(message: String) -> Nil { 1605 1720 logging.log(logging.Emergency, message) 1606 1721 } 1607 1722 1608 1723 /// Log a message to the Erlang logger with the level of `alert`. 1609 - /// 1724 + /// 1610 1725 /// See the [Erlang logger documentation][1] for more information. 1611 - /// 1726 + /// 1612 1727 /// [1]: https://www.erlang.org/doc/man/logger 1613 - /// 1728 + /// 1614 1729 pub fn log_alert(message: String) -> Nil { 1615 1730 logging.log(logging.Alert, message) 1616 1731 } 1617 1732 1618 1733 /// Log a message to the Erlang logger with the level of `critical`. 1619 - /// 1734 + /// 1620 1735 /// See the [Erlang logger documentation][1] for more information. 1621 - /// 1736 + /// 1622 1737 /// [1]: https://www.erlang.org/doc/man/logger 1623 - /// 1738 + /// 1624 1739 pub fn log_critical(message: String) -> Nil { 1625 1740 logging.log(logging.Critical, message) 1626 1741 } 1627 1742 1628 1743 /// Log a message to the Erlang logger with the level of `error`. 1629 - /// 1744 + /// 1630 1745 /// See the [Erlang logger documentation][1] for more information. 1631 - /// 1746 + /// 1632 1747 /// [1]: https://www.erlang.org/doc/man/logger 1633 - /// 1748 + /// 1634 1749 pub fn log_error(message: String) -> Nil { 1635 1750 logging.log(logging.Error, message) 1636 1751 } 1637 1752 1638 1753 /// Log a message to the Erlang logger with the level of `warning`. 1639 - /// 1754 + /// 1640 1755 /// See the [Erlang logger documentation][1] for more information. 1641 - /// 1756 + /// 1642 1757 /// [1]: https://www.erlang.org/doc/man/logger 1643 - /// 1758 + /// 1644 1759 pub fn log_warning(message: String) -> Nil { 1645 1760 logging.log(logging.Warning, message) 1646 1761 } 1647 1762 1648 1763 /// Log a message to the Erlang logger with the level of `notice`. 1649 - /// 1764 + /// 1650 1765 /// See the [Erlang logger documentation][1] for more information. 1651 - /// 1766 + /// 1652 1767 /// [1]: https://www.erlang.org/doc/man/logger 1653 - /// 1768 + /// 1654 1769 pub fn log_notice(message: String) -> Nil { 1655 1770 logging.log(logging.Notice, message) 1656 1771 } 1657 1772 1658 1773 /// Log a message to the Erlang logger with the level of `info`. 1659 - /// 1774 + /// 1660 1775 /// See the [Erlang logger documentation][1] for more information. 1661 - /// 1776 + /// 1662 1777 /// [1]: https://www.erlang.org/doc/man/logger 1663 - /// 1778 + /// 1664 1779 pub fn log_info(message: String) -> Nil { 1665 1780 logging.log(logging.Info, message) 1666 1781 } 1667 1782 1668 1783 /// Log a message to the Erlang logger with the level of `debug`. 1669 - /// 1784 + /// 1670 1785 /// See the [Erlang logger documentation][1] for more information. 1671 - /// 1786 + /// 1672 1787 /// [1]: https://www.erlang.org/doc/man/logger 1673 - /// 1788 + /// 1674 1789 pub fn log_debug(message: String) -> Nil { 1675 1790 logging.log(logging.Debug, message) 1676 1791 } ··· 1689 1804 1690 1805 /// Sign a message which can later be verified using the `verify_signed_message` 1691 1806 /// function to detect if the message has been tampered with. 1692 - /// 1807 + /// 1693 1808 /// Signed messages are not encrypted and can be read by anyone. They are not 1694 1809 /// suitable for storing sensitive information. 1695 - /// 1810 + /// 1696 1811 /// This function uses the secret key base from the request. If the secret 1697 1812 /// changes then the signature will no longer be verifiable. 1698 - /// 1813 + /// 1699 1814 pub fn sign_message( 1700 1815 request: Request, 1701 1816 message: BitArray, ··· 1705 1820 } 1706 1821 1707 1822 /// Verify a signed message which was signed using the `sign_message` function. 1708 - /// 1823 + /// 1709 1824 /// Returns the content of the message if the signature is valid, otherwise 1710 1825 /// returns an error. 1711 - /// 1826 + /// 1712 1827 /// This function uses the secret key base from the request. If the secret 1713 1828 /// changes then the signature will no longer be verifiable. 1714 - /// 1829 + /// 1715 1830 pub fn verify_signed_message( 1716 1831 request: Request, 1717 1832 message: String, ··· 1749 1864 /// wisp.ok() 1750 1865 /// |> wisp.set_cookie(request, "id", "123", wisp.PlainText, 60 * 60) 1751 1866 /// ``` 1752 - /// 1867 + /// 1753 1868 /// Setting a signed cookie that the client can read but not modify: 1754 - /// 1869 + /// 1755 1870 /// ```gleam 1756 1871 /// wisp.ok() 1757 1872 /// |> wisp.set_cookie(request, "id", value, wisp.Signed, 60 * 60) ··· 1821 1936 1822 1937 // TODO: chunk the body 1823 1938 /// Create a connection which will return the given body when read. 1824 - /// 1939 + /// 1825 1940 /// This function is intended for use in tests, though you probably want the 1826 1941 /// `wisp/testing` module instead. 1827 - /// 1942 + /// 1828 1943 pub fn create_canned_connection( 1829 1944 body: BitArray, 1830 1945 secret_key_base: String,
+4
src/wisp_ffi.erl
··· 1 + -module(wisp_ffi). 2 + -export([coerce/1]). 3 + 4 + coerce(X) -> X.