Separate http request build and perform phases in api library

I’m writing an api library for Salesforce (ex_force), and considering separate request build phase and request perform phase. (#6)

From

config = %ExForce.Config{}

{:ok, resp} = ExForce.query(soql, config)
{:ok, resp} = ExForce.describe_sobject("Account", config)

To

config = %ExForce.Config{}

{:ok, result} = soql |> ExForce.query() |> ExForce.request(config)

stream = soql |> ExForce.query() |> ExForce.stream_query(config)

However, I found following challenges

  • hard to keep type spec for each api function: does it return %SObject{}? Or list of this? or just plain map?
  • different endpoint uses different “decode” behavior, so request perform phase is not independent from request. So I may need to pass body parse function in the request.

So far I thought following ways to separate request build and perform phases. The general-purpose request struct is the simplest, but I want to give type information to library users for each function (which the current implementation does).

Could someone give some advice on this? Or is it the Elixir (or Erlang) way to return minimally structured data back, and force callers to use pattern match?

Use general-purpose request struct

  • pros: simple interface
  • cons: no type information
@spec get_sobject(sobject_id, sobject_name, list) :: Request.t()

@spec request(Request.t()) :: {:ok, any} | {:error, any}

Use different request struct

  • pros: keep type information for each request type
  • cons: need to define request types for different response type.
@spec get_sobject(sobject_id, sobject_name, list) :: SObjectRequest.t()

@spec request(SObjectRequest.t()) :: {:ok, SObject.t()} | {:error, any}

For example, ExAws uses ExAws.Operation protocol for perform(ops, config) - ref.

Return func

  • pros: keep type information at function level
  • cons: hard to instrument the request
@type request_func :: ((config_or_func) -> {:ok, any} | {:error, any})

@spec get_sobject(sobject_id, sobject_name, list) :: ((config_or_func) -> {:ok, SObject.t()} | {:error, any})

@spec request(request_func) :: {:ok, any} | {:error, any}