Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions lib/knock/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
30 changes: 27 additions & 3 deletions lib/knock/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,38 @@ 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]
defstruct host: "https://api.knock.app",
api_key: nil,
branch: nil,
adapter: Tesla.Adapter.Hackney,
json_client: Jason
json_client: Jason,
additional_middlewares: []

@typedoc """
Describes a Knock client
Expand All @@ -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 """
Expand All @@ -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()

Expand Down
53 changes: 53 additions & 0 deletions test/knock/api_test.exs
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions test/knock_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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