EXRequester
Quickly define your API functions using module attributes, inspired by retrofit.
Installation
Add exrequester
to your mix.exs deps
def deps do
[{:exrequester, github: "oarrabi/exrequester"}]
end
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(resource_id: resource_id)
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}
in the url will be replaced with the parameterresource_id
in the defined function_name
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 pictureget_picture/2
will execute the url, it takes the client and the parameters specified in the call todefreq 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(resource_id: resource_id)
@delete "/path/to/resource/{resource_id}"
defreq delete_picture(resource_id: resource_id)
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(resource_id: resource_id)
@put "/path/to/resource/{resource_id}"
defreq put_resource(resource_id: resource_id)
@delete "/path/to/resource/{resource_id}"
defreq delete_resource(resource_id: resource_id)
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(resource_id: resource_id, body: body)
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(resource_id: resource_id, sort: sort, filter: filter)
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(resource_id: resource_id, auth: auth, key1: key1)
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(resource_id: resource_id, auth: auth)
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 pass an anonymous function to be used as response parser. For example, we can pass a decoder when calling get_resource
.
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"
Compile time and runtime safty
Compile time safty
When definiing functions at compile time, exrequester
will not compile if you fail to define the correct method.
For example this:
@get "/path/to/resource/{resource_id}"
defreq get_resource()
When it gets compiled, it will return the following descriptive error.
== Compilation error on file lib/http_bin_sample.ex ==
** (ArgumentError) Function definition and url path are not matching:
URL: /path/to/resource/{resource_id}
Function: defreq get_resource()
Errors:
- Parameters [resource_id] are missing from function definition
Correct definition: defreq get_resource(resource_id: resource_id)
The error will have the correct function definition:
defreq get_resource(resource_id: resource_id)
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(resource_id: resource_id)
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(key: key)
please instead call:
get_resource(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