diff --git a/lib/knock/api.ex b/lib/knock/api.ex index a187553..e3dbd84 100644 --- a/lib/knock/api.ex +++ b/lib/knock/api.ex @@ -84,17 +84,18 @@ defmodule Knock.Api do def library_version, do: @lib_version defp http_client(config, opts \\ []) do - middleware = [ - {Tesla.Middleware.BaseUrl, config.host <> "/v1"}, - {Tesla.Middleware.JSON, engine: config.json_client}, - {Tesla.Middleware.Headers, - [ - {"Authorization", "Bearer " <> config.api_key}, - {"User-Agent", "knocklabs/knock-elixir@#{library_version()}"} - ] ++ - maybe_idempotency_key_header(Map.new(opts)) ++ - maybe_branch_header(config)} - ] + middleware = + [ + {Tesla.Middleware.BaseUrl, config.host <> "/v1"}, + {Tesla.Middleware.JSON, engine: config.json_client}, + {Tesla.Middleware.Headers, + [ + {"Authorization", "Bearer " <> config.api_key}, + {"User-Agent", "knocklabs/knock-elixir@#{library_version()}"} + ] ++ + maybe_idempotency_key_header(Map.new(opts)) ++ + maybe_branch_header(config)} + ] ++ maybe_additional_middlewares(config) Tesla.client(middleware, config.adapter) end @@ -108,4 +109,10 @@ defmodule Knock.Api do do: [{"X-Knock-Branch", to_string(branch)}] defp maybe_branch_header(_), do: [] + + defp maybe_additional_middlewares(%{additional_middlewares: middlewares}) + when is_list(middlewares), + do: middlewares + + defp maybe_additional_middlewares(_), do: [] end diff --git a/lib/knock/client.ex b/lib/knock/client.ex index e7994f4..c58027b 100644 --- a/lib/knock/client.ex +++ b/lib/knock/client.ex @@ -10,7 +10,29 @@ defmodule Knock.Client do # With optional branch client = Knock.Client.new(api_key: "sk_test_12345", branch: "my-feature-branch") + + # With custom Tesla middleware + client = Knock.Client.new( + api_key: "sk_test_12345", + additional_middlewares: [ + {Tesla.Middleware.Logger, level: :debug, filter_headers: ["Authorization"]}, + Tesla.Middleware.Retry + ] + ) ``` + + ### Custom middleware + + You can add custom Tesla middleware to the HTTP client by passing the `:additional_middlewares` + option when creating a client. This is useful for adding logging, retry logic, or other + custom behavior to HTTP requests without modifying the library itself. + + Middleware can be specified as either: + - A module atom: `Tesla.Middleware.Retry` + - A tuple with module and options: `{Tesla.Middleware.Logger, level: :debug, filter_headers: ["Authorization"]}` + + The additional middlewares are appended to the end of the middleware chain, after the + built-in middlewares (BaseUrl, JSON, and Headers). """ @enforce_keys [:api_key] @@ -18,7 +40,8 @@ defmodule Knock.Client do api_key: nil, branch: nil, adapter: Tesla.Adapter.Hackney, - json_client: Jason + json_client: Jason, + additional_middlewares: [] @typedoc """ Describes a Knock client @@ -28,7 +51,8 @@ defmodule Knock.Client do api_key: String.t(), branch: String.t() | nil, adapter: atom(), - json_client: atom() + json_client: atom(), + additional_middlewares: [module() | {module(), any()}] } @doc """ @@ -43,7 +67,7 @@ defmodule Knock.Client do opts = opts - |> Keyword.take([:host, :api_key, :branch, :adapter, :json_client]) + |> Keyword.take([:host, :api_key, :branch, :adapter, :json_client, :additional_middlewares]) |> Map.new() |> maybe_set_adapter_default() diff --git a/test/knock/api_test.exs b/test/knock/api_test.exs new file mode 100644 index 0000000..a678b1e --- /dev/null +++ b/test/knock/api_test.exs @@ -0,0 +1,53 @@ +defmodule Knock.ApiTest do + use ExUnit.Case + + alias Knock.Client + alias Knock.Api + @moduletag capture_log: true + + describe "http_client with additional_middlewares" do + test "includes custom middleware in the Tesla client" do + client = + Client.new( + api_key: "sk_test_12345", + additional_middlewares: [ + {Tesla.Middleware.Logger, level: :debug}, + Tesla.Middleware.Retry + ] + ) + + # Create the Tesla client through a private function call + # We'll use the get/3 function which internally calls http_client + # and verify the client structure + _tesla_client = + client + |> Api.get("/test") + |> case do + {:error, _} -> :ok + _ -> :ok + end + + # The test mainly ensures no errors occur when additional_middlewares are set + assert client.additional_middlewares == [ + {Tesla.Middleware.Logger, level: :debug}, + Tesla.Middleware.Retry + ] + end + + test "works with module-only middleware specification" do + client = + Client.new( + api_key: "sk_test_12345", + additional_middlewares: [Tesla.Middleware.Retry] + ) + + assert client.additional_middlewares == [Tesla.Middleware.Retry] + end + + test "works without additional middlewares" do + client = Client.new(api_key: "sk_test_12345") + + assert client.additional_middlewares == [] + end + end +end diff --git a/test/knock_test.exs b/test/knock_test.exs index 900fd2e..a1bd0af 100644 --- a/test/knock_test.exs +++ b/test/knock_test.exs @@ -82,5 +82,27 @@ defmodule KnockTest do assert knock.adapter == Tesla.Adapter.Mint end + + test "it can accept additional middlewares as a list" do + knock = + TestClient.client( + api_key: "sk_test_12345", + additional_middlewares: [ + {Tesla.Middleware.Logger, level: :debug}, + Tesla.Middleware.Retry + ] + ) + + assert knock.additional_middlewares == [ + {Tesla.Middleware.Logger, level: :debug}, + Tesla.Middleware.Retry + ] + end + + test "it defaults to empty list for additional middlewares" do + knock = TestClient.client(api_key: "sk_test_12345") + + assert knock.additional_middlewares == [] + end end end