Skip to content
This repository was archived by the owner on Sep 1, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
[ ![Codeship Status for StuartApp/stuart-client-elixir](https://app.codeship.com/projects/832b17c0-77a6-0136-8b5b-3e7b2f9f0830/status?branch=master)](https://app.codeship.com/projects/300202)
[![Codeship Status for StuartApp/stuart-client-elixir](https://app.codeship.com/projects/f9859ab0-b145-0137-da11-3e6824a8821c/status?branch=develop)](https://app.codeship.com/projects/363007)

# Stuart Elixir Client

For a complete documentation of all endpoints offered by the Stuart API, you can read the [Stuart API documentation](https://stuart.api-docs.io).
For a complete documentation of all endpoints offered by the Stuart API, you can read the [Stuart API documentation](https://api-docs.stuart.com/).

## Install

```elixir
# mix.exs

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

def deps do
[
{:stuart_client_elixir, "~> 1.2.0"}
{:stuart_client_elixir, "~> 1.3.1"}
]
end
```
Expand Down Expand Up @@ -74,3 +70,44 @@ job = %{
credentials = %Credentials{client_id: "...", client_secret: "..."}
StuartClientElixir.post("/v2/jobs", Jason.encode!(job), %{environment: Environment.sandbox(), credentials: credentials})
```

#### Send a PATCH request to the Stuart API

```elixir
alias StuartClientElixir.{Environment, Credentials}

job = %{
job: %{
deliveries: [
%{
id: "43035",
client_reference: "new_client_reference",
package_description: "new_package_description",
pickup: %{
comment: "new_comment",
contact: %{
firstname: "new_firstname",
lastname: "new_lastname",
phone: "+33628046091",
email: "sd@df.com",
company: "new_company"
}
},
dropoff: %{
comment: "new_comment",
contact: %{
firstname: "new_firstname",
lastname: "new_lastname",
phone: "+33628046095",
email: "new_email@mymail.com",
company: "new_company"
}
}
}
]
}
}

credentials = %Credentials{client_id: "...", client_secret: "..."}
StuartClientElixir.patch("/v2/jobs/1234", Jason.encode!(job), %{environment: Environment.sandbox(), credentials: credentials})
```
1 change: 1 addition & 0 deletions lib/stuart_client_elixir.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ defmodule StuartClientElixir do

defdelegate get(resource, options), to: HttpClient
defdelegate post(resource, body, options), to: HttpClient
defdelegate patch(resource, body, options), to: HttpClient
defdelegate forget_token!(client_id), to: Authenticator
end
2 changes: 2 additions & 0 deletions lib/stuart_client_elixir/authenticator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ defmodule StuartClientElixir.Authenticator do
{:ok, access_token}
else
{:error, %OAuth2.Response{} = oauth_response} -> {:error, oauth_response}
{:error, %OAuth2.Error{} = oauth_error} -> {:error, oauth_error}
end
end

Expand All @@ -47,6 +48,7 @@ defmodule StuartClientElixir.Authenticator do
client_secret: client_secret,
site: site
)
|> OAuth2.Client.put_serializer("application/json", Jason)
end

defp cache_exists?(%Credentials{client_id: client_id}),
Expand Down
2 changes: 1 addition & 1 deletion lib/stuart_client_elixir/environment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ defmodule StuartClientElixir.Environment do
@enforce_keys [:base_url]
defstruct @enforce_keys

def sandbox, do: %__MODULE__{base_url: "https://sandbox-api.stuart.com"}
def sandbox, do: %__MODULE__{base_url: "https://api.sandbox.stuart.com"}
def production, do: %__MODULE__{base_url: "https://api.stuart.com"}
end
64 changes: 45 additions & 19 deletions lib/stuart_client_elixir/http_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,52 @@ defmodule StuartClientElixir.HttpClient do

@callback get(url, options) :: ok_response | error_response
@callback post(url, body, options) :: ok_response | error_response
@callback patch(url, body, options) :: ok_response | error_response

def get(resource, %{environment: environment, credentials: credentials}) do
with url <- url(resource, environment),
{:ok, access_token} <- Authenticator.access_token(environment, credentials),
headers <- default_headers(access_token) do
HTTPoison.get(url, headers)
|> to_api_response()
else
{:error, %OAuth2.Response{}} = oauth_error -> to_api_response(oauth_error)
end
def get(resource, options) do
perform_request(:get, options, resource)
end

def post(resource, body, %{environment: environment, credentials: credentials}) do
def post(resource, body, options) do
perform_request(:post, options, resource, body)
end

def patch(resource, body, options) do
perform_request(:patch, options, resource, body)
end

#####################
# Private functions #
#####################

def perform_request(
method,
%{
environment: environment,
credentials: credentials
},
resource,
body \\ nil
)
when method in [:get, :post, :patch] do
with url <- url(resource, environment),
{:ok, access_token} <- Authenticator.access_token(environment, credentials),
headers <- default_headers(access_token) do
HTTPoison.post(url, body, headers)
case method do
:get -> HTTPoison.get(url, headers, default_options())
:post -> HTTPoison.post(url, body, headers, default_options())
:patch -> HTTPoison.patch(url, body, headers, default_options())
end
|> to_api_response()
else
{:error, %OAuth2.Response{}} = oauth_error -> to_api_response(oauth_error)
{:error, %OAuth2.Response{}} = oauth_response -> to_api_response(oauth_response)
{:error, %OAuth2.Error{}} = oauth_error -> to_api_response(oauth_error)
end
end

#####################
# Private functions #
#####################
defp default_options do
[recv_timeout: 60_000]
end

defp url(resource, %Environment{base_url: base_url}), do: "#{base_url}#{resource}"

Expand All @@ -46,15 +66,21 @@ defmodule StuartClientElixir.HttpClient do
]
end

defp to_api_response({:ok, %HTTPoison.Response{status_code: status_code, body: body}}) do
%{status_code: status_code, body: Jason.decode!(body)}
defp to_api_response({:ok, %HTTPoison.Response{status_code: 204}}) do
%{status_code: 204, body: ""}
end

defp to_api_response({:error, %HTTPoison.Error{} = error}) do
{:error, error}
defp to_api_response({:ok, %HTTPoison.Response{status_code: status_code, body: body}}) do
with {:ok, decoded_body} <- Jason.decode(body) do
%{status_code: status_code, body: decoded_body}
end
end

defp to_api_response({:error, %OAuth2.Response{status_code: status_code, body: body}}) do
%{status_code: status_code, body: body}
end

defp to_api_response({:error, error}) do
{:error, error}
end
end
16 changes: 8 additions & 8 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ defmodule StuartClientElixir.MixProject do
"GitHub" => "https://github.com/StuartApp/stuart-client-elixir"
}
},
version: "1.2.0",
elixir: "~> 1.7",
version: "1.3.1",
elixir: "~> 1.6",
start_permanent: Mix.env() == :prod,
deps: deps()
]
Expand All @@ -32,12 +32,12 @@ defmodule StuartClientElixir.MixProject do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
{:httpoison, "~> 1.5"},
{:oauth2, "~> 0.9.4"},
{:jason, "~> 1.1.2"},
{:cachex, "~> 3.1"},
{:mox, "~> 0.4.0", only: :test},
{:mock, "~> 0.3.2", only: :test}
{:httpoison, "~> 1.6"},
{:oauth2, "~> 2.0"},
{:jason, "~> 1.1"},
{:cachex, "~> 3.2"},
{:mox, "~> 0.5", only: :test},
{:mock, "~> 0.3", only: :test}
]
end
end
37 changes: 19 additions & 18 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
%{
"cachex": {:hex, :cachex, "3.1.1", "588bcf48d20eddad7bff5172f5453090a071eba3191a03f51f779f88e3ac1900", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "1.5.0", "71ae9f304bdf7f00e9cd1823f275c955bdfc68282bc5eb5c85c3a9ade865d68e", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm"},
"meck": {:hex, :meck, "0.8.12", "1f7b1a9f5d12c511848fec26bbefd09a21e1432eadb8982d9a8aceb9891a3cf2", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.2", "e98e998fd76c191c7e1a9557c8617912c53df3d4a6132f561eb762b699ef59fa", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"mox": {:hex, :mox, "0.4.0", "7f120840f7d626184a3d65de36189ca6f37d432e5d63acd80045198e4c5f7e6e", [:mix], [], "hexpm"},
"oauth2": {:hex, :oauth2, "0.9.4", "632e8e8826a45e33ac2ea5ac66dcc019ba6bb5a0d2ba77e342d33e3b7b252c6e", [:mix], [{:hackney, "~> 1.7", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"},
"cachex": {:hex, :cachex, "3.2.0", "a596476c781b0646e6cb5cd9751af2e2974c3e0d5498a8cab71807618b74fe2f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "aef93694067a43697ae0531727e097754a9e992a1e7946296f5969d6dd9ac986"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
"httpoison": {:hex, :httpoison, "1.6.1", "2ce5bf6e535cd0ab02e905ba8c276580bab80052c5c549f53ddea52d72e81f33", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "89149056039084024a284cd703b2d1900d584958dba432132cb21ef35aed7487"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fdf843bca858203ae1de16da2ee206f53416bbda5dc8c9e78f43243de4bc3afe"},
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
"meck": {:hex, :meck, "0.8.12", "1f7b1a9f5d12c511848fec26bbefd09a21e1432eadb8982d9a8aceb9891a3cf2", [:rebar3], [], "hexpm", "7a6ab35a42e6c846636e8ecd6fdf2cc2e3f09dbee1abb15c1a7c705c10775787"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mock": {:hex, :mock, "0.3.2", "e98e998fd76c191c7e1a9557c8617912c53df3d4a6132f561eb762b699ef59fa", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "1d63e81180518c413647e97ad141beeb9b75046715731ed3fca58c0232a4201d"},
"mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm", "052346cf322311c49a0f22789f3698eea030eec09b8c47367f0686ef2634ae14"},
"oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
}
33 changes: 32 additions & 1 deletion test/stuart_client_elixir/authenticator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ defmodule StuartClientElixirTest.AuthenticatorTest do
new: fn oauth_params ->
sample_client(oauth_params)
end,
put_serializer: fn oauth_client, content_type, serializer ->
client_with_serializer(oauth_client, content_type, serializer)
end,
get_token: fn oauth_client ->
get_token_response(oauth_client)
end
Expand All @@ -43,6 +46,17 @@ defmodule StuartClientElixirTest.AuthenticatorTest do
}}
end

test "returns an error for OAuth2.Error" do
assert Authenticator.access_token(Environment.sandbox(), error_credentials()) ==
{:error,
%OAuth2.Error{
reason:
{:options,
{:socket_options,
[packet_size: 0, packet: 0, header: 0, active: false, mode: :binary]}}
}}
end

test "returns a new access token when no access token exists" do
# when
{:ok, access_token} = Authenticator.access_token(Environment.sandbox(), good_credentials())
Expand Down Expand Up @@ -121,6 +135,10 @@ defmodule StuartClientElixirTest.AuthenticatorTest do
%Credentials{client_id: "client-id", client_secret: "bad"}
end

defp error_credentials do
%Credentials{client_id: "client-id", client_secret: "error"}
end

defp sample_client(
strategy: OAuth2.Strategy.ClientCredentials,
client_id: client_id,
Expand All @@ -143,6 +161,10 @@ defmodule StuartClientElixirTest.AuthenticatorTest do
}
end

defp client_with_serializer(oauth_client, content_type, serializer) do
%{oauth_client | serializers: %{content_type => serializer}}
end

defp get_token_response(%OAuth2.Client{client_secret: "client-secret"}) do
{:ok,
sample_client_with_token(
Expand All @@ -164,6 +186,15 @@ defmodule StuartClientElixirTest.AuthenticatorTest do
}}
end

defp get_token_response(%OAuth2.Client{client_secret: "error"}) do
{:error,
%OAuth2.Error{
reason:
{:options,
{:socket_options, [packet_size: 0, packet: 0, header: 0, active: false, mode: :binary]}}
}}
end

defp sample_token(access_token: access_token, expires_at: expires_at) do
%OAuth2.AccessToken{
access_token: access_token,
Expand All @@ -185,7 +216,7 @@ defmodule StuartClientElixirTest.AuthenticatorTest do
ref: nil,
request_opts: [],
token: sample_token(access_token: access_token, expires_at: expires_at),
site: "https://sandbox-api.stuart.com",
site: "https://api.sandbox.stuart.com",
strategy: OAuth2.Strategy.ClientCredentials,
token_method: :post,
token_url: "/oauth/token"
Expand Down
2 changes: 1 addition & 1 deletion test/stuart_client_elixir/environment_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule StuartClientElixirTest.EnvironmentTest do
alias StuartClientElixir.Environment

test "sandbox" do
assert Environment.sandbox() == %Environment{base_url: "https://sandbox-api.stuart.com"}
assert Environment.sandbox() == %Environment{base_url: "https://api.sandbox.stuart.com"}
end

test "production" do
Expand Down
Loading