defmodule Kadena.Chainweb.Request do @moduledoc """ A module to work with Chainweb requests. Requests are composed in a functional manner. The request does not happen until it is configured and passed to `perform/1`. """ alias Kadena.Chainweb.{Client, Error, Network} @type api_type :: :pact | :p2p @type network_id :: :testnet04 | :mainnet01 @type chain_id :: 0..19 | String.t() | nil @type method :: :get | :post | :put @type headers :: [{binary(), binary()}] @type body :: String.t() | nil @type query :: Keyword.t() @type encoded_query :: String.t() | nil @type endpoint :: String.t() | nil @type path :: String.t() | nil @type segment :: String.t() | nil @type opts :: Keyword.t() @type params :: Keyword.t() @type response :: {:ok, map()} | {:error, Error.t()} @type parsed_response :: {:ok, struct()} | {:error, Error.t()} @type location :: String.t() | nil @type t :: %__MODULE__{ method: method(), api_type: api_type(), network_id: network_id(), chain_id: chain_id(), endpoint: endpoint(), path: path(), segment: segment(), segment_path: path(), query: query(), headers: headers(), encoded_query: encoded_query(), location: location(), body: body() } defstruct [ :method, :api_type, :network_id, :chain_id, :endpoint, :path, :segment, :segment_path, :query, :headers, :encoded_query, :location, body: "" ] @spec new(method :: method(), opts :: opts()) :: t() def new(method, [{api_type, opts}]) when api_type in [:pact, :p2p] do network_id = Keyword.get(opts, :network_id) chain_id = Keyword.get(opts, :chain_id) endpoint = Keyword.get(opts, :endpoint) path = Keyword.get(opts, :path) segment = Keyword.get(opts, :segment) segment_path = Keyword.get(opts, :segment_path) %__MODULE__{ method: method, api_type: api_type, network_id: network_id, chain_id: chain_id, endpoint: endpoint, path: path, segment: segment, segment_path: segment_path, query: [], headers: [] } end @spec set_location(t(), location :: location()) :: t() def set_location(%__MODULE__{} = request, location), do: %{request | location: location} @spec set_chain_id(t(), chain_id :: chain_id()) :: t() def set_chain_id(%__MODULE__{} = request, chain_id), do: %{request | chain_id: chain_id} @spec set_network(t(), network :: network_id()) :: t() def set_network(%__MODULE__{} = request, network), do: %{request | network_id: network} @spec add_body(request :: t(), body :: body()) :: t() def add_body(%__MODULE__{} = request, body), do: %{request | body: body} @spec add_headers(request :: t(), headers :: headers()) :: t() def add_headers(%__MODULE__{} = request, headers), do: %{request | headers: headers} @spec add_query(request :: t(), params :: params()) :: t() def add_query(%__MODULE__{} = request, params), do: %{request | query: params, encoded_query: build_query_string(params)} @spec perform(request :: t()) :: response() def perform(%__MODULE__{method: method, headers: headers, body: body} = request) do request |> build_request_url() |> (&Client.request(method, &1, headers, body)).() end @spec results(response :: response(), opts :: opts()) :: parsed_response() def results({:ok, results}, as: resource), do: {:ok, resource.new(results)} def results({:error, error}, _resource), do: {:error, error} @spec build_request_url(request :: t()) :: binary() defp build_request_url( %__MODULE__{ endpoint: endpoint, path: path, segment: segment, segment_path: segment_path, encoded_query: encoded_query } = request ) do base_url = Network.base_url(request) IO.iodata_to_binary([ if(base_url, do: base_url, else: []), if(endpoint, do: ["/" | to_string(endpoint)], else: []), if(path, do: ["/" | to_string(path)], else: []), if(segment, do: ["/" | to_string(segment)], else: []), if(segment_path, do: ["/" | to_string(segment_path)], else: []), if(encoded_query, do: ["?" | encoded_query], else: []) ]) end @spec build_query_string(params :: params()) :: encoded_query() defp build_query_string(params) do params |> Enum.reject(&is_empty_param/1) |> encode_query() end @spec encode_query(query :: query()) :: encoded_query() defp encode_query([]), do: nil defp encode_query(query), do: URI.encode_query(query) @spec is_empty_param(param :: {atom(), any()}) :: boolean() defp is_empty_param({_key, nil}), do: true defp is_empty_param({_key, value}), do: to_string(value) == "" end