diff --git a/config/config.exs b/config/config.exs index f835efa..5709875 100644 --- a/config/config.exs +++ b/config/config.exs @@ -2,9 +2,10 @@ # and its dependencies with the aid of the Mix.Config module. use Mix.Config - config :sparkpost, api_endpoint: "https://api.sparkpost.com/api/v1/" - config :sparkpost, api_key: "YOUR API KEY HERE" - config :sparkpost, http_timeout: 5000 +config :sparkpost, + api_endpoint: "https://api.sparkpost.com/api/v1/", + api_key: "YOUR API KEY HERE", + http_timeout: 5000 # This configuration is loaded before any dependency and is restricted # to this project. If another project depends on this project, this diff --git a/lib/endpoint.ex b/lib/endpoint.ex index 9ca19a4..0528c9c 100644 --- a/lib/endpoint.ex +++ b/lib/endpoint.ex @@ -10,11 +10,19 @@ defmodule SparkPost.Endpoint do Make a request to the SparkPost API. ## Parameters - - method: HTTP request method as atom (:get, :post, ...) - - endpoint: SparkPost API endpoint as string ("transmissions", "templates", ...) - - options: keyword of optional elements including: - - :params: keyword of query parameters - - :body: request body (string) + - `method`: HTTP 1.1 request method as an atom: + - `:delete` + - `:get` + - `:head` + - `:options` + - `:patch` + - `:post` + - `:put` + - `endpoint`: SparkPost API endpoint as string ("transmissions", "templates", ...) + - `body`: A Map that will be encoded to JSON to be sent as the body of the request (defaults to empty) + - `headers`: A Map of headers of the form %{"Header-Name" => "Value"} to be sent with the request + - `options`: A Keyword list of optional elements including: + - `:params`: A Keyword list of query parameters ## Example List transmissions for the "ElixirRox" campaign: @@ -24,34 +32,24 @@ defmodule SparkPost.Endpoint do "id" => "102258558346809186", "name" => "102258558346809186", "state" => "Success"}, ...], status_code: 200} """ - def request(method, endpoint, options) do - url = if Keyword.has_key?(options, :params) do - Application.get_env(:sparkpost, :api_endpoint, @default_endpoint) <> endpoint - <> "?" <> URI.encode_query(options[:params]) - else - Application.get_env(:sparkpost, :api_endpoint, @default_endpoint) <> endpoint - end - - reqopts = if method in [:get, :delete] do - [ headers: base_request_headers() ] - else - [ - headers: ["Content-Type": "application/json"] ++ base_request_headers(), - body: encode_request_body(options[:body]) - ] - end + def request(method, endpoint, body \\ %{}, headers \\ %{}, options \\ []) do + url = Application.get_env(:sparkpost, :api_endpoint, @default_endpoint) <> endpoint - reqopts = [timeout: Application.get_env(:sparkpost, :http_timeout, 5000)] ++ reqopts + {:ok, request_body} = encode_request_body(body) + + request_headers = if method in [:get, :delete] do + headers + else + Map.merge(headers, %{"Content-Type": "application/json"}) + end + |> Map.merge(base_request_headers) - %{status_code: status_code, body: json} = HTTPotion.request(method, url, reqopts) + timeout = Application.get_env(:sparkpost, :http_timeout, 5000) - body = decode_response_body(json) + request_options = Keyword.put(options, :timeout, timeout) - if Map.has_key?(body, :errors) do - %SparkPost.Endpoint.Error{ status_code: status_code, errors: body.errors } - else - %SparkPost.Endpoint.Response{ status_code: status_code, results: body.results } - end + HTTPoison.request(method, url, request_body, request_headers, request_options) + |> handle_response end def marshal_response(response, struct_type, subkey\\nil) @@ -72,16 +70,30 @@ defmodule SparkPost.Endpoint do response end + defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}) when code >= 200 and code < 300 do + decoded_body = decode_response_body(body) + %SparkPost.Endpoint.Response{status_code: 200, results: decoded_body.results} + end + + defp handle_response({:ok, %HTTPoison.Response{status_code: code, body: body}}) when code >= 400 do + decoded_body = decode_response_body(body) + if Map.has_key?(decoded_body, :errors) do + %SparkPost.Endpoint.Error{status_code: code, errors: decoded_body.errors} + end + end + defp base_request_headers() do {:ok, version} = :application.get_key(:sparkpost, :vsn) - [ + %{ "User-Agent": "elixir-sparkpost/" <> to_string(version), "Authorization": Application.get_env(:sparkpost, :api_key) - ] + } end + # Do not try to remove nils from an empty map + defp encode_request_body(body) when is_map(body) and map_size(body) == 0, do: {:ok, ""} defp encode_request_body(body) do - body |> Washup.filter |> Poison.encode! + body |> Washup.filter |> Poison.encode end defp decode_response_body(body) do diff --git a/lib/mockserver.ex b/lib/mockserver.ex index 044bb02..20ef952 100644 --- a/lib/mockserver.ex +++ b/lib/mockserver.ex @@ -34,6 +34,6 @@ defmodule SparkPost.MockServer do end def mk_http_resp(status_code, body) do - fn (_method, _url, _opts) -> %{status_code: status_code, body: body} end + fn (_method, _url, _body, _headers, _opts) -> {:ok, %HTTPoison.Response{status_code: status_code, body: body}} end end end diff --git a/lib/transmission.ex b/lib/transmission.ex index be56813..d06b580 100644 --- a/lib/transmission.ex +++ b/lib/transmission.ex @@ -126,7 +126,7 @@ defmodule SparkPost.Transmission do recipients: Recipient.to_recipient_list(body.recipients), content: Content.to_content(body.content) } - response = Endpoint.request(:post, "transmissions", [body: body]) + response = Endpoint.request(:post, "transmissions", body) Endpoint.marshal_response(response, Transmission.Response) end @@ -150,7 +150,7 @@ defmodule SparkPost.Transmission do substitution_data: ""} """ def get(transid) do - response = Endpoint.request(:get, "transmissions/" <> transid, []) + response = Endpoint.request(:get, "transmissions/" <> transid) Endpoint.marshal_response(response, __MODULE__, :transmission) end @@ -179,7 +179,7 @@ defmodule SparkPost.Transmission do return_path: :required, state: "Success", substitution_data: nil}] """ def list(filters\\[]) do - response = Endpoint.request(:get, "transmissions", [params: filters]) + response = Endpoint.request(:get, "transmissions", %{}, %{}, [params: filters]) case response do %Endpoint.Response{} -> Enum.map(response.results, fn (trans) -> struct(__MODULE__, trans) end) diff --git a/lib/washup.ex b/lib/washup.ex index 668a0dd..8606e5c 100644 --- a/lib/washup.ex +++ b/lib/washup.ex @@ -10,6 +10,8 @@ defmodule Washup do iex> jenny = %{name: "Jennifer", age: 27, rank: "Captain", pets: nil} iex> Washup.filter(jenny) %{name: "Jennifer", age: 27, rank: "Captain"} + iex> Washup.filter("Plain String") + "Plain String" """ def filter(it) do cond do diff --git a/mix.exs b/mix.exs index 4b434d4..b55a6e9 100644 --- a/mix.exs +++ b/mix.exs @@ -20,13 +20,12 @@ defmodule SparkPost.Mixfile do end def application do - [applications: [:httpotion]] + [applications: [:httpoison]] end defp deps do [ - {:ibrowse, github: "cmullaparthi/ibrowse", tag: "v4.1.2"}, - {:httpotion, "~> 2.1.0"}, + {:httpoison, "~> 0.9"}, {:poison, "~> 1.5"}, {:mock, "~> 0.1.1", only: :test}, {:excoveralls, "~> 0.4", only: :test}, diff --git a/mix.lock b/mix.lock index b3ac27f..22d7615 100644 --- a/mix.lock +++ b/mix.lock @@ -1,17 +1,20 @@ %{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, - "certifi": {:hex, :certifi, "0.3.0", "389d4b126a47895fe96d65fcf8681f4d09eca1153dc2243ed6babad0aac1e763", [:rebar3], []}, + "certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, "credo": {:hex, :credo, "0.4.3", "29fe87aa2ef3c19bf8dd909594b9b79d9e2ed2857a153c00eb3f697f41cb6782", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []}, "ex_doc": {:hex, :ex_doc, "0.11.3", "bb16cb3f4135d880ce25279dc19a9d70802bc4f4942f0c3de9e4862517ae3ace", [:mix], [{:earmark, "~> 0.1.17 or ~> 0.2", [hex: :earmark, optional: true]}]}, - "excoveralls": {:hex, :excoveralls, "0.4.5", "1508e1c7f373f82805975c633e2468a83898b2b902acf79e7359486d71186ea3", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}, {:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}]}, + "excoveralls": {:hex, :excoveralls, "0.4.5", "1508e1c7f373f82805975c633e2468a83898b2b902acf79e7359486d71186ea3", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, "exjsx": {:hex, :exjsx, "3.2.0", "7136cc739ace295fc74c378f33699e5145bead4fdc1b4799822d0287489136fb", [:mix], [{:jsx, "~> 2.6.2", [hex: :jsx, optional: false]}]}, - "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]}]}, + "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]}]}, + "httpoison": {:hex, :httpoison, "0.9.0", "68187a2daddfabbe7ca8f7d75ef227f89f0e1507f7eecb67e4536b3c516faddb", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, "httpotion": {:hex, :httpotion, "2.1.0", "3fe84fbd13d4560c2514da656d022b1191a079178ee4992d245fc3c33c01ee18", [:mix], []}, "ibrowse": {:git, "https://github.com/cmullaparthi/ibrowse.git", "ea3305d21f37eced4fac290f64b068e56df7de80", [tag: "v4.1.2"]}, - "idna": {:hex, :idna, "1.0.3", "d456a8761cad91c97e9788c27002eb3b773adaf5c893275fc35ba4e3434bbd9b", [:rebar3], []}, + "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, "jsx": {:hex, :jsx, "2.6.2", "213721e058da0587a4bce3cc8a00ff6684ced229c8f9223245c6ff2c88fbaa5a", [:mix, :rebar], []}, "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, "mock": {:hex, :mock, "0.1.1", "e21469ca27ba32aa7b18b61699db26f7a778171b21c0e5deb6f1218a53278574", [:mix], [{:meck, "~> 0.8.2", [hex: :meck, optional: false]}]}, "poison": {:hex, :poison, "1.5.2", "560bdfb7449e3ddd23a096929fb9fc2122f709bcc758b2d5d5a5c7d0ea848910", [:mix], []}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}, "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5", "2e73e068cd6393526f9fa6d399353d7c9477d6886ba005f323b592d389fb47be", [:make], []}} diff --git a/test/endpoint_test.exs b/test/endpoint_test.exs index ad638ef..291dbdf 100644 --- a/test/endpoint_test.exs +++ b/test/endpoint_test.exs @@ -26,39 +26,26 @@ defmodule SparkPost.EndpointTest do end end - test "Endpoint.request forms correct URLs" do - base_url = Application.get_env(:sparkpost, :api_endpoint) - endpt = "transmissions" - params = [campaign_id: "campaign101"] - paramstr = URI.encode_query(params) - respfn = MockServer.mk_resp - with_mock HTTPotion, [request: fn(method, url, opts) -> - assert url == base_url <> endpt <> "?" <> paramstr - respfn.(method, url, opts) - end] do - Endpoint.request(:get, "transmissions", [params: params]) - end - end - test "Endpoint.request succeeds with Endpoint.Response" do - with_mock HTTPotion, [request: MockServer.mk_resp] do - Endpoint.request(:get, "transmissions", []) + with_mock HTTPoison, [request: fn(_, _, _, _, _) -> + r = MockServer.mk_resp + r.(nil, nil, nil, nil, nil) + end] do + Endpoint.request(:get, "transmissions", %{}) end end test "Endpoint.request populates Endpoint.Response" do status_code = 200 results = Poison.decode!(MockServer.create_json, [keys: :atoms]).results - with_mock HTTPotion, [request: MockServer.mk_resp] do - resp = %Endpoint.Response{} = Endpoint.request( - :get, "transmissions", []) - + with_mock HTTPoison, [request: MockServer.mk_resp] do + resp = %Endpoint.Response{} = Endpoint.request(:get, "transmissions", %{}, %{}, []) assert %Endpoint.Response{status_code: ^status_code, results: ^results} = resp end end test "Endpoint.request fails with Endpoint.Error" do - with_mock HTTPotion, [request: MockServer.mk_fail] do + with_mock HTTPoison, [request: MockServer.mk_fail] do %Endpoint.Error{} = Endpoint.request( :get, "transmissions", []) end @@ -67,7 +54,7 @@ defmodule SparkPost.EndpointTest do test "Endpoint.request populates Endpoint.Error" do status_code = 400 errors = Poison.decode!(MockServer.create_fail_json, [keys: :atoms]).errors - with_mock HTTPotion, [request: MockServer.mk_fail] do + with_mock HTTPoison, [request: MockServer.mk_fail] do resp = %Endpoint.Error{} = Endpoint.request( :get, "transmissions", []) @@ -77,42 +64,42 @@ defmodule SparkPost.EndpointTest do test "Endpoint.request includes the core HTTP headers" do respfn = MockServer.mk_resp - with_mock HTTPotion, [request: fn (method, url, opts) -> + with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> Enum.each(Headers.for_method(method), fn {header, tester} -> header_atom = String.to_atom(header) - assert Keyword.has_key?(opts[:headers], header_atom), "#{header} header required for #{method} requests" - assert tester.(opts[:headers][header_atom]), "Malformed header: #{header}. See Headers module in #{__ENV__.file} for formatting rules." + assert Map.has_key?(headers, header_atom), "#{header} header required for #{method} requests" + assert tester.(headers[header_atom]), "Malformed header: #{header}. See Headers module in #{__ENV__.file} for formatting rules." end) - respfn.(method, url, opts) + respfn.(method, url, body, headers, opts) end ] do Enum.each([:get, :post, :put, :delete], fn method -> - Endpoint.request(method, "transmissions", []) end) + Endpoint.request(method, "transmissions", %{}, %{}, []) end) end end test "Endpoint.request includes request bodies for appropriate methods" do respfn = MockServer.mk_resp - with_mock HTTPotion, [request: fn (method, url, opts) -> - assert opts[:body] == "{}" - respfn.(method, url, opts) + with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> + assert body == "" + respfn.(method, url, body, headers, opts) end ] do - Endpoint.request(:post, "transmissions", [body: %{}]) - Endpoint.request(:put, "transmissions", [body: %{}]) + Endpoint.request(:post, "transmissions", %{}, %{}, []) + Endpoint.request(:put, "transmissions", %{}, %{}, []) end end test "Endpoint.request includes request timeout" do respfn = MockServer.mk_resp - with_mock HTTPotion, [request: fn (method, url, opts) -> + with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> assert Keyword.has_key?(opts, :timeout) - respfn.(method, url, opts) + respfn.(method, url, body, headers, opts) end ] do - Endpoint.request(:post, "transmissions", []) - Endpoint.request(:put, "transmissions", []) - Endpoint.request(:get, "transmissions", []) + Endpoint.request(:post, "transmissions", %{}, %{}, []) + Endpoint.request(:put, "transmissions", %{}, %{}, []) + Endpoint.request(:get, "transmissions", %{}, %{}, []) end end end diff --git a/test/sparkpost_test.exs b/test/sparkpost_test.exs index b9e3265..91a5d00 100644 --- a/test/sparkpost_test.exs +++ b/test/sparkpost_test.exs @@ -6,7 +6,7 @@ defmodule SparkPostTest do import Mock test "send succeeds with a Transmission.Response" do - with_mock HTTPotion, [request: MockServer.mk_resp] do + with_mock HTTPoison, [request: MockServer.mk_resp] do resp = SparkPost.send( to: "you@there.com", from: "me@here.com", @@ -19,7 +19,7 @@ defmodule SparkPostTest do end test "send fails with a Endpoint.Error" do - with_mock HTTPotion, [request: MockServer.mk_fail] do + with_mock HTTPoison, [request: MockServer.mk_fail] do resp = SparkPost.send( to: "you@there.com", from: "me@here.com", @@ -37,8 +37,8 @@ defmodule SparkPostTest do subject = "Elixir and SparkPost..." text = "Raw text email is boring" html = "Rich text email is terrifying" - with_mock HTTPotion, [request: fn (method, url, opts) -> - inreq = Poison.decode!(opts[:body], [keys: :atoms]) + with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> + inreq = Poison.decode!(body, [keys: :atoms]) assert Recipient.to_recipient_list(inreq.recipients) == Recipient.to_recipient_list(to) assert Content.to_content(inreq.content) == %Content.Inline{ from: Address.to_address(from), @@ -46,7 +46,7 @@ defmodule SparkPostTest do text: text, html: html } - MockServer.mk_resp.(method, url, opts) + MockServer.mk_resp.(method, url, body, headers, opts) end] do SparkPost.send( to: to, diff --git a/test/transmission_test.exs b/test/transmission_test.exs index fda2f9b..baace42 100644 --- a/test/transmission_test.exs +++ b/test/transmission_test.exs @@ -50,7 +50,7 @@ defmodule SparkPost.TransmissionTest do defmodule TestRequests do def test_send(req, test_fn) do - with_mock HTTPotion, [ + with_mock HTTPoison, [ request: handle_send(test_fn) ] do Transmission.send(req) @@ -58,8 +58,8 @@ defmodule SparkPost.TransmissionTest do end defp handle_send(response_test_fn) do - fn (method, url, opts) -> - req = Poison.decode!(opts[:body], [keys: :atoms]) + fn (method, url, body, headers, opts) -> + req = Poison.decode!(body, [keys: :atoms]) fullreq = struct(Transmission, %{ req | options: struct(Transmission.Options, req.options), @@ -67,12 +67,12 @@ defmodule SparkPost.TransmissionTest do content: Content.to_content(req.content) }) response_test_fn.(fullreq) - MockServer.mk_resp.(method, url, opts) + MockServer.mk_resp.(method, url, body, headers, opts) end end defp parse_recipients_field(lst) when is_list(lst) do - Enum.map(lst, fn recip -> + Enum.map(lst, fn recip -> struct(Recipient, parse_recipient(recip)) end) end @@ -95,14 +95,14 @@ defmodule SparkPost.TransmissionTest do end test "Transmission.send succeeds with Transmission.Response" do - with_mock HTTPotion, [request: MockServer.mk_resp] do + with_mock HTTPoison, [request: MockServer.mk_resp] do resp = Transmission.send(TestStructs.basic_transmission) assert %Transmission.Response{} = resp end end test "Transmission.send fails with Endpoint.Error" do - with_mock HTTPotion, [request: MockServer.mk_fail] do + with_mock HTTPoison, [request: MockServer.mk_fail] do req = TestStructs.basic_transmission resp = Transmission.send(req) assert %SparkPost.Endpoint.Error{} = resp @@ -110,9 +110,9 @@ defmodule SparkPost.TransmissionTest do end test "Transmission.send emits a POST" do - with_mock HTTPotion, [request: fn (method, url, opts) -> + with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> assert method == :post - MockServer.mk_resp.(method, url, opts) + MockServer.mk_resp.(method, url, body, headers, opts) end] do Transmission.send(TestStructs.basic_transmission) end @@ -240,7 +240,7 @@ defmodule SparkPost.TransmissionTest do end test "Transmission.list succeeds with a list of Transmission" do - with_mock HTTPotion, [request: MockServer.mk_list] do + with_mock HTTPoison, [request: MockServer.mk_list] do resp = Transmission.list assert is_list(resp) Enum.each(resp, fn r -> assert %Transmission{} = r end) @@ -248,16 +248,16 @@ defmodule SparkPost.TransmissionTest do end test "Transmission.list fails with Endpoint.Error" do - with_mock HTTPotion, [request: MockServer.mk_fail] do + with_mock HTTPoison, [request: MockServer.mk_fail] do resp = Transmission.list assert %SparkPost.Endpoint.Error{} = resp end end test "Transmission.list emits a GET" do - with_mock HTTPotion, [request: fn (method, url, opts) -> + with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> assert method == :get - MockServer.mk_list.(method, url, opts) + MockServer.mk_list.(method, url, body, headers, opts) end] do Transmission.list end @@ -267,9 +267,9 @@ defmodule SparkPost.TransmissionTest do end test "Transmission.get emits a GET" do - with_mock HTTPotion, [request: fn (method, url, opts) -> + with_mock HTTPoison, [request: fn (method, url, body, headers, opts) -> assert method == :get - MockServer.mk_get.(method, url, opts) + MockServer.mk_get.(method, url, body, headers, opts) end] do Transmission.get("TRANSMISSION_ID") end