Skip to content

Commit 5f43828

Browse files
authored
Merge pull request #16 from DavidAntaramian/features/12-use-httpoison
HTTPoison Conversion
2 parents 270ff6d + b1eb970 commit 5f43828

10 files changed

+107
-103
lines changed

config/config.exs

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
# and its dependencies with the aid of the Mix.Config module.
33
use Mix.Config
44

5-
config :sparkpost, api_endpoint: "https://api.sparkpost.com/api/v1/"
6-
config :sparkpost, api_key: "YOUR API KEY HERE"
7-
config :sparkpost, http_timeout: 5000
5+
config :sparkpost,
6+
api_endpoint: "https://api.sparkpost.com/api/v1/",
7+
api_key: "YOUR API KEY HERE",
8+
http_timeout: 5000
89

910
# This configuration is loaded before any dependency and is restricted
1011
# to this project. If another project depends on this project, this

lib/endpoint.ex

+44-32
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@ defmodule SparkPost.Endpoint do
1010
Make a request to the SparkPost API.
1111
1212
## Parameters
13-
- method: HTTP request method as atom (:get, :post, ...)
14-
- endpoint: SparkPost API endpoint as string ("transmissions", "templates", ...)
15-
- options: keyword of optional elements including:
16-
- :params: keyword of query parameters
17-
- :body: request body (string)
13+
- `method`: HTTP 1.1 request method as an atom:
14+
- `:delete`
15+
- `:get`
16+
- `:head`
17+
- `:options`
18+
- `:patch`
19+
- `:post`
20+
- `:put`
21+
- `endpoint`: SparkPost API endpoint as string ("transmissions", "templates", ...)
22+
- `body`: A Map that will be encoded to JSON to be sent as the body of the request (defaults to empty)
23+
- `headers`: A Map of headers of the form %{"Header-Name" => "Value"} to be sent with the request
24+
- `options`: A Keyword list of optional elements including:
25+
- `:params`: A Keyword list of query parameters
1826
1927
## Example
2028
List transmissions for the "ElixirRox" campaign:
@@ -24,34 +32,24 @@ defmodule SparkPost.Endpoint do
2432
"id" => "102258558346809186", "name" => "102258558346809186",
2533
"state" => "Success"}, ...], status_code: 200}
2634
"""
27-
def request(method, endpoint, options) do
28-
url = if Keyword.has_key?(options, :params) do
29-
Application.get_env(:sparkpost, :api_endpoint, @default_endpoint) <> endpoint
30-
<> "?" <> URI.encode_query(options[:params])
31-
else
32-
Application.get_env(:sparkpost, :api_endpoint, @default_endpoint) <> endpoint
33-
end
34-
35-
reqopts = if method in [:get, :delete] do
36-
[ headers: base_request_headers() ]
37-
else
38-
[
39-
headers: ["Content-Type": "application/json"] ++ base_request_headers(),
40-
body: encode_request_body(options[:body])
41-
]
42-
end
35+
def request(method, endpoint, body \\ %{}, headers \\ %{}, options \\ []) do
36+
url = Application.get_env(:sparkpost, :api_endpoint, @default_endpoint) <> endpoint
4337

44-
reqopts = [timeout: Application.get_env(:sparkpost, :http_timeout, 5000)] ++ reqopts
38+
{:ok, request_body} = encode_request_body(body)
39+
40+
request_headers = if method in [:get, :delete] do
41+
headers
42+
else
43+
Map.merge(headers, %{"Content-Type": "application/json"})
44+
end
45+
|> Map.merge(base_request_headers)
4546

46-
%{status_code: status_code, body: json} = HTTPotion.request(method, url, reqopts)
47+
timeout = Application.get_env(:sparkpost, :http_timeout, 5000)
4748

48-
body = decode_response_body(json)
49+
request_options = Keyword.put(options, :timeout, timeout)
4950

50-
if Map.has_key?(body, :errors) do
51-
%SparkPost.Endpoint.Error{ status_code: status_code, errors: body.errors }
52-
else
53-
%SparkPost.Endpoint.Response{ status_code: status_code, results: body.results }
54-
end
51+
HTTPoison.request(method, url, request_body, request_headers, request_options)
52+
|> handle_response
5553
end
5654

5755
def marshal_response(response, struct_type, subkey\\nil)
@@ -72,16 +70,30 @@ defmodule SparkPost.Endpoint do
7270
response
7371
end
7472

73+
defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}) when code >= 200 and code < 300 do
74+
decoded_body = decode_response_body(body)
75+
%SparkPost.Endpoint.Response{status_code: 200, results: decoded_body.results}
76+
end
77+
78+
defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}) when code >= 400 do
79+
decoded_body = decode_response_body(body)
80+
if Map.has_key?(decoded_body, :errors) do
81+
%SparkPost.Endpoint.Error{status_code: code, errors: decoded_body.errors}
82+
end
83+
end
84+
7585
defp base_request_headers() do
7686
{:ok, version} = :application.get_key(:sparkpost, :vsn)
77-
[
87+
%{
7888
"User-Agent": "elixir-sparkpost/" <> to_string(version),
7989
"Authorization": Application.get_env(:sparkpost, :api_key)
80-
]
90+
}
8191
end
8292

93+
# Do not try to remove nils from an empty map
94+
defp encode_request_body(body) when is_map(body) and map_size(body) == 0, do: {:ok, ""}
8395
defp encode_request_body(body) do
84-
body |> Washup.filter |> Poison.encode!
96+
body |> Washup.filter |> Poison.encode
8597
end
8698

8799
defp decode_response_body(body) do

lib/mockserver.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ defmodule SparkPost.MockServer do
3434
end
3535

3636
def mk_http_resp(status_code, body) do
37-
fn (_method, _url, _opts) -> %{status_code: status_code, body: body} end
37+
fn (_method, _url, _body, _headers, _opts) -> {:ok, %HTTPoison.Response{status_code: status_code, body: body}} end
3838
end
3939
end

lib/transmission.ex

+3-3
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ defmodule SparkPost.Transmission do
126126
recipients: Recipient.to_recipient_list(body.recipients),
127127
content: Content.to_content(body.content)
128128
}
129-
response = Endpoint.request(:post, "transmissions", [body: body])
129+
response = Endpoint.request(:post, "transmissions", body)
130130
Endpoint.marshal_response(response, Transmission.Response)
131131
end
132132

@@ -150,7 +150,7 @@ defmodule SparkPost.Transmission do
150150
substitution_data: ""}
151151
"""
152152
def get(transid) do
153-
response = Endpoint.request(:get, "transmissions/" <> transid, [])
153+
response = Endpoint.request(:get, "transmissions/" <> transid)
154154
Endpoint.marshal_response(response, __MODULE__, :transmission)
155155
end
156156

