exrequester

Quickly create API clients using module attributes.



Documentation




Build Status Hex.pm API Docs Coverage Status Inline docs

EXRequester

Quickly create API clients using module attributes, inspired by retrofit.

Installation

Add exrequester to your mix.exs deps

def deps do
  [{:exrequester, "~> 0.1.0"}]
end

Then fetch your project's dependencies:

$ mix deps.get

Usage

Start by adding use EXRequester in your module

defmodule SampleAPI do
  use EXRequester
end

This will make defreq/1 macro available. This macro takes a function name and a set of parametrs as its argument

defmodule SampleAPI do
  use EXRequester

  @get "/path/to/resource/{resource_id}"
  defreq get_picture
end

The above is the simplest form to define an api function.

  • @get is used to define the relative path that will be fetched
  • {resource_id} the resource id to use, this has to be passed when calling the function

Compiling the above will make the following functions available:

defmodule SampleAPI do
  def client(base_url)
  def get_picture(client, resource_id: resource_id)
end
  • client/1 is used to set hte base url that will be used in get picture
  • get_picture/2 will execute the url, it takes the client and the parameters specified in the call to defreq get_picture...

For example, to call get_picture you would do this:

SampleAPI.client("http://base_url.com")
|> SampleAPI.get_picture(resource_id: 123)

Setting HTTP method

Define a get request endpoint

defmodule SampleAPI do
  use EXRequester

  @get "/path/to/resource/{resource_id}"
  defreq get_resource

  @delete "/path/to/resource/{resource_id}"
  defreq delete_picture
end

Now to use it:

SampleAPI.client("http://base_url.com")
|> SampleAPI.get_resource(resource_id: 123)

SampleAPI.client("http://base_url.com")
|> SampleAPI.post_picture(resource_id: 123, body: %{key: value})

This will hit http://base_url.com/path/to/resource/123

Available http methods are:

defmodule SampleAPI do
  use EXRequester

  @get "/path/to/resource/{resource_id}"
  defreq get_resource

  @put "/path/to/resource/{resource_id}"
  defreq put_resource

  @delete "/path/to/resource/{resource_id}"
  defreq delete_resource
end

Handling request body

Body is handled as a normal parameter

defmodule SampleAPI do
  use EXRequester

  @post "/path/to/resource/{resource_id}"
  defreq post_picture
end

Body is handled in a special way based on its type.

  • String bodyis sent as is
  • List and map bodies is Json encode
SampleAPI.post_picture(resource_id: 123, body: ["1", "2"])
SampleAPI.post_picture(resource_id: 123, body: %{key: value})

Will send json: [\"1\", \"2\"] and {\"key\":\"value\"}

  • Keyword list are currently ignored and will send an empty body

Handling query

To add query to your api endpoint you would use the following:

defmodule SampleAPI do
  use EXRequester

  @query [:sort, :filter]
  @get "/path/to/resource/{resource_id}"
  defreq get_resource
end

You now can call the function defined with all, some or none of the query values:

SampleAPI.client("http://base_url.com")
|> SampleAPI.get_resource(resource_id: 123)

SampleAPI.client("http://base_url.com")
|> SampleAPI.get_resource(resource_id: 123, sort: "ascending")

SampleAPI.client("http://base_url.com")
|> SampleAPI.get_resource(resource_id: 123, sort: "ascending", filter: "all")

These will hit the following endpoint in order:

http://base_url.com/path/to/resource/123

http://base_url.com/path/to/resource/123?sort=ascending

http://base_url.com/path/to/resource/123?sort=ascending&filter=all

Setting headers

Dynamic Headers

defmodule SampleAPI do
  use EXRequester

  @headers [
    Authorization: :auth,
    Key1: :key1
  ]
  @get "/path/to/resource/{resource_id}"
  defreq get_resource
end

Now to use it:

SampleAPI.client("http://base_url.com")
|> SampleAPI.get_resource(resource_id: 123, auth1: "1", key1: "2")

This will hit http://base_url.com/path/to/resource/123 The Authorization and Key1 headers will also be set.

Static Headers

Static headers are defined by using strings, instead of atom, in the @headers definition

defmodule SampleAPI do
  use EXRequester

  @headers [
    Authorization: :auth,
    Accept: "application/json",
    "Accept-Language": "en-US"
  ]
  @get "/path/to/resource/{resource_id}"
  defreq get_resource
end

Calling SampleAPI.get_resource will perform a request that always sends these headers:

Accept: application/json
Accept-Language: en-US

Notice the use of quotes in the "Accept-Language". This is needed since Accept-Language is not a valid atom name. In order to solve that, add quotation around atoms.

Decoding HTTP Response

EXRequester allows you to define a parse function/block to be used as a parser for the resceived response. The parser can be set in three ways.

First: You can pass the anonymouse function at the function definition, For example:

defmodule SampleAPI do
  ....
  defreq get_resource(fn response ->
    "Value is " <> response.body
  end)
end

When calling get_resource the HTTP response of type EXRequester.Response will be sent to the passed anonymous function. Using this way, you can create a response decoder in place.

Second: By defining a body to the get_resource function, inside this body, you can use response object which will be injected by the macro

defmodule SampleAPI do
  ....
  defreq get_resource do
    "Value is " <> response.body
  end
end

response will be set by the macro to the value of the EXRequester.Response received.

Alternatively, you can pass a response decoder when calling the method pass a decoder as a parameter when calling get_resource For example:

SampleAPI.client("http://base_url.com")
|> SampleAPI.get_resource(resource_id: 123, auth: "1", decoder: fn response ->
  # Parse the response and return a new one
  "Response is " <> response.body
end)

The anonymous function passed to decoder will receive an EXRequester.Response structure. The anonymous function can parse the response and return a new response. The returned new parsed response will finally returned from get_resource.

In the above example, the return value will be "Response is The body content"

Handle response

Hitting any request will return a EXRequester.Response strucutre. This structure contains headers, status_code and body

The body will not be parsed and will be returned as is.

Runtime safty

When calling the wrong method at runtime, exrequester will fail with a descriptive message.

For example:

@get "/path/to/resource/{resource_id}"
defreq get_resource

If you wrongly call the method as:

AMod.client("http://localhost:9090/")
|> AMod.get_resource(key: 123)

The following error will be raised:

** (RuntimeError) You are trying to call the wrong function
get_resource(client, key: key)
please instead call:
get_resource(client, resource_id: resource_id)

The error will inform you about the correct method invocation

Future improvments

  • Ability to set the URL in the function definition instead