diff --git a/lib/supabase/fetcher.ex b/lib/supabase/fetcher.ex index d39ff0a..7167e93 100644 --- a/lib/supabase/fetcher.ex +++ b/lib/supabase/fetcher.ex @@ -108,7 +108,7 @@ defmodule Supabase.Fetcher do url: Finch.Request.url(), options: Finch.request_opts(), service: Supabase.service(), - query: map, + query: list({String.t(), String.t()}), body_decoder: module, error_parser: module } @@ -119,9 +119,9 @@ defmodule Supabase.Fetcher do :client, :body, method: :get, - query: %{}, + query: [], options: [], - headers: %{}, + headers: [], body_decoder: Supabase.Fetcher.JSONDecoder, error_parser: Supabase.ErrorParser ] @@ -505,6 +505,68 @@ defmodule Supabase.Fetcher do get_header(resp, header) || default end + @doc """ + Tries to find and return the value for a query param, given it name, if it doesn't + existis, it returns the default value informed or `nil`. + """ + @spec get_query_param(t, param :: String.t(), default :: String.t() | nil) :: String.t() | nil + def get_query_param(%__MODULE__{} = builder, key, default \\ nil) + when is_binary(key) and (is_binary(default) or is_nil(default)) do + case List.keyfind(builder.query, key, 0) do + nil -> default + {^key, value} -> value + end + end + + @doc """ + Tries to find and return the value for a request headers, given it name, if it doesn't + existis, it returns the default value informed or `nil`. + + Do not confuse with `get_header/2`. + """ + @spec get_req_header(t, name :: String.t(), default :: String.t() | nil) :: String.t() | nil + def get_req_header(%__MODULE__{} = builder, key, default \\ nil) + when is_binary(key) and (is_binary(default) or is_nil(default)) do + case List.keyfind(builder.headers, key, 0) do + nil -> default + {^key, value} -> value + end + end + + @doc """ + Merges an existing query param value with a new one, prepending the new value + with the existing one. If no current value exists for the param, this function + will behave the same as `with_query/2`. + """ + @spec merge_query_param(t, param :: String.t(), value :: String.t(), + with: joinner :: String.t() + ) :: t + def merge_query_param(%__MODULE__{} = builder, key, value, [with: w] \\ [with: ","]) + when is_binary(key) and is_binary(value) and is_binary(w) do + if curr = get_query_param(builder, key) do + with_query(builder, %{key => Enum.join([curr, value], w)}) + else + with_query(builder, %{key => value}) + end + end + + @doc """ + Merges an existing request header value with a new one, prepending the new value + with the existing one. If no current value exists for the header, this function + will behave the same as `with_headers/2`. + """ + @spec merge_req_header(t, header :: String.t(), value :: String.t(), + with: joinner :: String.t() + ) :: t + def merge_req_header(%__MODULE__{} = builder, key, value, [with: w] \\ [with: ","]) + when is_binary(key) and is_binary(value) and is_binary(w) do + if curr = get_req_header(builder, key) do + with_headers(builder, %{key => Enum.join([curr, value], w)}) + else + with_headers(builder, %{key => value}) + end + end + defimpl Inspect, for: Supabase.Fetcher do import Inspect.Algebra diff --git a/test/supabase/fetcher_test.exs b/test/supabase/fetcher_test.exs index e905f14..d83453b 100644 --- a/test/supabase/fetcher_test.exs +++ b/test/supabase/fetcher_test.exs @@ -165,7 +165,116 @@ defmodule Supabase.FetcherTest do end end - # VCR and "integration" tests + describe "get_query_param/3" do + setup ctx do + fetcher = ctx.client |> Fetcher.new() |> Fetcher.with_query(%{"key1" => "value1", "key2" => "value2"}) + {:ok, Map.put(ctx, :fetcher, fetcher)} + end + + test "retrieves an existing query parameter", %{fetcher: fetcher} do + assert Fetcher.get_query_param(fetcher, "key1") == "value1" + end + + test "returns nil for a missing query parameter", %{fetcher: fetcher} do + assert Fetcher.get_query_param(fetcher, "key3") == nil + end + + test "returns the default value for a missing query parameter", %{fetcher: fetcher} do + assert Fetcher.get_query_param(fetcher, "key3", "default_value") == "default_value" + end + + test "handles empty query parameters gracefully", %{client: client} do + fetcher = Fetcher.new(client) + assert Fetcher.get_query_param(fetcher, "key") == nil + end + end + + describe "get_req_header/3" do + setup ctx do + fetcher = + Fetcher.new(ctx.client) + |> Fetcher.with_headers(%{ + "Authorization" => "Bearer token", + "Content-Type" => "application/json" + }) + + {:ok, fetcher: fetcher} + end + + test "retrieves an existing header", %{fetcher: fetcher} do + assert Fetcher.get_req_header(fetcher, "Authorization") == "Bearer token" + end + + test "returns nil for a missing header", %{fetcher: fetcher} do + assert Fetcher.get_req_header(fetcher, "Accept") == nil + end + + test "returns the default value for a missing header", %{fetcher: fetcher} do + assert Fetcher.get_req_header(fetcher, "Accept", "application/xml") == "application/xml" + end + + test "handles empty headers gracefully", %{client: client} do + fetcher = Fetcher.new(client) + assert Fetcher.get_req_header(fetcher, "Authorization") == nil + end + end + + describe "merge_query_param/4" do + setup ctx do + fetcher = Fetcher.new(ctx.client) |> Fetcher.with_query(%{"key1" => "value1"}) + {:ok, fetcher: fetcher} + end + + test "merges a new query parameter when key does not exist", %{fetcher: fetcher} do + updated_fetcher = Fetcher.merge_query_param(fetcher, "key2", "value2") + assert Fetcher.get_query_param(updated_fetcher, "key2") == "value2" + end + + test "merges with default separator when key exists", %{fetcher: fetcher} do + updated_fetcher = Fetcher.merge_query_param(fetcher, "key1", "value2") + assert Fetcher.get_query_param(updated_fetcher, "key1") == "value1,value2" + end + + test "merges with custom separator when specified", %{fetcher: fetcher} do + updated_fetcher = Fetcher.merge_query_param(fetcher, "key1", "value2", [with: "|"]) + assert Fetcher.get_query_param(updated_fetcher, "key1") == "value1|value2" + end + + test "handles merging into an empty query", %{client: client} do + fetcher = Fetcher.new(client) + updated_fetcher = Fetcher.merge_query_param(fetcher, "key1", "value1") + assert Fetcher.get_query_param(updated_fetcher, "key1") == "value1" + end + end + + describe "merge_req_header/4" do + setup ctx do + fetcher = Fetcher.new(ctx.client) |> Fetcher.with_headers(%{"key1" => "value1"}) + {:ok, fetcher: fetcher} + end + + test "merges a new header value when key does not exist", %{fetcher: fetcher} do + updated_fetcher = Fetcher.merge_req_header(fetcher, "key2", "value2") + assert Fetcher.get_req_header(updated_fetcher, "key2") == "value2" + end + + test "merges with default separator when key exists", %{fetcher: fetcher} do + updated_fetcher = Fetcher.merge_req_header(fetcher, "key1", "value2") + assert Fetcher.get_req_header(updated_fetcher, "key1") == "value1,value2" + end + + test "merges with custom separator when specified", %{fetcher: fetcher} do + updated_fetcher = Fetcher.merge_req_header(fetcher, "key1", "value2", [with: "|"]) + assert Fetcher.get_req_header(updated_fetcher, "key1") == "value1|value2" + end + + test "handles merging into an empty header", %{client: client} do + fetcher = Fetcher.new(client) + updated_fetcher = Fetcher.merge_req_header(fetcher, "key1", "value1") + assert Fetcher.get_req_header(updated_fetcher, "key1") == "value1" + end + end + defp have_header?(headers, name) do Enum.any?(headers, fn {k, _} -> String.downcase(k) == String.downcase(name)