@@ -179,7 +179,7 @@ defmodule SparkPost.Transmission do
179179
return_path: :required, state: "Success", substitution_data: nil}]
180180
"""
181181
def list(filters\\[]) do
182-
response = Endpoint.request(:get, "transmissions", [params: filters])
182+
response = Endpoint.request(:get, "transmissions", %{}, %{}, [params: filters])
183183
case response do
184184
%Endpoint.Response{} ->
185185
Enum.map(response.results, fn (trans) -> struct(__MODULE__, trans) end)

lib/washup.ex

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ defmodule Washup do
1010
iex> jenny = %{name: "Jennifer", age: 27, rank: "Captain", pets: nil}
1111
iex> Washup.filter(jenny)
1212
%{name: "Jennifer", age: 27, rank: "Captain"}
13+
iex> Washup.filter("Plain String")
14+
"Plain String"
1315
"""
1416
def filter(it) do
1517
cond do

mix.exs

+2-3
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,12 @@ defmodule SparkPost.Mixfile do
2020
end
2121

2222
def application do
23-
[applications: [:httpotion]]
23+
[applications: [:httpoison]]
2424
end
2525

2626
defp deps do
2727
[
28-
{:ibrowse, github: "cmullaparthi/ibrowse", tag: "v4.1.2"},
29-
{:httpotion, "~> 2.1.0"},
28+
{:httpoison, "~> 0.9"},
3029
{:poison, "~> 1.5"},
3130
{:mock, "~> 0.1.1", only: :test},
3231
{:excoveralls, "~> 0.4", only: :test},

mix.lock

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
%{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []},
2-
"certifi": {:hex, :certifi, "0.3.0", "389d4b126a47895fe96d65fcf8681f4d09eca1153dc2243ed6babad0aac1e763", [:rebar3], []},
2+
"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []},
33
"credo": {:hex, :credo, "0.4.3", "29fe87aa2ef3c19bf8dd909594b9b79d9e2ed2857a153c00eb3f697f41cb6782", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]},
44
"earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []},
55
"ex_doc": {:hex, :ex_doc, "0.11.3", "bb16cb3f4135d880ce25279dc19a9d70802bc4f4942f0c3de9e4862517ae3ace", [:mix], [{:earmark, "~> 0.1.17 or ~> 0.2", [hex: :earmark, optional: true]}]},
6-
"excoveralls": {:hex, :excoveralls, "0.4.5", "1508e1c7f373f82805975c633e2468a83898b2b902acf79e7359486d71186ea3", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}, {:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}]},
6+
"excoveralls": {:hex, :excoveralls, "0.4.5", "1508e1c7f373f82805975c633e2468a83898b2b902acf79e7359486d71186ea3", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]},
77
"exjsx": {:hex, :exjsx, "3.2.0", "7136cc739ace295fc74c378f33699e5145bead4fdc1b4799822d0287489136fb", [:mix], [{:jsx, "~> 2.6.2", [hex: :jsx, optional: false]}]},
8-
"hackney": {:hex, :hackney, "1.4.8", "c8c6977ed55cc5095e3929f6d94a6f732dd2e31ae42a7b9236d5574ec3f5be13", [:rebar3], [{:ssl_verify_hostname, "1.0.5", [hex: :ssl_verify_hostname, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:idna, "1.0.3", [hex: :idna, optional: false]}, {:certifi, "0.3.0", [hex: :certifi, optional: false]}]},
8+
"hackney": {:hex, :hackney, "1.6.1", "ddd22d42db2b50e6a155439c8811b8f6df61a4395de10509714ad2751c6da817", [:rebar3], [{:certifi, "0.4.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}]},
9+
"httpoison": {:hex, :httpoison, "0.9.0", "68187a2daddfabbe7ca8f7d75ef227f89f0e1507f7eecb67e4536b3c516faddb", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]},
910
"httpotion": {:hex, :httpotion, "2.1.0", "3fe84fbd13d4560c2514da656d022b1191a079178ee4992d245fc3c33c01ee18", [:mix], []},
1011
"ibrowse": {:git, "https://github.com/cmullaparthi/ibrowse.git", "ea3305d21f37eced4fac290f64b068e56df7de80", [tag: "v4.1.2"]},
11-
"idna": {:hex, :idna, "1.0.3", "d456a8761cad91c97e9788c27002eb3b773adaf5c893275fc35ba4e3434bbd9b", [:rebar3], []},
12+
"idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []},
1213
"jsx": {:hex, :jsx, "2.6.2", "213721e058da0587a4bce3cc8a00ff6684ced229c8f9223245c6ff2c88fbaa5a", [:mix, :rebar], []},
1314
"meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []},
15+
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
1416
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
1517
"mock": {:hex, :mock, "0.1.1", "e21469ca27ba32aa7b18b61699db26f7a778171b21c0e5deb6f1218a53278574", [:mix], [{:meck, "~> 0.8.2", [hex: :meck, optional: false]}]},
1618
"poison": {:hex, :poison, "1.5.2", "560bdfb7449e3ddd23a096929fb9fc2122f709bcc758b2d5d5a5c7d0ea848910", [:mix], []},
19+
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []},
1720
"ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5", "2e73e068cd6393526f9fa6d399353d7c9477d6886ba005f323b592d389fb47be", [:make], []}}

