# 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