Commit Diff


commit - d41859a46afd1a54ec1b5c5dac5d8f88104b1b33
commit + 07b9f14059ba33b55b45ff61b78b05470a963068
blob - 38d357f7c440babbac320f31b5cab51bc8194f0b
blob + 1b7aebe30f53e1899976c373ef9a2ed6474097ec
--- config/runtime.exs
+++ config/runtime.exs
@@ -11,9 +11,7 @@ if config_env() == :prod do
 
           case String.split(line, "=", parts: 2) do
             [k, v] when byte_size(k) > 0 ->
-              if String.contains?(v, "$"),
-                do: acc,
-                else: Map.put(acc, k, v |> String.trim("\"") |> String.trim("'"))
+              Map.put(acc, k, v |> String.trim("\"") |> String.trim("'"))
 
             _ ->
               acc
blob - 2a618c27dce7829a505dde17a087acf12fd0164a
blob + ffc6b643e6de43cd73fda7e826f7066d716193eb
--- lib/shortener/db.ex
+++ lib/shortener/db.ex
@@ -47,43 +47,27 @@ defmodule Shirts.DB do
     GenServer.call(__MODULE__, :list)
   end
 
-  # --- server callbacks ---
-
   @impl true
   def handle_call({:insert, code, url}, _from, conn) do
     result =
-      case Exqlite.Sqlite3.prepare(conn, "INSERT INTO shirts (code, url) VALUES (?1, ?2)") do
-        {:ok, stmt} ->
-          r =
-            with :ok <- Exqlite.Sqlite3.bind(stmt, [code, url]),
-                 :done <- Exqlite.Sqlite3.step(conn, stmt) do
-              :ok
-            else
-              {:error, reason} -> {:error, reason}
-            end
+      with_stmt(conn, "INSERT INTO shirts (code, url) VALUES (?1, ?2)", fn stmt ->
+        with :ok <- Exqlite.Sqlite3.bind(stmt, [code, url]),
+             :done <- Exqlite.Sqlite3.step(conn, stmt) do
+          :ok
+        end
+      end)
 
-          _ = Exqlite.Sqlite3.release(conn, stmt)
-          r
-
-        {:error, reason} ->
-          {:error, reason}
-      end
-
     {:reply, result, conn}
   end
 
   def handle_call({:lookup, code}, _from, conn) do
     result =
-      case Exqlite.Sqlite3.prepare(conn, "SELECT url FROM shirts WHERE code = ?1") do
-        {:ok, stmt} ->
-          r = do_lookup(conn, stmt, code)
-          _ = Exqlite.Sqlite3.release(conn, stmt)
-          r
+      with_stmt(conn, "SELECT url FROM shirts WHERE code = ?1", fn stmt ->
+        with :ok <- Exqlite.Sqlite3.bind(stmt, [code]) do
+          lookup_step(conn, stmt)
+        end
+      end)
 
-        {:error, reason} ->
-          {:error, reason}
-      end
-
     {:reply, result, conn}
   end
 
@@ -91,40 +75,40 @@ defmodule Shirts.DB do
     sql = "SELECT code, url, created_at FROM shirts ORDER BY created_at ASC"
 
     result =
-      case Exqlite.Sqlite3.prepare(conn, sql) do
-        {:ok, stmt} ->
-          r =
-            case Exqlite.Sqlite3.fetch_all(conn, stmt) do
-              {:ok, rows} ->
-                {:ok,
-                 Enum.map(rows, fn [code, url, created_at] ->
-                   %{code: code, url: url, created_at: created_at}
-                 end)}
+      with_stmt(conn, sql, fn stmt ->
+        with {:ok, rows} <- Exqlite.Sqlite3.fetch_all(conn, stmt) do
+          {:ok,
+           Enum.map(rows, fn [code, url, created_at] ->
+             %{code: code, url: url, created_at: created_at}
+           end)}
+        end
+      end)
 
-              {:error, reason} ->
-                {:error, reason}
-            end
-
-          _ = Exqlite.Sqlite3.release(conn, stmt)
-          r
-
-        {:error, reason} ->
-          {:error, reason}
-      end
-
     {:reply, result, conn}
   end
 
-  defp do_lookup(conn, stmt, code) do
-    with :ok <- Exqlite.Sqlite3.bind(stmt, [code]) do
-      case Exqlite.Sqlite3.step(conn, stmt) do
-        {:row, [url]} -> {:ok, url}
-        :done -> :not_found
-        {:error, reason} -> {:error, reason}
-      end
+  defp lookup_step(conn, stmt) do
+    case Exqlite.Sqlite3.step(conn, stmt) do
+      {:row, [url]} -> {:ok, url}
+      :done -> :not_found
+      {:error, reason} -> {:error, reason}
     end
   end
 