test/endpoint_test.exs

+24-37
Original file line numberDiff line numberDiff line change
@@ -26,39 +26,26 @@ defmodule SparkPost.EndpointTest do
2626
end
2727
end
2828

29-
test "Endpoint.request forms correct URLs" do
30-
base_url = Application.get_env(:sparkpost, :api_endpoint)
31-
endpt = "transmissions"
32-
params = [campaign_id: "campaign101"]
33-
paramstr = URI.encode_query(params)
34-
respfn = MockServer.mk_resp
35-
with_mock HTTPotion, [request: fn(method, url, opts) ->
36-
assert url == base_url <> endpt <> "?" <> paramstr
37-
respfn.(method, url, opts)
38-
end] do
39-
Endpoint.request(:get, "transmissions", [params: params])
40-
end
41-
end
42-
4329
test "Endpoint.request succeeds with Endpoint.Response" do
44-
with_mock HTTPotion, [request: MockServer.mk_resp] do
45-
Endpoint.request(:get, "transmissions", [])
30+
with_mock HTTPoison, [request: fn(_, _, _, _, _) ->
31+
r = MockServer.mk_resp
32+
r.(nil, nil, nil, nil, nil)
33+
end] do
34+
Endpoint.request(:get, "transmissions", %{})
4635
end
4736
end
4837

