Skip to content

Commit 887166f

Browse files
authored
Refactor HTTP client (#33)
1 parent 8982d87 commit 887166f

42 files changed

Lines changed: 1215 additions & 159 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.credo.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@
121121
#
122122
{Credo.Check.Refactor.Apply, []},
123123
{Credo.Check.Refactor.CondStatements, []},
124-
{Credo.Check.Refactor.CyclomaticComplexity, []},
124+
{Credo.Check.Refactor.CyclomaticComplexity, [max_complexity: 12]},
125125
{Credo.Check.Refactor.FilterCount, []},
126126
{Credo.Check.Refactor.FilterFilter, []},
127127
{Credo.Check.Refactor.FunctionArity, []},
@@ -130,7 +130,7 @@
130130
{Credo.Check.Refactor.MatchInCondition, []},
131131
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
132132
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
133-
{Credo.Check.Refactor.Nesting, [max_nesting: 3]},
133+
{Credo.Check.Refactor.Nesting, [max_nesting: 4]},
134134
{Credo.Check.Refactor.RedundantWithClauseResult, []},
135135
{Credo.Check.Refactor.RejectReject, []},
136136
{Credo.Check.Refactor.UnlessWithElse, []},

config/test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ config :open_api_typesense,
77
scheme: "http",
88
# see https://hexdocs.pm/req/Req.html#new/1
99
options: [
10+
max_retries: 0,
1011
retry: false
1112
]

coveralls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"deps",
55
"lib/open_api_typesense.ex",
66
"lib/open_api_typesense/schemas",
7+
"lib/open_api_typesense/converter.ex",
78
"lib/open_api_typesense/encoder.ex"
89
]
910
}

lib/open_api_typesense/client.ex

Lines changed: 46 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,12 @@ defmodule OpenApiTypesense.Client do
7878
if client do
7979
client.request(conn, params)
8080
else
81-
req_client = build_req_client(conn, params)
82-
req_request(req_client, params)
81+
{_req, resp} =
82+
conn
83+
|> build_req_client(params)
84+
|> Req.Request.run_request()
85+
86+
parse_resp(resp, params)
8387
end
8488
end
8589

@@ -105,100 +109,70 @@ defmodule OpenApiTypesense.Client do
105109
method: Access.get(opts, :method, :get),
106110
body: encode_body(opts),
107111
url: url,
108-
decode_json: [keys: :atoms]
112+
decode_body: false
109113
]
110114
|> Req.new()
111115
|> Req.Request.merge_options(req_options)
112116
|> Req.Request.put_header("x-typesense-api-key", Map.get(conn, :api_key))
113117
end
114118

115-
defp req_request(req_client, opts) do
116-
{_req, resp} = Req.Request.run_request(req_client)
117-
parse_resp(resp, opts)
118-
end
119-
120-
defp parse_resp(%Req.Response{status: code, body: list}, %{response: resp})
121-
when is_list(list) and code in 200..299 do
122-
response =
123-
Enum.find_value(resp, fn {status, [{mod, _t}]} ->
124-
if status === code do
125-
Enum.map(list, fn body ->
126-
struct(mod, body)
127-
end)
128-
end
129-
end)
130-
131-
{:ok, response}
132-
end
133-
134-
defp parse_resp(%Req.Response{status: code, body: body}, %{response: resp})
135-
when code in 200..299 do
136-
response =
137-
Enum.find_value(resp, fn resp ->
138-
case {resp, body} do
139-
{{status, {:string, :generic}}, ""} when status === code ->
140-
body
141-
142-
{{status, {mod, t}}, _} when status === code and t !== :generic ->
143-
struct(mod, body)
119+
defp encode_body(opts) do
120+
body = opts[:args][:body]
144121

145-
{{status, [{_mod, t}]}, nil} when status === code and t !== :generic ->
146-
[]
122+
case {opts[:request], body} do
123+
{nil, _} ->
124+
Jason.encode_to_iodata!(body)
147125

148-
{{status, :map}, _} when status === code ->
149-
body
126+
{[{"application/octet-stream", {:string, :generic}}], body} when not is_binary(body) ->
127+
Enum.map_join(body, "\n", &Jason.encode_to_iodata!/1)
150128

151-
{_, _} ->
152-
body
153-
|> String.splitter("\n")
154-
|> Enum.map(&Jason.decode!/1)
155-
end
156-
end)
129+
{[{"application/json", _}], body} when not is_binary(body) ->
130+
Jason.encode_to_iodata!(body)
157131

