From 2f6e9b263217e48ac94d6eec7409f55788c8893a Mon Sep 17 00:00:00 2001 From: Peter Urbak Date: Mon, 27 Sep 2021 10:59:47 +0200 Subject: [PATCH 1/2] Improves error handling request.ex to avoid throwing HTTPoison errors to caller --- lib/elixir_lokalise_api/processor.ex | 2 + lib/elixir_lokalise_api/request.ex | 38 ++++++++++++++----- mix.exs | 17 ++++++--- mix.lock | 5 +++ .../endpoints/projects_test.exs | 2 +- 5 files changed, 48 insertions(+), 16 deletions(-) diff --git a/lib/elixir_lokalise_api/processor.ex b/lib/elixir_lokalise_api/processor.ex index 53dfb58..c1486f9 100644 --- a/lib/elixir_lokalise_api/processor.ex +++ b/lib/elixir_lokalise_api/processor.ex @@ -28,6 +28,8 @@ defmodule ElixirLokaliseApi.Processor do {:error, error, status} = ElixirLokaliseApi.Projects.find(nil) """ + @spec parse(HTTPoison.Response.t(), atom, String.t()) :: + {:ok, struct | map} | {:error, map, integer} def parse(response, module, type) do data_key = module.data_key singular_data_key = module.singular_data_key diff --git a/lib/elixir_lokalise_api/request.ex b/lib/elixir_lokalise_api/request.ex index e645b36..54bfb19 100644 --- a/lib/elixir_lokalise_api/request.ex +++ b/lib/elixir_lokalise_api/request.ex @@ -12,22 +12,38 @@ defmodule ElixirLokaliseApi.Request do @defaults [type: nil, data: nil, url_params: Keyword.new(), query_params: Keyword.new()] @doc """ - Prepares and sends an HTTP request with the provided verb and options + Prepares and sends an HTTP request with the provided verb and options. """ + @spec do_request(method(), url(), Keyword.t()) :: + {:ok, struct | map} | {:error, String.t() | {map, integer}} def do_request(verb, module, opts) do opts = opts |> prepare_opts() - Request.request!( - verb, - UrlGenerator.generate(module, opts), - Processor.encode(opts[:data]), - headers(), - request_params(opts[:query_params]) - ) - |> Processor.parse(module, opts[:type]) + request = + Request.request( + verb, + UrlGenerator.generate(module, opts), + Processor.encode(opts[:data]), + headers(), + request_params(opts[:query_params]) + ) + + with {:ok, response} <- request, + {:ok, parsed_result} <- Processor.parse(response, module, opts[:type]) do + {:ok, parsed_result} + else + {:error, error} -> + # HTTPoison error + {:error, error.reason} + + {:error, data, status} -> + # Processor error + {:error, {data, status}} + end end - defp headers() do + @spec headers :: Keyword.t() + defp headers do [ "X-Api-Token": Config.api_token(), Accept: "application/json", @@ -35,7 +51,9 @@ defmodule ElixirLokaliseApi.Request do ] end + @spec request_params(Keyword.t()) :: Keyword.t() defp request_params(params), do: Keyword.merge(Config.request_options(), params: params) + @spec prepare_opts(Keyword.t()) :: Keyword.t() defp prepare_opts(opts), do: Keyword.merge(@defaults, opts) end diff --git a/mix.exs b/mix.exs index 12b3dfc..cb83cc1 100644 --- a/mix.exs +++ b/mix.exs @@ -14,6 +14,11 @@ defmodule ElixirLokaliseApi.MixProject do docs: docs(), deps: deps(), test_coverage: [tool: ExCoveralls], + + # Dialyxir + dialyzer: [plt_add_deps: :project], + + # CLI env preferred_cli_env: [ vcr: :test, "vcr.delete": :test, @@ -35,11 +40,13 @@ defmodule ElixirLokaliseApi.MixProject do defp deps do [ - {:httpoison, "~> 1.8.0"}, + {:credo, "~> 1.5", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.1", only: :dev, runtime: false}, + {:httpoison, "~> 1.8"}, {:jason, "~> 1.2"}, - {:ex_doc, ">= 0.0.0", only: [:dev, :test], runtime: false}, - {:exvcr, "~> 0.13.2", only: :test}, - {:excoveralls, "~> 0.14.2", only: :test} + {:ex_doc, "~> 0.25", only: [:dev, :test], runtime: false}, + {:exvcr, "~> 0.13", only: :test}, + {:excoveralls, "~> 0.14", only: :test} ] end @@ -48,7 +55,7 @@ defmodule ElixirLokaliseApi.MixProject do extras: [ "CHANGELOG.md": [title: "Changelog"], "LICENSE.md": [title: "License"], - "README.md": [title: "Overview"], + "README.md": [title: "Overview"] ], main: "readme", homepage_url: @source_url, diff --git a/mix.lock b/mix.lock index 22d788d..87f26f9 100644 --- a/mix.lock +++ b/mix.lock @@ -1,11 +1,16 @@ %{ + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"}, + "credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"}, + "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.25.3", "3edf6a0d70a39d2eafde030b8895501b1c93692effcbd21347296c18e47618ce", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "9ebebc2169ec732a38e9e779fd0418c9189b3ca93f4a676c961be6c1527913f5"}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, "excoveralls": {:hex, :excoveralls, "0.14.2", "f9f5fd0004d7bbeaa28ea9606251bb643c313c3d60710bad1f5809c845b748f0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ca6fd358621cb4d29311b29d4732c4d47dac70e622850979bc54ed9a3e50f3e1"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, "exvcr": {:hex, :exvcr, "0.13.2", "e17fd3ee3a341f41a3aa65a3ce73a339759a9d0658f83782492c6e9b6cf9daa4", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.8.0", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "17f41a533d14f582fe6b5f83214f058cf5ba77c6a7bc15bc53a9ea1827d92d96"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"}, "httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, diff --git a/test/elixir_lokalise_api/endpoints/projects_test.exs b/test/elixir_lokalise_api/endpoints/projects_test.exs index d0616a5..b13bacc 100644 --- a/test/elixir_lokalise_api/endpoints/projects_test.exs +++ b/test/elixir_lokalise_api/endpoints/projects_test.exs @@ -119,7 +119,7 @@ defmodule ElixirLokaliseApi.ProjectsTest do use_cassette "project_create_error" do project_data = %{name: "Elixir SDK", description: "Created via API"} - {:error, %{} = data, status} = Projects.create(project_data) + {:error, {%{} = data, status}} = Projects.create(project_data) assert status == 400 assert data.error.message == "Invalid `X-Api-Token` header" end From 62a9b30439d26e4794ff63354bef2e27643d0a04 Mon Sep 17 00:00:00 2001 From: Peter Urbak Date: Mon, 27 Sep 2021 11:15:55 +0200 Subject: [PATCH 2/2] Extends request spec --- lib/elixir_lokalise_api/request.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir_lokalise_api/request.ex b/lib/elixir_lokalise_api/request.ex index 54bfb19..1f92c5e 100644 --- a/lib/elixir_lokalise_api/request.ex +++ b/lib/elixir_lokalise_api/request.ex @@ -15,7 +15,7 @@ defmodule ElixirLokaliseApi.Request do Prepares and sends an HTTP request with the provided verb and options. """ @spec do_request(method(), url(), Keyword.t()) :: - {:ok, struct | map} | {:error, String.t() | {map, integer}} + {:ok, struct | map} | {:error, atom | String.t() | {map, integer}} def do_request(verb, module, opts) do opts = opts |> prepare_opts()