4938
test "Endpoint.request populates Endpoint.Response" do
5039
status_code = 200
5140
results = Poison.decode!(MockServer.create_json, [keys: :atoms]).results
52-
with_mock HTTPotion, [request: MockServer.mk_resp] do
53-
resp = %Endpoint.Response{} = Endpoint.request(
54-
:get, "transmissions", [])
55-
41+
with_mock HTTPoison, [request: MockServer.mk_resp] do
42+
resp = %Endpoint.Response{} = Endpoint.request(:get, "transmissions", %{}, %{}, [])
5643
assert %Endpoint.Response{status_code: ^status_code, results: ^results} = resp
5744
end
5845
end
5946

6047
test "Endpoint.request fails with Endpoint.Error" do
61-
with_mock HTTPotion, [request: MockServer.mk_fail] do
48+
with_mock HTTPoison, [request: MockServer.mk_fail] do
6249
%Endpoint.Error{} = Endpoint.request(
6350
:get, "transmissions", [])
6451
end
@@ -67,7 +54,7 @@ defmodule SparkPost.EndpointTest do
6754
test "Endpoint.request populates Endpoint.Error" do
6855
status_code = 400
6956
errors = Poison.decode!(MockServer.create_fail_json, [keys: :atoms]).errors
70-
with_mock HTTPotion, [request: MockServer.mk_fail] do
57+
with_mock HTTPoison, [request: MockServer.mk_fail] do
7158
resp = %Endpoint.Error{} = Endpoint.request(
7259
:get, "transmissions", [])
7360

@@ -77,42 +64,42 @@ defmodule SparkPost.EndpointTest do
7764

7865
test "Endpoint.request includes the core HTTP headers" do
7966
respfn = MockServer.mk_resp
80-
with_mock HTTPotion, [request: fn (method, url, opts) ->
67+
with_mock HTTPoison, [request: fn (method, url, body, headers, opts) ->
8168
Enum.each(Headers.for_method(method), fn {header, tester} ->
8269
header_atom = String.to_atom(header)
83-
assert Keyword.has_key?(opts[:headers], header_atom), "#{header} header required for #{method} requests"
84-
assert tester.(opts[:headers][header_atom]), "Malformed header: #{header}. See Headers module in #{__ENV__.file} for formatting rules."
70+
assert Map.has_key?(headers, header_atom), "#{header} header required for #{method} requests"
71+
assert tester.(headers[header_atom]), "Malformed header: #{header}. See Headers module in #{__ENV__.file} for formatting rules."
8572
end)
86-
respfn.(method, url, opts)
73+
respfn.(method, url, body, headers, opts)
8774
end
8875
] do
8976
Enum.each([:get, :post, :put, :delete], fn method ->
90-
Endpoint.request(method, "transmissions", []) end)
77+
Endpoint.request(method, "transmissions", %{}, %{}, []) end)
9178
end
9279
end
9380