158-
{:ok, response}
132+
{_, body} when is_binary(body) ->
133+
body
134+
end
159135
end
160136

161137
defp parse_resp(%Req.Response{status: code, body: body}, %{response: resp}) do
162-
message =
163-
Enum.find_value(resp, fn {status, type} ->
164-
if status === code do
165-
{mod, _t} = type
166-
struct(mod, body)
167-
end
168-
end)
169-
170-
{:error, message}
138+
{_status, mod} = Enum.find(resp, fn {status, _} -> status === code end)
139+
parse_body(code, mod, body)
171140
end
172141

173142
defp parse_resp(error, _opts_resp) do
174143
{:error, Exception.message(error)}
175144
end
176145

177-
defp encode_body(opts) do
178-
if opts[:request] do
179-
[content_type] = opts[:request]
180-
parse_content_type(content_type, opts[:args][:body])
181-
else
182-
Jason.encode_to_iodata!(opts[:args][:body])
183-
end
146+
defp parse_body(code, {mod, :t}, body) when code in 400..499 do
147+
payload =
148+
body
149+
|> Jason.decode!()
150+
|> OpenApiTypesense.Converter.to_atom_keys()
151+
152+
{:error, struct(mod, payload)}
184153
end
185154

186-
defp parse_content_type({"application/octet-stream", {:string, :generic}}, body) do
187-
Enum.map_join(body, "\n", &Jason.encode_to_iodata!/1)
155+
defp parse_body(_code, [{_mod, _t}], "null") do
156+
{:ok, []}
188157
end
189158

190-
# defp parse_content_type({"application/json", {module, :t}}, body) when is_atom(module) do
191-
# atom_keys = Map.keys(mod.__struct__()) |> Enum.reject(&(&1 == :__struct__))
192-
# string_keys = Enum.map(atom_keys, &to_string/1)
193-
# keys = atom_keys ++ string_keys
159+
defp parse_body(_code, [{mod, _t}], body) when is_binary(body) do
160+
{:ok, Poison.decode!(body, as: [mod.__struct__()])}
161+
end
194162

195-
# body
196-
# |> Map.take(keys)
197-
# |> OpenApiTypesense.Converter.to_atom_keys(safe: false)
198-
# |> Jason.encode_to_iodata!()
199-
# end
163+
defp parse_body(_code, {:string, :generic}, "") do
164+
{:ok, ""}
165+
end
166+
167+
defp parse_body(_code, {:string, :generic}, body) do
168+
{:ok, String.split(body, "\n") |> Enum.map(&Jason.decode!/1)}
169+
end
170+
171+
defp parse_body(_code, :map, body) do
172+
{:ok, Jason.decode!(body, keys: :atoms)}
173+
end
200174

