11defmodule OpenApiTypesense.Client do
2- @ moduledoc since: "0.1 .0"
2+ @ moduledoc since: "0.2 .0"
33 @ moduledoc """
44 Http client for Typesense server.
55 """
66
77 alias OpenApiTypesense.Connection
88
9- @ typedoc since: "0.1.0"
10- @ type request_body ( ) :: iodata ( ) | nil
9+ @ typedoc since: "0.2.0"
10+ @ type response ( ) ::
11+ { :ok , any ( ) }
12+ | { :error , OpenApiTypesense.ApiResponse . t ( ) }
13+ | { :error , String . t ( ) }
14+ | :error
1115
12- @ typedoc since: "0.1.0"
13- @ type request_method ( ) :: :get | :post | :delete | :patch | :put
14-
15- @ typedoc since: "0.1.0"
16- @ type request_path ( ) :: String . t ( )
17-
18- @ doc since: "0.1.0"
16+ @ doc since: "0.2.0"
1917 @ spec get_host :: String . t ( ) | nil
2018 def get_host , do: Application . get_env ( :open_api_typesense , :host )
2119
22- @ doc since: "0.1 .0"
20+ @ doc since: "0.2 .0"
2321 @ spec get_scheme :: String . t ( ) | nil
2422 def get_scheme , do: Application . get_env ( :open_api_typesense , :scheme )
2523
26- @ doc since: "0.1 .0"
24+ @ doc since: "0.2 .0"
2725 @ spec get_port :: non_neg_integer ( ) | nil
2826 def get_port do
2927 Application . get_env ( :open_api_typesense , :port )
@@ -38,87 +36,129 @@ defmodule OpenApiTypesense.Client do
3836 > function will still return the key and accessible inside
3937 > shell (assuming bad actors [pun unintended `:/`] can get in as well).
4038 """
41- @ doc since: "0.1 .0"
39+ @ doc since: "0.2 .0"
4240 @ spec api_key :: String . t ( ) | nil
4341 def api_key , do: Application . get_env ( :open_api_typesense , :api_key )
4442
45- def run ( opts \\ % { } ) do
46- conn = Connection . new ( )
43+ @ doc """
44+ Command for making http requests.
45+
46+ > #### On using this function {: .info}
47+ > Functions e.g. `OpenApiTypesense.Health.health` don't need to explicitly pass
48+ > a `connection` unless you want to use custom `connection`. See [`README`](/README.md) for
49+ > more details or `OpenApiTypesense.Connection` module.
50+
51+ ## Options
52+
53+ - `:body`: Payload for passing as request body (defaults to `nil`).
54+ - `:url`: Request path.
55+ - `:method`: Request method (e.g. `:get`, `:post`, `:put`, `:patch`, `:delete`). Defaults to `:get`.
56+ - `:query`: Request query params (defaults to `%{}`).
57+
58+ ## Examples
59+ iex> alias OpenApiTypesense.Client
60+
61+ iex> connection = %OpenApiTypesense.Connection{
62+ ...> host: "localhost",
63+ ...> api_key: "some_api_key",
64+ ...> port: "8108",
65+ ...> scheme: "http"
66+ ...> }
67+
68+ iex> opts = %{
69+ ...> args: [],
70+ ...> call: {OpenApiTypesense.Health, :health},
71+ ...> url: "/health",
72+ ...> method: :get,
73+ ...> response: [{200, {OpenApiTypesense.HealthStatus, :t}}],
74+ ...> opts: opts
75+ ...> }
76+
77+ iex> Client.request(connection, opts)
78+ {:ok, %OpenApiTypesense.HealthStatus{ok: true}}
79+ """
80+ @ doc since: "0.2.0"
81+ @ spec request ( Connection . t ( ) , map ( ) ) :: response ( )
82+ def request ( conn , opts \\ % { } ) do
4783 # Req.Request.append_error_steps and its retry option are used here.
4884 # options like retry, max_retries, etc. can be found in:
4985 # https://hexdocs.pm/req/Req.Steps.html#retry/1
5086 # NOTE: look at source code in Github
51- retry = fn request ->
87+ retry =
5288 if Mix . env ( ) === :test do
53- { req , resp_or_err } = request
54-
5589 # disabled in order to cut time in tests
56- req = % { req | options: % { retry: false } }
90+ false
91+ else
92+ :safe_transient
93+ end
5794
58- Req.Steps . retry ( { req , resp_or_err } )
95+ max_retries =
96+ if Mix . env ( ) === :test do
97+ # disabled in order to cut time in tests
98+ 0
5999 else
60- Req.Steps . retry ( request )
100+ # default
101+ 3
61102 end
62- end
63103
64104 url =
65105 % URI {
66106 scheme: conn . scheme ,
67107 host: conn . host ,
68108 port: conn . port ,
69- path: opts [ :path ] ,
70- query: URI . encode_query ( opts [ :query ] || % { } )
109+ path: opts [ :url ] ,
110+ query: URI . encode_query ( opts [ :opts ] || % { } )
71111 }
72112
73- response =
74- % Req.Request {
75- body: opts [ :body ] || nil ,
113+ { _req , resp } =
114+ [
76115 method: opts [ :method ] || :get ,
77- url: url
78- }
79- |> Req.Request . put_header ( "x-typesense-api-key" , HttpClient . api_key ( ) )
80- |> Req.Request . put_header ( "content-type" , opts [ :content_type ] || "application/json" )
81- |> Req.Request . append_error_steps ( retry: retry )
82- |> Req.Request . run! ( )
83-
84- case response . status in 200 .. 299 do
85- true ->
86- body =
87- if opts [ :content_type ] === "text/plain" ,
88- do: String . split ( response . body , "\n " , trim: true ) |> Enum . map ( & Jason . decode! / 1 ) ,
89- else: Jason . decode! ( response . body )
90-
91- { :ok , body }
92-
93- false ->
94- { :error , Jason . decode! ( response . body ) [ "message" ] }
95- end
96-
97- # @spec request(map) :: {:ok, term} | {:error, term} | Operation.t()
98- # def request(%{opts: opts} = info) do
99- # %Operation{
100- # request_method: method,
101- # request_server: server,
102- # request_url: url
103- # } = operation = Operation.new(info)
104-
105- # metadata = %{
106- # client: __MODULE__,
107- # info: info,
108- # request_method: method,
109- # request_server: server,
110- # request_url: url
111- # }
112-
113- # :telemetry.span([:oapi_github, :request], metadata, fn ->
114- # result = reduce_stack(operation)
115-
116- # if Config.wrap(opts) do
117- # wrap_result(result, metadata)
118- # else
119- # {result, Map.put(metadata, :response_code, 0)}
120- # end
121- # end)
122- # end
116+ body: opts [ :body ] || nil ,
117+ url: url ,
118+ retry: retry ,
119+ max_retries: max_retries ,
120+ decode_json: [ keys: :atoms! ]
121+ ]
122+ |> Req . new ( )
123+ |> Req.Request . put_header ( "x-typesense-api-key" , api_key ( ) )
124+ |> Req.Request . run_request ( )
125+
126+ parse_resp ( resp , opts . response )
127+ end
128+
129+ defp parse_resp ( % Req.TransportError { } = error , _opts_resp ) do
130+ { :error , Exception . message ( error ) }
131+ end
132+
133+ defp parse_resp ( % Req.HTTPError { } = error , _opts_resp ) do
134+ { :error , Exception . message ( error ) }
135+ end
136+
137+ defp parse_resp ( resp , opts_resp ) do
138+ { code , values } =
139+ opts_resp
140+ |> Enum . find ( fn { code , _values } ->
141+ code === resp . status
142+ end )
143+
144+ parse_values ( code , values , resp . body )
145+ end
146+
147+ defp parse_values ( code , values , body ) when is_list ( values ) do
148+ status = if code in 200 .. 299 , do: :ok , else: :error
149+
150+ resp =
151+ values
152+ |> Enum . map ( fn { module , _func_name } ->
153+ struct ( module , body )
154+ end )
155+
156+ { status , resp }
157+ end
158+
159+ defp parse_values ( code , { module , _func_name } = _values , body ) do
160+ status = if code in 200 .. 299 , do: :ok , else: :error
161+
162+ { status , struct ( module , body ) }
123163 end
124164end
0 commit comments