Skip to content

Commit a89717a

Browse files
authored
Refactor default Req client (#25)
This commit refactors the existing default Req HTTP client to provide full support for all options documented in https://hexdocs.pm/req/Req.html#new/1. Key changes: - the client now accepts all options defined in Req.new/1 - global configuration can be set in `config.exs` - per-function call overrides are possible using the `req` argument - the Req client configuration has been deprecated and moved from the Client module to the Connection module
1 parent 15d6b32 commit a89717a

9 files changed

Lines changed: 123 additions & 55 deletions

File tree

config/dev.exs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ config :open_api_typesense,
44
api_key: "xyz",
55
host: "localhost",
66
port: 8108,
7-
scheme: "http"
7+
scheme: "http",
8+
# see https://hexdocs.pm/req/Req.html#new/1
9+
options: [
10+
retry: false
11+
]
812

913
config :oapi_generator,
1014
default: [

config/test.exs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@ config :open_api_typesense,
55
host: "localhost",
66
port: 8108,
77
scheme: "http",
8-
max_retries: 0,
9-
retry: false
8+
# see https://hexdocs.pm/req/Req.html#new/1
9+
options: [
10+
retry: false
11+
]

lib/open_api_typesense/client.ex

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,24 @@ defmodule OpenApiTypesense.Client do
2323

2424
@doc since: "0.2.0"
2525
@spec get_host :: String.t() | nil
26+
@deprecated "Use OpenApiTypesense.Connection.config(:host) instead"
2627
def get_host, do: Application.get_env(:open_api_typesense, :host)
2728

2829
@doc since: "0.2.0"
2930
@spec get_scheme :: String.t() | nil
31+
@deprecated "Use OpenApiTypesense.Connection.config(:scheme) instead"
3032
def get_scheme, do: Application.get_env(:open_api_typesense, :scheme)
3133

3234
@doc since: "0.2.0"
3335
@spec get_port :: non_neg_integer() | nil
36+
@deprecated "Use OpenApiTypesense.Connection.config(:port) instead"
3437
def get_port, do: Application.get_env(:open_api_typesense, :port)
3538

3639
@doc since: "0.5.0"
3740
@spec get_client :: keyword() | nil
41+
@deprecated "Use OpenApiTypesense.Connection.config(:client) instead"
3842
def get_client, do: Application.get_env(:open_api_typesense, :client)
3943

40-
defp test_max_retries, do: Application.get_env(:open_api_typesense, :max_retries)
41-
defp test_retry, do: Application.get_env(:open_api_typesense, :retry)
42-
4344
@doc """
4445
Returns the Typesense's API key
4546
@@ -90,40 +91,42 @@ defmodule OpenApiTypesense.Client do
9091
if client do
9192
client.request(conn, opts)
9293
else
93-
default_client(conn, opts)
94+
req_client = build_req_client(conn, opts)
95+
req_request(req_client, opts)
9496
end
9597
end
9698

97-
defp default_client(conn, opts) do
98-
# Req.Request.append_error_steps and its retry option are used here.
99-
# options like retry, max_retries, etc. can be found in:
100-
# https://hexdocs.pm/req/Req.Steps.html#retry/1
101-
# NOTE: look at source code in Github
99+
def build_req_client(conn, opts) do
100+
# Default request options. These can be overridden in this hierarchy:
101+
# 1. Via the `:options` key in `config.exs`.
102+
# 2. By passing `:req` key to `opts` arg to request/2.
103+
req_options =
104+
conn
105+
|> Map.get(:options, [])
106+
|> Keyword.merge(Access.get(opts, :req, []))
107+
102108
url =
103109
%URI{
104110
scheme: conn.scheme,
105111
host: conn.host,
106112
port: conn.port,
107-
path: opts[:url],
108-
query: URI.encode_query(opts[:query] || [])
113+
path: Access.get(opts, :url),
114+
query: URI.encode_query(Access.get(opts, :query, []))
109115
}
110116

111-
{_req, resp} =
112-
[
113-
method: opts[:method] || :get,
114-
body: encode_body(opts),
115-
url: url,
116-
max_retries: test_max_retries() || opts[:req][:max_retries] || 3,
117-
retry: test_retry() || opts[:req][:retry] || :safe_transient,
118-
compress_body: opts[:req][:compress] || false,
119-
cache: opts[:req][:cache] || false,
120-
decode_json: [keys: :atoms]
121-
]
122-
|> Req.new()
123-
|> Req.Request.merge_options(Map.to_list(Map.get(conn, :options, %{})))
124-
|> Req.Request.put_header("x-typesense-api-key", Map.get(conn, :api_key))
125-
|> Req.Request.run_request()
117+
[
118+
method: Access.get(opts, :method, :get),
119+
body: encode_body(opts),
120+
url: url,
121+
decode_json: [keys: :atoms]
122+
]
123+
|> Req.new()
124+
|> Req.Request.merge_options(req_options)
125+
|> Req.Request.put_header("x-typesense-api-key", Map.get(conn, :api_key))
126+
end
126127

128+
defp req_request(req_client, opts) do
129+
{_req, resp} = Req.Request.run_request(req_client)
127130
parse_resp(resp, opts[:response])
128131
end
129132

lib/open_api_typesense/connection.ex

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,25 @@ defmodule OpenApiTypesense.Connection do
55
Fetches credentials either from application env or map.
66
"""
77

8-
alias OpenApiTypesense.Client
9-
108
@derive {Inspect, except: [:api_key]}
11-
defstruct [:host, :api_key, :port, :scheme, :client]
9+
defstruct [
10+
:host,
11+
:api_key,
12+
:port,
13+
:scheme,
14+
:client,
15+
# see https://hexdocs.pm/req/Req.html#new/1
16+
options: []
17+
]
1218

1319
@typedoc since: "0.2.0"
1420
@type t() :: %{
1521
host: binary() | nil,
1622
api_key: binary() | nil,
1723
port: non_neg_integer() | nil,
1824
scheme: binary() | nil,
19-
client: list() | nil
25+
client: list() | nil,
26+
options: list()
2027
}
2128

2229
@doc """
@@ -49,15 +56,16 @@ defmodule OpenApiTypesense.Connection do
4956

5057
@doc since: "0.2.0"
5158
@spec new(t() | map()) :: %__MODULE__{}
52-
def new(%__MODULE__{} = connection) when is_struct(connection) do
53-
connection
59+
def new(%__MODULE__{} = conn) when is_struct(conn, __MODULE__) do
60+
conn
5461
end
5562

56-
def new(connection) when is_map(connection) do
57-
missing_fields = Enum.sort(required_fields() -- Map.keys(connection))
63+
def new(conn) when is_map(conn) do
64+
missing_fields = Enum.sort(required_fields() -- Map.keys(conn))
5865

5966
if missing_fields == [] do
60-
struct(__MODULE__, connection)
67+
fields = Map.merge(defaults(), conn)
68+
struct(__MODULE__, fields)
6169
else
6270
raise ArgumentError, "Missing required fields: #{inspect(missing_fields)}"
6371
end
@@ -75,19 +83,29 @@ defmodule OpenApiTypesense.Connection do
7583
# another HTTP client. See README.
7684
__MODULE__
7785
|> struct(%{})
78-
|> Map.drop([:__struct__])
79-
|> Map.drop([:client])
86+
|> Map.drop([:__struct__, :client, :options])
8087
|> Map.keys()
8188
end
8289

8390
@spec defaults :: map()
8491
defp defaults do
8592
%{
86-
host: Client.get_host(),
87-
api_key: Client.api_key(),
88-
port: Client.get_port(),
89-
scheme: Client.get_scheme(),
90-
client: Client.get_client()
93+
host: config(:host),
94+
api_key: config(:api_key),
95+
port: config(:port),
96+
scheme: config(:scheme),
97+
client: config(:client),
98+
options: config(:options, [])
9199
}
92100
end
101+
102+
@spec config(atom()) :: any()
103+
def config(key, default \\ nil) do
104+
Access.get(config(), key, default)
105+
end
106+
107+
@spec config :: list()
108+
def config do
109+
Application.get_all_env(:open_api_typesense)
110+
end
93111
end

lib/open_api_typesense/operations/health.ex

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ defmodule OpenApiTypesense.Health do
1414
"""
1515

1616
@spec health :: {:ok, OpenApiTypesense.HealthStatus.t()} | :error
17-
def health, do: health(Connection.new())
17+
def health do
18+
health(Connection.new(), [])
19+
end
1820

1921
@doc """
2022
Either one of:
@@ -24,12 +26,12 @@ defmodule OpenApiTypesense.Health do
2426
"""
2527
@spec health(Connection.t() | map() | keyword()) ::
2628
{:ok, OpenApiTypesense.HealthStatus.t()} | :error
27-
def health(opts) when is_list(opts) do
28-
health(Connection.new(), opts)
29+
def health(conn) when is_struct(conn, Connection) do
30+
health(conn, [])
2931
end
3032

31-
def health(conn) do
32-
health(conn, [])
33+
def health(conn) when is_map(conn) do
34+
health(Connection.new(conn), [])
3335
end
3436

3537
@doc """

test/connection_test.exs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ defmodule ConnectionTest do
1717
api_key: "xyz",
1818
host: "localhost",
1919
port: 8108,
20-
scheme: "http"
20+
scheme: "http",
21+
options: [retry: false]
2122
}
2223
end
2324

@@ -35,7 +36,8 @@ defmodule ConnectionTest do
3536
api_key: "myapikey",
3637
host: "otherhost",
3738
port: 9200,
38-
scheme: "https"
39+
scheme: "https",
40+
options: [retry: false]
3941
}
4042
end
4143

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule ClientTest do
1+
defmodule CustomClientTest do
22
use ExUnit.Case, async: false
33
require Logger
44

@@ -110,7 +110,6 @@ defmodule ClientTest do
110110
assert CustomClient === Application.get_env(:open_api_typesense, :client)
111111

112112
assert {:ok, %{"ok" => true}} = OpenApiTypesense.Health.health()
113-
assert {:ok, %{"ok" => true}} = OpenApiTypesense.Health.health([])
114113
assert {:ok, %{"ok" => true}} = OpenApiTypesense.Health.health(conn)
115114
assert {:ok, %{"ok" => true}} = OpenApiTypesense.Health.health(map_conn)
116115
assert {:ok, %{"ok" => true}} = OpenApiTypesense.Health.health(conn, [])

test/default_client_test.exs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
defmodule DefaultClientTest do
2+
use ExUnit.Case, async: false
3+
4+
alias OpenApiTypesense.Client
5+
alias OpenApiTypesense.Connection
6+
7+
require Logger
8+
9+
describe "request/2" do
10+
@tag ["27.1": true, "27.0": true, "26.0": true]
11+
test "default to req http client if no custom client set" do
12+
conn = Connection.new()
13+
14+
opts = %{
15+
args: [],
16+
call: {OpenApiTypesense.Health, :health},
17+
url: "/health",
18+
method: :get,
19+
response: [{200, {OpenApiTypesense.HealthStatus, :t}}],
20+
opts: []
21+
}
22+
23+
assert {:ok, %OpenApiTypesense.HealthStatus{ok: true}} = Client.request(conn, opts)
24+
end
25+
end
26+
27+
describe "build_req_client/2" do
28+
test "default req options" do
29+
req = Client.build_req_client(Connection.new(), [])
30+
assert req.headers == %{"x-typesense-api-key" => ["xyz"]}
31+
assert req.options == %{decode_json: [keys: :atoms], retry: false}
32+
end
33+
34+
test "override req options through req field" do
35+
req = Client.build_req_client(Connection.new(), req: [retry: true, cache: false])
36+
assert req.options == %{decode_json: [keys: :atoms], cache: false, retry: true}
37+
end
38+
end
39+
end

test/operations/health_test.exs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ defmodule HealthTest do
1515
@tag ["27.1": true, "27.0": true, "26.0": true]
1616
test "success: health check", %{conn: conn, map_conn: map_conn} do
1717
assert {:ok, %HealthStatus{ok: true}} = Health.health()
18-
assert {:ok, %HealthStatus{ok: true}} = Health.health([])
1918
assert {:ok, %HealthStatus{ok: true}} = Health.health(conn)
2019
assert {:ok, %HealthStatus{ok: true}} = Health.health(map_conn)
2120
assert {:ok, %HealthStatus{ok: true}} = Health.health(conn, [])

0 commit comments

Comments
 (0)