201-
defp parse_content_type({"application/json", _}, body) do
202-
Jason.encode_to_iodata!(body)
175+
defp parse_body(_code, {mod, _t}, body) do
176+
{:ok, Poison.decode!(body, as: mod.__struct__())}
203177
end
204178
end
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
defmodule OpenApiTypesense.Converter do
2+
@moduledoc """
3+
Converting your maps/structs that contains string or mix of atom keys into full atom keyed maps.
4+
"""
5+
6+
defstruct safe: true,
7+
underscore: true,
8+
high_perf: false,
9+
ignore: false
10+
11+
@spec to_atom_keys(
12+
map() | list() | tuple() | struct(),
13+
map() | list() | %__MODULE__{}
14+
) :: map() | list() | tuple()
15+
def to_atom_keys(value, opts \\ %{})
16+
17+
def to_atom_keys(%{__struct__: _type} = struct, %__MODULE__{} = opts) when is_struct(struct) do
18+
struct
19+
|> Map.from_struct()
20+
|> to_atom_keys(opts)
21+
end
22+
23+
def to_atom_keys(map, %__MODULE__{} = opts) when is_map(map) do
24+
Map.new(map, fn {k, v} -> {convert_key(k, opts), to_atom_keys(v, opts)} end)
25+
end
26+
27+
def to_atom_keys(list, %__MODULE__{} = opts) when is_list(list) do
28+
Enum.map(list, &to_atom_keys(&1, opts))
29+
end
30+
31+
def to_atom_keys(tuple, %__MODULE__{} = opts) when is_tuple(tuple) do
32+
tuple
33+
|> Tuple.to_list()
34+
|> to_atom_keys(opts)
35+
|> List.to_tuple()
36+
end
37+
38+
def to_atom_keys(val, opts = %__MODULE__{}) when is_struct(opts) do
39+
val
40+
end
41+
42+
def to_atom_keys(val, opts = %{}) when is_map(opts) do
43+
to_atom_keys(val, struct(__MODULE__, opts))
44+
end
45+
46+
def to_atom_keys(val, opts) when is_list(opts) do
47+
to_atom_keys(val, Enum.into(opts, %{}))
48+
end
49+
50+
defp convert_key(key, opts) do
51+
key
52+
|> as_underscore(opts.underscore, opts.high_perf)
53+
|> as_atom(opts.safe, opts.ignore)
54+
end
55+
56+
@spec as_atom(String.t() | atom(), safe? :: boolean(), ignore? :: boolean()) ::
57+
atom() | String.t()
58+
defp as_atom(key, true, true) when is_binary(key) do
59+
as_atom(key, true, false)
60+
rescue
61+
ArgumentError -> key
62+
end
63+
64+
defp as_atom(key, true, false) when is_binary(key) do
65+
String.to_existing_atom(key)
66+
end
67+
68+
defp as_atom(key, false, _) when is_binary(key) do
69+
String.to_atom(key)
70+
end
71+
72+
defp as_atom(key, _, _), do: key
73+
74+
@spec as_underscore(
75+
String.t() | atom(),
76+
underscore? :: boolean(),
77+
high_perf? :: boolean()
78+
) :: String.t()
79+
defp as_underscore(key, true, false) when is_binary(key) do
80+
underscore(key)
81+
end
82+
83+
defp as_underscore(key, true, false) when is_atom(key) do
84+
key
85+
|> Atom.to_string()
86+
|> as_underscore(true, false)
87+
end
88+
89+
defp as_underscore(key, true, true) when is_binary(key) do
90+
high_perf_underscore(key)
91+
end
92+
93+
defp as_underscore(key, true, true) when is_atom(key) do
94+
key
95+
|> Atom.to_string()
96+
|> as_underscore(true, true)
97+
end
98+
99+
defp as_underscore(key, _, _), do: key
100+
101+
@spec underscore(atom() | String.t()) :: String.t()
102+
defp underscore(key) do
103+
key
104+
|> Macro.underscore()
105+
|> String.replace(~r/-/, "_")
106+
end
107+
108+
@spec high_perf_underscore(String.t() | charlist()) :: String.t() | charlist()
109+
defp high_perf_underscore(key) when is_binary(key) do
110+
key
111+
|> String.to_charlist()
112+
|> high_perf_underscore()
113+
|> to_string()
114+
end
115+
116+
defp high_perf_underscore([h | t]) do
117+
[to_lower_char(h)] ++ to_underscore(t, h)
118+
end
119+
120+
defp high_perf_underscore(~c"") do
121+
~c""
122+
end
123+
124+
@spec to_underscore(charlist(), char()) :: charlist()
125+
defp to_underscore([h | t], prev) when prev == ?_ or prev == ?- do
126+
[to_lower_char(h)] ++ to_underscore(t, h)
127+
end
128+
129+
defp to_underscore([h0, h1 | t], _prev)
130+
when h0 >= ?A and h0 <= ?Z and not (h1 >= ?A and h1 <= ?Z) and h1 != ?_ and h1 != ?- do
131+
[?_, to_lower_char(h0), h1] ++ to_underscore(t, h1)
132+
end
133+
134+
defp to_underscore([h | t], prev)
135+
when h >= ?A and h <= ?Z and not (prev >= ?A and prev <= ?Z) and prev != ?_ and prev != ?- do
136+
[?_, to_lower_char(h)] ++ to_underscore(t, h)
137+
end
138+
139+
defp to_underscore([h | t], _prev) do
140+
[to_lower_char(h)] ++ to_underscore(t, h)
141+
end
142+
143+
defp to_underscore(~C"", _h) do
144+
~C""
145+
end
146+
147+
@spec to_lower_char(char()) :: char()
148+
defp to_lower_char(?-), do: ?_
149+
150+
defp to_lower_char(char) when char >= ?A and char <= ?Z do
151+
char + 32
152+
end
153+
154+
defp to_lower_char(char) do
155+
char
156+
end
157+
end

