a simple gleam link shortener using a lustre server component and sqlight + cake. has user-friendly, readable slugs
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

first commit

lekkice.moe d91f6bd4

+519
+8
.envrc
··· 1 + export DIRENV_WARN_TIMEOUT=20s 2 + 3 + eval "$(devenv direnvrc)" 4 + 5 + # The use_devenv function supports passing flags to the devenv command 6 + # For example: use devenv --impure --option services.postgres.enable:bool true 7 + use devenv 8 +
+23
.github/workflows/test.yml
··· 1 + name: test 2 + 3 + on: 4 + push: 5 + branches: 6 + - master 7 + - main 8 + pull_request: 9 + 10 + jobs: 11 + test: 12 + runs-on: ubuntu-latest 13 + steps: 14 + - uses: actions/checkout@v4 15 + - uses: erlef/setup-beam@v1 16 + with: 17 + otp-version: "27.1.2" 18 + gleam-version: "1.11.1" 19 + rebar3-version: "3" 20 + # elixir-version: "1" 21 + - run: gleam deps download 22 + - run: gleam test 23 + - run: gleam format --check app test
+8
.gitignore
··· 1 + *.beam 2 + *.ez 3 + /build 4 + erl_crash.dump 5 + .devenv* 6 + db/main.db 7 + 8 + priv/static/css/app.css
+24
README.md
··· 1 + # glortener 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/glortener)](https://hex.pm/packages/glortener) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/glortener/) 5 + 6 + ```sh 7 + gleam add glortener@1 8 + ``` 9 + ```gleam 10 + import glortener 11 + 12 + pub fn main() -> Nil { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/glortener>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + ```
+9
db/schema.sql
··· 1 + CREATE TABLE IF NOT EXISTS "schema_migrations" (version varchar(128) primary key); 2 + CREATE TABLE links ( 3 + url text, 4 + slug text 5 + ); 6 + CREATE UNIQUE INDEX uq_links ON links (slug); 7 + -- Dbmate schema migrations 8 + INSERT INTO "schema_migrations" (version) VALUES 9 + ('20250810004954');
+119
devenv.lock
··· 1 + { 2 + "nodes": { 3 + "devenv": { 4 + "locked": { 5 + "dir": "src/modules", 6 + "lastModified": 1754772503, 7 + "owner": "cachix", 8 + "repo": "devenv", 9 + "rev": "3020356e20ca2856b692756564b07095319d9553", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "dir": "src/modules", 14 + "owner": "cachix", 15 + "repo": "devenv", 16 + "type": "github" 17 + } 18 + }, 19 + "flake-compat": { 20 + "flake": false, 21 + "locked": { 22 + "lastModified": 1747046372, 23 + "owner": "edolstra", 24 + "repo": "flake-compat", 25 + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", 26 + "type": "github" 27 + }, 28 + "original": { 29 + "owner": "edolstra", 30 + "repo": "flake-compat", 31 + "type": "github" 32 + } 33 + }, 34 + "git-hooks": { 35 + "inputs": { 36 + "flake-compat": "flake-compat", 37 + "gitignore": "gitignore", 38 + "nixpkgs": [ 39 + "nixpkgs" 40 + ] 41 + }, 42 + "locked": { 43 + "lastModified": 1754416808, 44 + "owner": "cachix", 45 + "repo": "git-hooks.nix", 46 + "rev": "9c52372878df6911f9afc1e2a1391f55e4dfc864", 47 + "type": "github" 48 + }, 49 + "original": { 50 + "owner": "cachix", 51 + "repo": "git-hooks.nix", 52 + "type": "github" 53 + } 54 + }, 55 + "gitignore": { 56 + "inputs": { 57 + "nixpkgs": [ 58 + "git-hooks", 59 + "nixpkgs" 60 + ] 61 + }, 62 + "locked": { 63 + "lastModified": 1709087332, 64 + "owner": "hercules-ci", 65 + "repo": "gitignore.nix", 66 + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 67 + "type": "github" 68 + }, 69 + "original": { 70 + "owner": "hercules-ci", 71 + "repo": "gitignore.nix", 72 + "type": "github" 73 + } 74 + }, 75 + "nixpkgs": { 76 + "locked": { 77 + "lastModified": 1754299112, 78 + "owner": "cachix", 79 + "repo": "devenv-nixpkgs", 80 + "rev": "16c21c9f5c6fb978466e91182a248dd8ca1112ac", 81 + "type": "github" 82 + }, 83 + "original": { 84 + "owner": "cachix", 85 + "ref": "rolling", 86 + "repo": "devenv-nixpkgs", 87 + "type": "github" 88 + } 89 + }, 90 + "nixpkgs-unstable": { 91 + "locked": { 92 + "lastModified": 1767116409, 93 + "owner": "NixOS", 94 + "repo": "nixpkgs", 95 + "rev": "cad22e7d996aea55ecab064e84834289143e44a0", 96 + "type": "github" 97 + }, 98 + "original": { 99 + "owner": "NixOS", 100 + "ref": "nixos-unstable", 101 + "repo": "nixpkgs", 102 + "type": "github" 103 + } 104 + }, 105 + "root": { 106 + "inputs": { 107 + "devenv": "devenv", 108 + "git-hooks": "git-hooks", 109 + "nixpkgs": "nixpkgs", 110 + "nixpkgs-unstable": "nixpkgs-unstable", 111 + "pre-commit-hooks": [ 112 + "git-hooks" 113 + ] 114 + } 115 + } 116 + }, 117 + "root": "root", 118 + "version": 7 119 + }
+23
devenv.nix
··· 1 + { pkgs, inputs, ... }: 2 + 3 + let 4 + pkgs-unstable = import inputs.nixpkgs-unstable { system = pkgs.stdenv.system; }; 5 + in 6 + { 7 + packages = with pkgs-unstable; [ 8 + gleam 9 + beamMinimal27Packages.erlang 10 + beamMinimal27Packages.rebar3 11 + 12 + inotify-tools 13 + 14 + dbmate 15 + sqlite 16 + ]; 17 + 18 + languages.javascript.pnpm.enable = true; 19 + 20 + env.DATABASE_URL = "file:db/main.db"; 21 + } 22 + 23 +
+4
devenv.yaml
··· 1 + inputs: 2 + nixpkgs-unstable: 3 + url: github:NixOS/nixpkgs/nixos-unstable 4 +
+28
gleam.toml
··· 1 + name = "glortener" 2 + version = "1.0.0" 3 + 4 + # Fill out these fields if you intend to generate HTML documentation or publish 5 + # your project to the Hex package manager. 6 + # 7 + # description = "" 8 + # licences = ["Apache-2.0"] 9 + # repository = { type = "github", user = "", repo = "" } 10 + # links = [{ title = "Website", href = "" }] 11 + # 12 + # For a full reference of all the available options, you can have a look at 13 + # https://gleam.run/writing-gleam/gleam-toml/. 14 + 15 + [dependencies] 16 + gleam_stdlib = ">= 0.44.0 and < 2.0.0" 17 + wisp = ">= 2.1.1 and < 3.0.0" 18 + sqlight = ">= 1.0.3 and < 2.0.0" 19 + mist = ">= 5.0.3 and < 6.0.0" 20 + gleam_erlang = ">= 1.3.0 and < 2.0.0" 21 + gleam_http = ">= 4.3.0 and < 5.0.0" 22 + cake = ">= 2.2.0 and < 3.0.0" 23 + envoy = ">= 1.1.0 and < 2.0.0" 24 + cake_sqlight = ">= 1.2.0 and < 2.0.0" 25 + friendly_id = ">= 1.0.0 and < 2.0.0" 26 + 27 + [dev-dependencies] 28 + gleeunit = ">= 1.0.0 and < 2.0.0"
+48
manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "cake", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "cake", source = "hex", outer_checksum = "C5F55176DFB6D7E9580DBAC0A638299C70E0DB5C1F00537B01E5A29A1CE49D8F" }, 6 + { name = "cake_sqlight", version = "1.2.0", build_tools = ["gleam"], requirements = ["cake", "gleam_stdlib", "gleam_time", "sqlight"], otp_app = "cake_sqlight", source = "hex", outer_checksum = "BFBCC0988C119D5435F0867F566ADFB82CAA8AF125B9A2F3778C6E3384E69452" }, 7 + { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, 8 + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, 9 + { name = "esqlite", version = "0.9.0", build_tools = ["rebar3"], requirements = [], otp_app = "esqlite", source = "hex", outer_checksum = "CCF72258A4EE152EC7AD92AA9A03552EB6CA1B06B65C93AD5B6E55C302E05855" }, 10 + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, 11 + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 12 + { name = "friendly_id", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "iv"], otp_app = "friendly_id", source = "hex", outer_checksum = "829E9775131E9BD2DDE90EC4C439C92AD0902C2C671FA8EFD75B96C8DAB1BA54" }, 13 + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, 14 + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, 15 + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, 16 + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, 17 + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, 18 + { name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" }, 19 + { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, 20 + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, 21 + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 22 + { name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" }, 23 + { name = "gramps", version = "6.0.0", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "8B7195978FBFD30B43DF791A8A272041B81E45D245314D7A41FC57237AA882A0" }, 24 + { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, 25 + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, 26 + { name = "iv", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "iv", source = "hex", outer_checksum = "724100253DF130F19A32D683CBC41665C42E66AECBFA06CEE628FD426CF4F0C7" }, 27 + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, 28 + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, 29 + { name = "mist", version = "5.0.3", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7C4BE717A81305323C47C8A591E6B9BA4AC7F56354BF70B4D3DF08CC01192668" }, 30 + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, 31 + { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" }, 32 + { name = "sqlight", version = "1.0.3", build_tools = ["gleam"], requirements = ["esqlite", "gleam_stdlib"], otp_app = "sqlight", source = "hex", outer_checksum = "CADD79663C9B61D4BAC960A47CC2D42CA8F48EAF5804DBEB79977287750F4B16" }, 33 + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, 34 + { name = "wisp", version = "2.1.1", build_tools = ["gleam"], requirements = ["directories", "exception", "filepath", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "46E2E31DECD61A3748CF6CB317D9AC432BBC8D8A6E65655A9E787BDC69389DE0" }, 35 + ] 36 + 37 + [requirements] 38 + cake = { version = ">= 2.2.0 and < 3.0.0" } 39 + cake_sqlight = { version = ">= 1.2.0 and < 2.0.0" } 40 + envoy = { version = ">= 1.1.0 and < 2.0.0" } 41 + friendly_id = { version = ">= 1.0.0 and < 2.0.0" } 42 + gleam_erlang = { version = ">= 1.3.0 and < 2.0.0" } 43 + gleam_http = { version = ">= 4.3.0 and < 5.0.0" } 44 + gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 45 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 46 + mist = { version = ">= 5.0.3 and < 6.0.0" } 47 + sqlight = { version = ">= 1.0.3 and < 2.0.0" } 48 + wisp = { version = ">= 2.1.1 and < 3.0.0" }
+35
src/glortener.gleam
··· 1 + import envoy 2 + import gleam/result 3 + import glortener/web 4 + import sqlight 5 + 6 + import gleam/erlang/process 7 + import glortener/router 8 + import mist 9 + import wisp 10 + import wisp/wisp_mist 11 + 12 + pub fn main() { 13 + use conn <- sqlight.with_connection( 14 + envoy.get("DATABASE_URL") |> result.unwrap(":memory:"), 15 + ) 16 + 17 + wisp.configure_logger() 18 + 19 + let context = web.Context(db: conn) 20 + 21 + let secret_key_base = 22 + envoy.get("SECRET_KEY_BASE") 23 + |> result.lazy_unwrap(fn() { wisp.random_string(64) }) 24 + 25 + let handler = router.handle_request(_, context) 26 + 27 + let assert Ok(_) = 28 + handler 29 + |> wisp_mist.handler(secret_key_base) 30 + |> mist.new 31 + |> mist.port(8000) 32 + |> mist.start 33 + 34 + process.sleep_forever() 35 + }
+71
src/glortener/links.gleam
··· 1 + import cake/adapter/sqlite 2 + import gleam/dynamic/decode 3 + import gleam/result 4 + 5 + import cake/delete as d 6 + import cake/insert as i 7 + import cake/select as s 8 + import cake/where as w 9 + import sqlight 10 + 11 + import gleam/option.{type Option, None, Some} 12 + 13 + pub type Link { 14 + Link(slug: String, url: String) 15 + } 16 + 17 + fn from_sqlite() -> decode.Decoder(Link) { 18 + use slug <- decode.field(0, decode.string) 19 + use url <- decode.field(1, decode.string) 20 + Link(slug:, url:) |> decode.success() 21 + } 22 + 23 + pub fn create_link( 24 + conn: sqlight.Connection, 25 + link: Link, 26 + ) -> Result(Nil, sqlight.Error) { 27 + [ 28 + [i.string(link.slug), i.string(link.url)] |> i.row, 29 + ] 30 + |> i.from_values(table_name: "links", columns: ["slug", "url"]) 31 + |> i.to_query() 32 + |> sqlite.run_write_query(from_sqlite(), conn) 33 + |> result.map(fn(_) { Nil }) 34 + } 35 + 36 + pub fn get_from_slug(conn: sqlight.Connection, slug: String) -> Option(Link) { 37 + let result = 38 + s.new() 39 + |> s.from_table("links") 40 + |> s.selects([s.col("slug"), s.col("url")]) 41 + |> s.where(w.col("slug") |> w.eq(w.string(slug))) 42 + |> s.to_query 43 + |> sqlite.run_read_query(from_sqlite(), conn) 44 + 45 + case result { 46 + Ok([link]) -> Some(link) 47 + _ -> None 48 + } 49 + } 50 + 51 + pub fn get_all_links(conn: sqlight.Connection) -> List(Link) { 52 + let result = 53 + s.new() 54 + |> s.from_table("links") 55 + |> s.selects([s.col("slug"), s.col("url")]) 56 + |> s.to_query 57 + |> sqlite.run_read_query(from_sqlite(), conn) 58 + 59 + case result { 60 + Ok(slugs) -> slugs 61 + _ -> [] 62 + } 63 + } 64 + 65 + fn delete_link(conn: sqlight.Connection, slug: String) { 66 + d.new() 67 + |> d.table("links") 68 + |> d.where(w.col("slug") |> w.eq(w.string(slug))) 69 + |> d.to_query() 70 + |> sqlite.run_write_query(from_sqlite(), conn) 71 + }
+78
src/glortener/router.gleam
··· 1 + import gleam/dynamic/decode 2 + import gleam/http.{Get, Post} 3 + import gleam/option.{None, Some} 4 + import gleam/result 5 + import gleam/string 6 + import glortener/links.{type Link, Link} 7 + import glortener/web 8 + import sqlight 9 + import wisp.{type Request, type Response} 10 + 11 + type CreateLinkError { 12 + DecodeError(List(decode.DecodeError)) 13 + DatabaseError(sqlight.Error) 14 + } 15 + 16 + pub fn handle_request(req: Request, context: web.Context) -> Response { 17 + use req <- web.middleware(req) 18 + 19 + case wisp.path_segments(req) { 20 + [] -> home_page(req) 21 + 22 + ["/api/create"] -> create(req, context) 23 + 24 + [slug] -> redirect_to_slug(req, context, slug) 25 + 26 + _ -> wisp.not_found() 27 + } 28 + } 29 + 30 + fn home_page(req: Request) -> Response { 31 + use <- wisp.require_method(req, Get) 32 + 33 + wisp.ok() 34 + |> wisp.html_body("i'm not joe!!!!") 35 + } 36 + 37 + fn create(req: Request, context: web.Context) -> Response { 38 + use <- wisp.require_method(req, Post) 39 + use json <- wisp.require_json(req) 40 + 41 + let result = { 42 + use link <- result.try( 43 + decode.run(json, link_decoder()) 44 + |> result.map_error(DecodeError), 45 + ) 46 + use _ <- result.try( 47 + links.create_link(context.db, link) 48 + |> result.map_error(DatabaseError), 49 + ) 50 + Ok(Nil) 51 + } 52 + 53 + case result { 54 + Ok(_) -> 55 + wisp.ok() 56 + |> wisp.string_body("success!") 57 + Error(error) -> wisp.bad_request(string.inspect(error)) 58 + } 59 + } 60 + 61 + fn redirect_to_slug( 62 + req: Request, 63 + context: web.Context, 64 + slug: String, 65 + ) -> Response { 66 + use <- wisp.require_method(req, Get) 67 + 68 + case links.get_from_slug(context.db, slug) { 69 + Some(link) -> wisp.redirect(to: link.url) 70 + None -> wisp.not_found() 71 + } 72 + } 73 + 74 + pub fn link_decoder() -> decode.Decoder(Link) { 75 + use slug <- decode.field("slug", decode.string) 76 + use url <- decode.field("url", decode.string) 77 + decode.success(Link(slug:, url:)) 78 + }
+19
src/glortener/web.gleam
··· 1 + import wisp 2 + import sqlight 3 + 4 + pub type Context { 5 + Context(db: sqlight.Connection) 6 + } 7 + 8 + pub fn middleware( 9 + req: wisp.Request, 10 + handle_request: fn(wisp.Request) -> wisp.Response, 11 + ) -> wisp.Response { 12 + let req = wisp.method_override(req) 13 + use <- wisp.log_request(req) 14 + use <- wisp.rescue_crashes 15 + use req <- wisp.handle_head(req) 16 + use req <- wisp.csrf_known_header_protection(req) 17 + 18 + handle_request(req) 19 + }
+13
test/glortener_test.gleam
··· 1 + import gleeunit 2 + 3 + pub fn main() -> Nil { 4 + gleeunit.main() 5 + } 6 + 7 + // gleeunit test functions end in `_test` 8 + pub fn hello_world_test() { 9 + let name = "Joe" 10 + let greeting = "Hello, " <> name <> "!" 11 + 12 + assert greeting == "Hello, Joe!" 13 + }