9481
test "Endpoint.request includes request bodies for appropriate methods" do
9582
respfn = MockServer.mk_resp
96-
with_mock HTTPotion, [request: fn (method, url, opts) ->
97-
assert opts[:body] == "{}"
98-
respfn.(method, url, opts)
83+
with_mock HTTPoison, [request: fn (method, url, body, headers, opts) ->
84+
assert body == ""
85+
respfn.(method, url, body, headers, opts)
9986
end
10087
] do
101-
Endpoint.request(:post, "transmissions", [body: %{}])
102-
Endpoint.request(:put, "transmissions", [body: %{}])
88+
Endpoint.request(:post, "transmissions", %{}, %{}, [])
89+
Endpoint.request(:put, "transmissions", %{}, %{}, [])
10390
end
10491
end
10592

10693
test "Endpoint.request includes request timeout" do
10794
respfn = MockServer.mk_resp
108-
with_mock HTTPotion, [request: fn (method, url, opts) ->
95+
with_mock HTTPoison, [request: fn (method, url, body, headers, opts) ->
10996
assert Keyword.has_key?(opts, :timeout)
110-
respfn.(method, url, opts)
97+
respfn.(method, url, body, headers, opts)
11198
end
11299
] do
113-
Endpoint.request(:post, "transmissions", [])
114-
Endpoint.request(:put, "transmissions", [])
115-
Endpoint.request(:get, "transmissions", [])
100+
Endpoint.request(:post, "transmissions", %{}, %{}, [])
101+
Endpoint.request(:put, "transmissions", %{}, %{}, [])
102+
Endpoint.request(:get, "transmissions", %{}, %{}, [])
116103
end
117104
end
118105
end

test/sparkpost_test.exs

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule SparkPostTest do
66
import Mock
77

88
test "send succeeds with a Transmission.Response" do
9-
with_mock HTTPotion, [request: MockServer.mk_resp] do
9+
with_mock HTTPoison, [request: MockServer.mk_resp] do
1010
resp = SparkPost.send(
1111
to: "you@there.com",
1212
from: "me@here.com",
@@ -19,7 +19,7 @@ defmodule SparkPostTest do
1919
end
2020

2121
test "send fails with a Endpoint.Error" do
22-
with_mock HTTPotion, [request: MockServer.mk_fail] do
22+
with_mock HTTPoison, [request: MockServer.mk_fail] do
2323
resp = SparkPost.send(
2424
to: "you@there.com",
2525
from: "me@here.com",
@@ -37,16 +37,16 @@ defmodule SparkPostTest do
3737
subject = "Elixir and SparkPost..."
3838
text = "Raw text email is boring"
3939
html = "<marquee>Rich text email is terrifying</marquee>"
40-
with_mock HTTPotion, [request: fn (method, url, opts) ->
41-
inreq = Poison.decode!(opts[:body], [keys: :atoms])
40+
with_mock HTTPoison, [request: fn (method, url, body, headers, opts) ->
41+
inreq = Poison.decode!(body, [keys: :atoms])
4242
assert Recipient.to_recipient_list(inreq.recipients) == Recipient.to_recipient_list(to)
4343
assert Content.to_content(inreq.content) == %Content.Inline{
4444
from: Address.to_address(from),
4545
subject: subject,
4646
text: text,
4747
html: html
4848
}
49-
MockServer.mk_resp.(method, url, opts)
49+
MockServer.mk_resp.(method, url, body, headers, opts)
5050
end] do
5151
SparkPost.send(
5252
to: to,

0 commit comments

Comments
 (0)