lib/open_api_typesense/operations/analytics.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ defmodule OpenApiTypesense.Analytics do
8686
method: :delete,
8787
response: [
8888
{200, {OpenApiTypesense.AnalyticsRuleDeleteResponse, :t}},
89+
{401, {OpenApiTypesense.ApiResponse, :t}},
8990
{404, {OpenApiTypesense.ApiResponse, :t}}
9091
],
9192
opts: opts

lib/open_api_typesense/operations/collections.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ defmodule OpenApiTypesense.Collections do
173173
method: :get,
174174
response: [
175175
{200, {OpenApiTypesense.CollectionResponse, :t}},
176+
{401, {OpenApiTypesense.ApiResponse, :t}},
176177
{404, {OpenApiTypesense.ApiResponse, :t}}
177178
],
178179
opts: opts

lib/open_api_typesense/operations/conversations.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ defmodule OpenApiTypesense.Conversations do
140140
request: [{"application/json", {OpenApiTypesense.ConversationModelUpdateSchema, :t}}],
141141
response: [
142142
{200, {OpenApiTypesense.ConversationModelSchema, :t}},
143+
{400, {OpenApiTypesense.ApiResponse, :t}},
143144
{401, {OpenApiTypesense.ApiResponse, :t}},
144145
{404, {OpenApiTypesense.ApiResponse, :t}}
145146
],

lib/open_api_typesense/operations/documents.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ defmodule OpenApiTypesense.Documents do
316316
request: [{"application/json", :map}],
317317
response: [
318318
{201, :map},
319+
{400, {OpenApiTypesense.ApiResponse, :t}},
319320
{401, {OpenApiTypesense.ApiResponse, :t}},
320321
{404, {OpenApiTypesense.ApiResponse, :t}},
321322
{409, {OpenApiTypesense.ApiResponse, :t}}
@@ -444,6 +445,7 @@ defmodule OpenApiTypesense.Documents do
444445
request: [{"application/json", :map}],
445446
response: [
446447
{200, :map},
448+
{400, {OpenApiTypesense.ApiResponse, :t}},
447449
{401, {OpenApiTypesense.ApiResponse, :t}},
448450
{404, {OpenApiTypesense.ApiResponse, :t}}
449451
],
@@ -514,6 +516,7 @@ defmodule OpenApiTypesense.Documents do
514516
request: [{"application/json", {OpenApiTypesense.SearchOverrideSchema, :t}}],
515517
response: [
516518
{200, {OpenApiTypesense.SearchOverride, :t}},
519+
{400, {OpenApiTypesense.ApiResponse, :t}},
517520
{401, {OpenApiTypesense.ApiResponse, :t}},
518521
{404, {OpenApiTypesense.ApiResponse, :t}}
519522
],

0 commit comments

Comments
 (0)