+  defp with_stmt(conn, sql, fun) do
+    case Exqlite.Sqlite3.prepare(conn, sql) do
+      {:ok, stmt} ->
+        try do
+          fun.(stmt)
+        after
+          Exqlite.Sqlite3.release(conn, stmt)
+        end
+
+      {:error, reason} ->
+        {:error, reason}
+    end
+  end
+
   @impl true
   def terminate(_reason, conn) do
     Logger.info("Closing database connection")
blob - 1c8962d6361212cacd06b49b659ce6b362902368
blob + 092700072553550e2204e3d3e869b0a773467f86
--- lib/shortener/router.ex
+++ lib/shortener/router.ex
@@ -27,9 +27,7 @@ defmodule Shirts.Router do
       base_url = Application.fetch_env!(:shirts, :base_url)
       short_url = "#{base_url}/s/#{code}"
 
-      conn
-      |> put_resp_content_type("application/json")
-      |> send_resp(201, Jason.encode!(%{short_url: short_url, code: code, url: url}))
+      json(conn, 201, %{short_url: short_url, code: code, url: url})
     else
       {:error, :unauthorized} ->
         json(conn, 401, %{error: "unauthorized"})
@@ -51,13 +49,7 @@ defmodule Shirts.Router do
   get "/history" do
     with :ok <- check_auth(conn),
          {:ok, shirts} <- Shirts.DB.list() do
-      if wants_json?(conn) do
-        json(conn, 200, %{shirts: shirts, count: length(shirts)})
-      else
-        conn
-        |> put_resp_content_type("text/plain")
-        |> send_resp(200, history_table(shirts))
-      end
+      json(conn, 200, %{shirts: shirts, count: length(shirts)})
     else
       {:error, :unauthorized} ->
         json(conn, 401, %{error: "unauthorized"})
@@ -121,54 +113,6 @@ defmodule Shirts.Router do
     end
   end
 
-  defp wants_json?(conn) do
-    get_req_header(conn, "accept")
-    |> Enum.any?(&String.contains?(&1, "application/json"))
-  end
-
-  # Renders the link history as a colored ANSI table matching the `shist` script.
-  @history_columns ["code", "created_at", "url"]
-
-  defp history_table([]), do: "shirts (0)\n"
-
-  defp history_table(shirts) do
-    base_url = Application.fetch_env!(:shirts, :base_url)
-
-    rows =
-      Enum.map(shirts, fn s ->
-        %{"code" => "#{base_url}/s/#{s.code}", "created_at" => s.created_at, "url" => s.url}
-      end)
-
-    table =
-      Owl.Table.new(rows,
-        border_style: :solid_rounded,
-        padding_x: 1,
-        sort_columns: fn a, b ->
-          Enum.find_index(@history_columns, &(&1 == a)) <=
-            Enum.find_index(@history_columns, &(&1 == b))
-        end,
-        render_cell: [
-          header: &(&1 |> Owl.Data.tag(:cyan)),
-          body: fn
-            "code", v -> Owl.Data.tag(v, :cyan)
-            "url", v -> Owl.Data.tag(v, :blue)
-            "created_at", v -> Owl.Data.tag(v, :light_black)
-          end
-        ]
-      )
-
-    [
-      Owl.Data.tag("shirts", :bright),
-      " ",
-      Owl.Data.tag("(#{length(shirts)})", :light_black),
-      "\n\n",
-      table,
-      "\n"
-    ]
-    |> Owl.Data.to_chardata()
-    |> IO.chardata_to_string()
-  end
-
   defp random_code do
     :crypto.strong_rand_bytes(6)
     |> Base.url_encode64(padding: false)
blob - 502180943c37ebb3d98511f069129366510f07e9
blob + d5dbeba9d23237432eb43bc1c7fd8ed36722dce5
--- mix.exs
+++ mix.exs
@@ -25,8 +25,7 @@ defmodule Shirts.MixProject do
       {:bandit, "~> 1.10"},
       {:plug, "~> 1.19"},
       {:exqlite, "~> 0.37"},
-      {:jason, "~> 1.4"},
-      {:owl, "~> 0.13"}
+      {:jason, "~> 1.4"}
     ]
   end
 end
blob - e37e5b91f425fec0a997030fd08e172cde639f91
blob + 57d13f80abb43864c11d780bb759beb1ef72641b
--- mix.lock
+++ mix.lock
@@ -17,7 +17,6 @@
   "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
   "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
   "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
-  "owl": {:hex, :owl, "0.13.1", "1ec4a5dea170465f0e90c502c203079224516bc0cbd599281c8667b3c6ef8848", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "351e768af8f2edc575cdaab1a5a2f6d6381be591758a026c701c703145508a0c"},
   "plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"},
   "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
   "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},