defmodule Stripe do use Stripe.OpenApi, path: [:code.priv_dir(:striped), "openapi", "spec3.sdk.json"] |> Path.join(), base_url: "https://api.stripe.com" @external_resource "README.md" @moduledoc @external_resource |> File.read!() |> String.split("") |> Enum.fetch!(1) |> String.replace("__VERSION__", @version) @doc """ Perform Stripe API requests. """ @spec request( method :: binary(), path :: binary(), client :: Stripe.t(), params :: map(), opts :: Keyword.t() ) :: {:ok, term} | {:error, Stripe.ApiErrors.t()} | {:error, term()} def request(method, path, client, params, opts \\ []) def request(method, path, client, params, opts) when method in [:get, :delete] do query = (params || %{}) |> UriQuery.params() |> URI.encode_query() url = URI.parse(client.base_url <> path) |> URI.append_query(query) |> URI.to_string() headers = build_headers(client) attempts = 1 do_request(client, method, url, headers, "", attempts, opts) |> Tuple.delete_at(2) end def request(:post = method, path, client, params, opts) do url = client.base_url <> path headers = client |> build_headers() |> maybe_concat( ["Idempotency-Key: #{generate_idempotency_key()}"], client.idempotency_key == nil ) body = (params || %{}) |> UriQuery.params() |> URI.encode_query() attempts = 1 do_request(client, method, url, headers, body, attempts, opts) |> Tuple.delete_at(2) end defp do_request(client, method, url, headers, body, attempts, opts) do telemetry_metadata = %{attempt: attempts, method: method, url: url} http_opts = opts[:http_opts] || [] Stripe.Telemetry.span(:request, telemetry_metadata, fn -> result = case client.http_client.request(method, url, headers, body, http_opts) do {:ok, resp} -> decoded_body = Jason.decode!(resp.body) if should_retry?(resp, attempts, client.max_network_retries, decoded_body) do do_request(client, method, url, headers, body, attempts + 1, opts) else case resp do %{status: status, headers: headers} when status >= 200 and status <= 299 -> {:ok, convert_value(decoded_body), %{request_id: extract_request_id(headers), status: status}} _ -> {:error, build_error(decoded_body), %{request_id: extract_request_id(headers), status: resp.status}} end end {:error, error} -> if should_retry?(%{}, attempts, client.max_network_retries) do do_request(client, method, url, headers, body, attempts + 1, opts) else {:error, error, %{}} end end extra_telemetry_metadata = case result do {:ok, _, extra} -> Map.put(extra, :result, :ok) {:error, error, extra} -> Map.merge(extra, %{result: :error, error: error}) end telemetry_metadata = Map.merge(telemetry_metadata, extra_telemetry_metadata) {result, telemetry_metadata} end) end defp extract_request_id(headers) do List.keyfind(headers, "request-id", 0, {nil, nil}) |> elem(1) end defp should_retry?(_response, attempts, max_network_retries, decoded_body \\ %{}) defp should_retry?(_response, attempts, max_network_retries, _decoded_body) when attempts >= max_network_retries do false end defp should_retry?(%{status: 429}, _, _, _) do true end defp should_retry?(_response, _attempts, _max_network_retries, %{code: "lock_timeout"}) do true end defp should_retry?(%{headers: headers}, _attempts, _max_network_retries, _decoded_body) do case headers |> List.keyfind("stripe-should-retry", 0, nil) do nil -> true {_, bool} -> String.to_atom(bool) end end defp should_retry?(_, _, _, _) do true end defp build_headers(client) do [ {"user-agent", client.user_agent}, {"authorization", "Bearer #{client.api_key}"} ] |> maybe_concat(["stripe-version: #{client.version}"], client.version != nil) end defp generate_idempotency_key do binary = << System.system_time(:nanosecond)::64, :erlang.phash2({node(), self()}, 16_777_216)::24, System.unique_integer([:positive])::32 >> Base.hex_encode32(binary, case: :lower, padding: false) end defp maybe_concat(headers, _header, false), do: headers defp maybe_concat(headers, header, true), do: Enum.concat(headers, header) defp convert_map(value) do Enum.reduce(value, %{}, fn {key, value}, acc -> Map.put(acc, String.to_atom(key), convert_value(value)) end) end defp build_error(%{"error" => error}) do struct = Stripe.ApiErrors map = convert_map(error) struct!(struct, map) end defp convert_struct(struct, object) do struct_keys = Map.keys(struct.__struct__) |> List.delete(:__struct__) processed_map = struct_keys |> Enum.reduce(%{}, fn key, acc -> string_key = to_string(key) converted_value = case string_key do _ -> Map.get(object, string_key) |> convert_value() end Map.put(acc, key, converted_value) end) struct!(struct, processed_map) end defp convert_object(struct, object) do if known_struct?(struct) do convert_struct(struct, object) else convert_map(object) end end defp convert_value(%{"object" => type, "deleted" => _} = object) do type |> object_type_to_struct(deleted: true) |> convert_object(object) end defp convert_value(%{"object" => type} = object) do type |> object_type_to_struct() |> convert_object(object) end defp convert_value(map) when is_map(map) do convert_map(map) end defp convert_value(values) when is_list(values) do Enum.map(values, &convert_value/1) end defp convert_value(value) do value end defp known_struct?(struct) do function_exported?(struct, :__struct__, 0) end defp object_type_to_struct(object, opts \\ []) defp object_type_to_struct(object, deleted: true) do module = object |> String.split(".") |> Enum.map(&Macro.camelize/1) Module.concat(["Stripe", "Deleted#{module}"]) end defp object_type_to_struct(object, _) do module = object |> String.split(".") |> Enum.map(&Macro.camelize/1) Module.concat(["Stripe" | module]) end end