HTTPehaviour Build Status

HTTP client for Elixir, based on HTTPoison.

But why not HTTPoison?

HTTPoison does not provide a clean way of overriding steps of the HTTP request. This project is an attempt to fix this.

Installation

First, add HTTPehaviour to your mix.exs dependencies:

def deps do
  [{:httpehaviour, "~> 0.9"}]
end

and run $ mix deps.get. Now, list the :httpehaviour application as part of your application dependencies:

def application do
  [applications: [:httpehaviour]]
end

Usage

iex> HTTPehaviour.start
iex> HTTPehaviour.get! "http://httparrot.herokuapp.com/get"
%HTTPehaviour.Response{
  body: "{\n  \"args\": {},\n  \"headers\": {} ...",
  headers: %{"connection" => "keep-alive", "content-length" => "517", ...},
  status_code: 200
}
iex> HTTPehaviour.get! "http://localhost:1"
** (HTTPehaviour.Error) :econnrefused
iex> HTTPehaviour.get "http://localhost:1"
{:error, %HTTPehaviour.Error{id: nil, reason: :econnrefused}}

You can also easily pattern match on the HTTPehaviour.Response struct:

case HTTPehaviour.get(url) do
  {:ok, %HTTPehaviour.Response{status_code: 200, body: body}} ->
    IO.puts body
  {:ok, %HTTPehaviour.Response{status_code: 404}} ->
    IO.puts "Not found :("
  {:error, %HTTPehaviour.Error{reason: reason}} ->
    IO.inspect reason
end

Overriding parts of the request

The request will follow like this:

If any callback is called and returns { :halt, state }, it will finish it and return HTTPehaviour.Error

You can define a module that implement the following callbacks

defcallback init_request(request :: HTTPehaviour.Request.t) :: { :continue, any } | { :halt, any }

defcallback process_request_url(url :: binary, state :: any) :: { :continue, binary, any } | { :halt, any }
defcallback process_request_body(body :: binary, state :: any) :: { :continue, binary, any } | { :halt, any }
defcallback process_request_headers(headers :: HTTPehaviour.headers, state :: any) :: { :continue, HTTPehaviour.headers, any } | { :halt, any }

defcallback process_response_status_code(status_code :: integer, state :: any) :: { :continue, integer, any } | { :halt, any }
defcallback process_response_headers(headers :: HTTPehaviour.headers, state :: any) :: { :continue, HTTPehaviour.headers, any } | { :halt, any }
defcallback process_response_body(body :: binary, state :: any) :: { :continue, binary, any } | { :halt, any }
defcallback process_response_chunk(chunk :: binary, state :: any) :: { :continue, binary, any } | { :halt, any }

defcallback terminate_request(state :: any) :: any

Here's a simple example to build a client for the GitHub API

defmodule GitHub do
  use HTTPehaviour.Client
  @expected_fields ~w(
    login id avatar_url gravatar_id url html_url followers_url
    following_url gists_url starred_url subscriptions_url
    organizations_url repos_url events_url received_events_url type
    site_admin name company blog location email hireable bio
    public_repos public_gists followers following created_at updated_at)

  def process_request_url(url, state) do
    { :continue, "https://api.github.com" <> url, state }
  end

  def process_response_body(body, state) do
    body = body |> Poison.decode!
                |> Dict.take(@expected_fields)
                |> Enum.map(fn({k, v}) -> {String.to_atom(k), v} end)
    { :continue, body, state }
  end

  def users do
    get!("/users/edgurgel", [], behaviour: __MODULE__).body[:public_repos]
  end
end

One can pass state data through the request and even get the final state back after the request is completed.

The request will run:

init_request -> process_request_url -> process_request_headers -> process_request_body -> process_response_status_code -> process_request_headers -> process_response_body -> terminate_request

For async requests it will do process_response_chunk instead of process_response_body

This is still a work in progress.

You can see more usage examples in the test files (located in the test/) directory.

License

Copyright © 2015 Eduardo Gurgel <eduardo@gurgel.me>

This work is free. You can redistribute it and/or modify it under the
terms of the MIT License. See the LICENSE file for more details.