Current section
Files
Jump to
Current section
Files
lib/xdk.ex
# AUTO-GENERATED FILE - DO NOT EDIT
# This file was automatically generated by the XDK build tool.
# Any manual changes will be overwritten on the next generation.
defmodule Xdk do
@moduledoc """
Auto-generated X API client.
Uses Finch for HTTP transport and supports bearer token authentication.
"""
@version "0.1.0"
defstruct [
:base_url,
:finch,
:auth,
:headers,
:request_opts
]
@type auth ::
nil | {:bearer, String.t()} | {:oauth2, map()} | {:oauth1, OAuther.Credentials.t()}
@type t :: %__MODULE__{
base_url: String.t(),
finch: atom() | nil,
auth: auth(),
headers: [{String.t(), String.t()}],
request_opts: keyword()
}
@spec new(keyword()) :: t()
def new(opts \\ []) do
defaults = Application.get_env(:xdk, :default_config, [])
opts = Keyword.merge(defaults, opts)
%__MODULE__{
base_url: Keyword.get(opts, :base_url, "https://api.x.com"),
finch: Keyword.get(opts, :finch),
auth: resolve_auth(opts),
headers: Keyword.get(opts, :headers, []),
request_opts: Keyword.get(opts, :request_opts, [])
}
end
@doc """
Returns a child spec for a Finch pool suitable for the X API.
Add to your supervision tree:
children = [
Xdk.child_spec(name: MyApp.XFinch),
...
]
Then: `Xdk.new(finch: MyApp.XFinch, auth: {:bearer, token})`
"""
@spec child_spec(keyword()) :: Supervisor.child_spec()
def child_spec(opts) do
name = Keyword.fetch!(opts, :name)
pool_opts = Keyword.get(opts, :pools, %{})
Finch.child_spec(name: name, pools: pool_opts)
end
@spec request(t(), atom(), String.t(), keyword()) ::
{:ok, term()} | {:error, Xdk.Errors.error()}
def request(%__MODULE__{} = client, method, path, opts \\ []) do
params = Keyword.get(opts, :params, %{})
query = Keyword.get(opts, :query, [])
json = Keyword.get(opts, :json)
raw_body = Keyword.get(opts, :body)
url = client.base_url <> interpolate_path(path, params)
url =
case query do
[] -> url
_ -> url <> "?" <> Xdk.Query.encode(query)
end
headers = build_headers(client, method, url, opts)
{encoded_body, headers} =
cond do
json != nil ->
case Jason.encode(json) do
{:ok, bin} -> {bin, [{"content-type", "application/json"} | headers]}
{:error, err} -> throw({:encode_error, err})
end
raw_body != nil ->
{raw_body, headers}
true ->
{nil, headers}
end
finch = require_finch!(client)
method
|> Finch.build(url, headers, encoded_body)
|> Finch.request(finch, client.request_opts)
|> handle_response()
catch
{:encode_error, err} ->
{:error, Xdk.Errors.DecodeError.exception(error: err, raw_body: nil)}
end
@doc false
def require_finch!(%__MODULE__{finch: nil}) do
raise ArgumentError, """
Xdk requires a Finch pool name.
Start Finch in your application supervision tree:
children = [
{Finch, name: MyApp.Finch}
]
Then create a client:
client = Xdk.new(finch: MyApp.Finch, auth: {:bearer, token})
Or set a default via application config:
config :xdk, :default_config, finch: MyApp.Finch
"""
end
def require_finch!(%__MODULE__{finch: finch}), do: finch
@doc false
def build_finch_request(%__MODULE__{} = client, method, path, opts) do
params = Keyword.get(opts, :params, %{})
query = Keyword.get(opts, :query, [])
url = client.base_url <> interpolate_path(path, params)
url =
case query do
[] -> url
_ -> url <> "?" <> Xdk.Query.encode(query)
end
headers = build_headers(client, method, url, opts)
Finch.build(method, url, headers)
end
defp interpolate_path(path, params) do
Enum.reduce(params, path, fn {k, v}, acc ->
String.replace(acc, "{#{k}}", URI.encode_www_form(to_string(v)))
end)
end
defp resolve_auth(opts) do
case Keyword.get(opts, :auth) do
nil ->
case Keyword.get(opts, :bearer) do
nil -> nil
token -> {:bearer, token}
end
auth ->
auth
end
end
defp build_headers(%__MODULE__{} = client, method, url, opts) do
per_request_auth = Keyword.get(opts, :auth, client.auth)
[{"user-agent", "xdk-elixir/#{@version}"}]
|> Kernel.++(client.headers)
|> Kernel.++(Keyword.get(opts, :headers, []))
|> Kernel.++(auth_headers(per_request_auth, method, url))
end
defp auth_headers(nil, _method, _url), do: []
defp auth_headers({:bearer, token}, _method, _url), do: [{"authorization", "Bearer #{token}"}]
defp auth_headers({:oauth2, %{access_token: token}}, _method, _url),
do: [{"authorization", "Bearer #{token}"}]
defp auth_headers({:oauth1, credentials}, method, url) do
{uri, query_string} =
case String.split(url, "?", parts: 2) do
[base] -> {base, ""}
[base, qs] -> {base, qs}
end
query_params =
query_string
|> URI.decode_query()
|> Enum.to_list()
signed_params = OAuther.sign(to_string(method), uri, query_params, credentials)
{header, _params} = OAuther.header(signed_params)
[header]
end
defp handle_response({:ok, %Finch.Response{status: 429, headers: headers, body: body}}) do
decoded = try_decode_body(body, headers)
{limit, remaining, reset, retry_after} = parse_rate_limit_headers(headers)
{:error,
Xdk.Errors.RateLimitError.exception(
status: 429,
body: decoded,
headers: headers,
limit: limit,
remaining: remaining,
reset: reset,
retry_after_ms: retry_after
)}
end
defp handle_response({:ok, %Finch.Response{status: status, body: body, headers: headers}})
when status in 200..299 do
case try_decode_body(body, headers) do
{:ok, decoded} -> {:ok, decoded}
{:error, err} -> {:error, Xdk.Errors.DecodeError.exception(error: err, raw_body: body)}
end
end
defp handle_response({:ok, %Finch.Response{status: status, body: body, headers: headers}}) do
decoded =
case try_decode_body(body, headers) do
{:ok, d} -> d
_ -> body
end
{:error, Xdk.Errors.ApiError.exception(status: status, body: decoded, headers: headers)}
end
defp handle_response({:error, reason}) do
{:error, Xdk.Errors.TransportError.exception(reason: reason)}
end
defp try_decode_body("", _), do: {:ok, nil}
defp try_decode_body(nil, _), do: {:ok, nil}
defp try_decode_body(body, headers) do
content_type =
Enum.find_value(headers, "", fn
{k, v} when k in ["content-type", "Content-Type"] -> v
_ -> nil
end)
if String.contains?(content_type, "application/json") do
Jason.decode(body)
else
{:ok, body}
end
end
defp parse_rate_limit_headers(headers) do
get = fn name ->
Enum.find_value(headers, fn
{k, v} when k == name -> v
_ -> nil
end)
end
limit = parse_int(get.("x-rate-limit-limit"))
remaining = parse_int(get.("x-rate-limit-remaining"))
reset = parse_int(get.("x-rate-limit-reset"))
retry_after =
case reset do
nil -> nil
unix -> max(0, (unix - System.system_time(:second)) * 1000)
end
{limit, remaining, reset, retry_after}
end
defp parse_int(nil), do: nil
defp parse_int(s) when is_binary(s) do
case Integer.parse(s) do
{n, _} -> n
:error -> nil
end
end
end