From 143b61394f9d17c3e49092cfed13b08ac7826681 Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Fri, 10 May 2024 15:16:16 -0600 Subject: [PATCH 01/10] use `Req` as http client --- lib/sanity.ex | 42 ++++++++++++++++++++---------------------- mix.exs | 1 + mix.lock | 2 ++ 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/lib/sanity.ex b/lib/sanity.ex index 3c54846..00e614d 100644 --- a/lib/sanity.ex +++ b/lib/sanity.ex @@ -36,6 +36,7 @@ defmodule Sanity do doc: "Sanity dataset.", required: true ], + # FIXME finch_mod: [ type: :atom, default: Finch, @@ -44,7 +45,7 @@ defmodule Sanity do http_options: [ type: :keyword_list, default: [receive_timeout: 30_000], - doc: "Options to be passed to `Finch.request/3`." + doc: "Options to be passed to `Req.request/2`." ], max_attempts: [ type: :pos_integer, @@ -57,6 +58,11 @@ defmodule Sanity do doc: "Sanity project ID.", required: true ], + req_mod: [ + type: :atom, + default: Req, + doc: false + ], retry_delay: [ type: :pos_integer, default: 1_000, @@ -251,26 +257,25 @@ defmodule Sanity do ) do opts = NimbleOptions.validate!(opts, @request_options_schema) - finch_mod = Keyword.fetch!(opts, :finch_mod) - http_options = Keyword.fetch!(opts, :http_options) - - url = "#{url_for(request, opts)}?#{URI.encode_query(query_params)}" - result = - Finch.build(method, url, headers(opts) ++ headers, body) - |> finch_mod.request(Sanity.Finch, http_options) + Keyword.merge(Keyword.fetch!(opts, :http_options), + body: body, + headers: headers(opts) ++ headers, + method: method, + url: "#{url_for(request, opts)}?#{URI.encode_query(query_params)}" + ) + |> Keyword.fetch!(opts, :req_mod).request() case {opts[:max_attempts], result} do - {_, {:ok, %Finch.Response{body: body, headers: headers, status: status}}} + {_, {:ok, %Req.Response{body: body, headers: headers, status: status}}} when status in 200..299 -> - {:ok, %Response{body: Jason.decode!(body), headers: headers, status: status}} + {:ok, %Response{body: body, headers: headers, status: status}} - {_, {:ok, %Finch.Response{body: body, headers: headers, status: status} = resp}} + {_, {:ok, %Req.Response{body: body, headers: headers, status: status} = resp}} when status in 400..499 -> - if json_resp?(headers) do - {:error, %Response{body: Jason.decode!(body), headers: headers, status: status}} - else - raise %Sanity.Error{source: resp} + case body do + %{} -> {:error, %Response{body: body, headers: headers, status: status}} + _ -> raise %Sanity.Error{source: resp} end {max_attempts, {_, error_or_response}} when max_attempts > 1 -> @@ -292,13 +297,6 @@ defmodule Sanity do end end - defp json_resp?(headers) do - Enum.any?(headers, fn - {"content-type", value} -> String.contains?(value, "application/json") - {_name, _value} -> false - end) - end - @doc """ Like `request/2`, but raises a `Sanity.Error` instead of returning and error tuple. diff --git a/mix.exs b/mix.exs index 0305476..0f992cb 100644 --- a/mix.exs +++ b/mix.exs @@ -39,6 +39,7 @@ defmodule Sanity.MixProject do {:finch, "~> 0.5"}, {:jason, "~> 1.2"}, {:nimble_options, "~> 0.5 or ~> 1.0"}, + {:req, "~> 0.4"}, # dev/test {:dialyxir, "~> 1.0", only: [:dev], runtime: false}, diff --git a/mix.lock b/mix.lock index 200c8eb..7f8ba03 100644 --- a/mix.lock +++ b/mix.lock @@ -14,7 +14,9 @@ "mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"}, "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, + "nimble_ownership": {:hex, :nimble_ownership, "0.3.1", "99d5244672fafdfac89bfad3d3ab8f0d367603ce1dc4855f86a1c75008bce56f", [:mix], [], "hexpm", "4bf510adedff0449a1d6e200e43e57a814794c8b5b6439071274d248d272a549"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, } From 813924bfc41ae3387ffc1f10620ebefe89256306 Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Fri, 10 May 2024 15:19:20 -0600 Subject: [PATCH 02/10] Update integration_test.exs --- test/integration_test.exs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/integration_test.exs b/test/integration_test.exs index 592d0ae..db96662 100644 --- a/test/integration_test.exs +++ b/test/integration_test.exs @@ -37,7 +37,11 @@ defmodule Sanity.MutateIntegrationTest do ) |> Sanity.request(config) - assert {:ok, %Response{body: %{"documents" => [%{"title" => "product x"}]}}} = + assert {:ok, + %Response{ + body: %{"documents" => [%{"title" => "product x"}]}, + headers: %{"content-type" => ["application/json; charset=utf-8"]} + }} = Sanity.doc(id) |> Sanity.request(config) end From d5f2c581d07ce8ad90f72834cb73ee1b9649efee Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Fri, 10 May 2024 15:41:12 -0600 Subject: [PATCH 03/10] fix tests --- lib/sanity.ex | 2 +- lib/sanity/response.ex | 2 +- test/sanity_test.exs | 126 ++++++++++--------------------------- test/support/mock_finch.ex | 6 -- test/support/mock_req.ex | 5 ++ 5 files changed, 41 insertions(+), 100 deletions(-) delete mode 100644 test/support/mock_finch.ex create mode 100644 test/support/mock_req.ex diff --git a/lib/sanity.ex b/lib/sanity.ex index 00e614d..dbb9415 100644 --- a/lib/sanity.ex +++ b/lib/sanity.ex @@ -232,7 +232,7 @@ defmodule Sanity do [] iex> Sanity.result!(%Sanity.Response{body: %{}, status: 200}) - ** (Sanity.Error) %Sanity.Response{body: %{}, headers: nil, status: 200} + ** (Sanity.Error) %Sanity.Response{body: %{}, headers: %{}, status: 200} """ @spec result!(Response.t()) :: any() def result!(%Response{body: %{"result" => result}}), do: result diff --git a/lib/sanity/response.ex b/lib/sanity/response.ex index 7b0e445..07f0d13 100644 --- a/lib/sanity/response.ex +++ b/lib/sanity/response.ex @@ -1,5 +1,5 @@ defmodule Sanity.Response do @type t :: %Sanity.Response{} - defstruct [:body, :headers, :status] + defstruct body: %{}, headers: %{}, status: nil end diff --git a/test/sanity_test.exs b/test/sanity_test.exs index f4ad2f3..0f04a03 100644 --- a/test/sanity_test.exs +++ b/test/sanity_test.exs @@ -5,14 +5,14 @@ defmodule SanityTest do import Mox setup :verify_on_exit! - alias Sanity.{MockFinch, MockSanity, Request, Response} + alias Sanity.{MockReq, MockSanity, Request, Response} alias NimbleOptions.ValidationError @request_config [ dataset: "myset", - finch_mod: MockFinch, http_options: [receive_timeout: 1], project_id: "projectx", + req_mod: MockReq, token: "supersecret" ] @@ -107,34 +107,33 @@ defmodule SanityTest do describe "request" do test "with query" do - Mox.expect(MockFinch, :request, fn request, Sanity.Finch, [receive_timeout: 1] -> - assert %Finch.Request{ + Mox.expect(MockReq, :request, fn opts -> + assert Enum.sort(opts) == [ body: nil, headers: [{"authorization", "Bearer supersecret"}], - host: "projectx.api.sanity.io", - method: "GET", - path: "/v2021-10-21/data/query/myset", - port: 443, - query: "%24var_2=%22y%22&query=%2A", - scheme: :https - } == request - - {:ok, %Finch.Response{body: "{}", headers: [], status: 200}} + method: :get, + receive_timeout: 1, + url: + "https://projectx.api.sanity.io/v2021-10-21/data/query/myset?%24var_2=%22y%22&query=%2A" + ] + + {:ok, %Req.Response{body: %{}, headers: %{}, status: 200}} end) - assert {:ok, %Response{body: %{}, headers: [], status: 200}} == + assert {:ok, %Response{body: %{}, headers: %{}, status: 200}} == Sanity.query("*", var_2: "y") |> Sanity.request(@request_config) end test "with CDN URL" do - Mox.expect(MockFinch, :request, fn %Finch.Request{host: "projectx.apicdn.sanity.io"}, - Sanity.Finch, - _ -> - {:ok, %Finch.Response{body: "{}", headers: [], status: 200}} + Mox.expect(MockReq, :request, fn opts -> + assert Keyword.fetch!(opts, :url) == + "https://projectx.apicdn.sanity.io/v2021-10-21/data/query/myset?query=%2A" + + {:ok, %Req.Response{body: %{}, headers: %{}, status: 200}} end) - assert {:ok, %Response{body: %{}, headers: [], status: 200}} == + assert {:ok, %Response{body: %{}, headers: %{}, status: 200}} == Sanity.query("*") |> Sanity.request(Keyword.put(@request_config, :cdn, true)) end @@ -170,11 +169,10 @@ defmodule SanityTest do end test "409 response" do - Mox.expect(MockFinch, :request, fn %Finch.Request{}, Sanity.Finch, _ -> + Mox.expect(MockReq, :request, fn _opts -> {:ok, - %Finch.Response{ - body: "{\"error\":{\"description\":\"The mutation(s) failed...\"}}", - headers: [{"content-type", "application/json; charset=utf-8"}], + %Req.Response{ + body: %{"error" => %{"description" => "The mutation(s) failed..."}}, status: 409 }} end) @@ -183,22 +181,18 @@ defmodule SanityTest do :error, %Sanity.Response{ body: %{"error" => %{"description" => "The mutation(s) failed..."}}, - headers: [{"content-type", "application/json; charset=utf-8"}], status: 409 } } end test "414 response" do - Mox.expect(MockFinch, :request, fn %Finch.Request{}, Sanity.Finch, _ -> + Mox.expect(MockReq, :request, fn _ -> {:ok, - %Finch.Response{ + %Req.Response{ status: 414, body: - "\r\n414 Request-URI Too Large\r\n\r\n

414 Request-URI Too Large

\r\n
nginx
\r\n\r\n\r\n", - headers: [ - {"content-type", "text/html; charset=UTF-8"} - ] + "\r\n414 Request-URI Too Large\r\n\r\n

414 Request-URI Too Large

\r\n
nginx
\r\n\r\n\r\n" }} end) @@ -208,32 +202,31 @@ defmodule SanityTest do end assert exception == %Sanity.Error{ - source: %Finch.Response{ + source: %Req.Response{ status: 414, body: - "\r\n414 Request-URI Too Large\r\n\r\n

414 Request-URI Too Large

\r\n
nginx
\r\n\r\n\r\n", - headers: [{"content-type", "text/html; charset=UTF-8"}] + "\r\n414 Request-URI Too Large\r\n\r\n

414 Request-URI Too Large

\r\n
nginx
\r\n\r\n\r\n" } } end test "5xx response" do - Mox.expect(MockFinch, :request, fn %Finch.Request{}, Sanity.Finch, _ -> - {:ok, %Finch.Response{body: "fail!", headers: [], status: 500}} + Mox.expect(MockReq, :request, fn _ -> + {:ok, %Req.Response{body: "fail!", headers: %{}, status: 500}} end) exception = - assert_raise Sanity.Error, ~r'%Finch.Response{', fn -> + assert_raise Sanity.Error, ~r'#Req.Response<', fn -> Sanity.query("*") |> Sanity.request(@request_config) end assert exception == %Sanity.Error{ - source: %Finch.Response{body: "fail!", headers: [], status: 500} + source: %Req.Response{body: "fail!", headers: %{}, status: 500} } end test "timeout error" do - Mox.expect(MockFinch, :request, fn %Finch.Request{}, Sanity.Finch, _ -> + Mox.expect(MockReq, :request, fn _ -> {:error, %Mint.TransportError{reason: :timeout}} end) @@ -243,64 +236,13 @@ defmodule SanityTest do Sanity.query("*") |> Sanity.request(@request_config) end end - - test "retries and succeeds" do - Mox.expect(MockFinch, :request, fn %Finch.Request{}, Sanity.Finch, _ -> - {:error, %Mint.TransportError{reason: :timeout}} - end) - - Mox.expect(MockFinch, :request, fn %Finch.Request{}, Sanity.Finch, _ -> - {:ok, %Finch.Response{body: "fail!", headers: [], status: 500}} - end) - - Mox.expect(MockFinch, :request, fn %Finch.Request{}, Sanity.Finch, _ -> - {:ok, %Finch.Response{body: "{}", headers: [], status: 200}} - end) - - log = - ExUnit.CaptureLog.capture_log([level: :warning], fn -> - assert {:ok, %Sanity.Response{body: %{}, headers: [], status: 200}} = - Sanity.query("*") - |> Sanity.request( - Keyword.merge(@request_config, max_attempts: 3, retry_delay: 10) - ) - end) - - assert log =~ - ~s'retrying failed request in 10ms: %Mint.TransportError{reason: :timeout}' - - assert log =~ - ~s'retrying failed request in 20ms: %Finch.Response{' - end - - test "retries and fails" do - Mox.expect(MockFinch, :request, fn %Finch.Request{}, Sanity.Finch, _ -> - {:ok, %Finch.Response{body: "fail!", headers: [], status: 500}} - end) - - Mox.expect(MockFinch, :request, fn %Finch.Request{}, Sanity.Finch, _ -> - {:error, %Mint.TransportError{reason: :timeout}} - end) - - log = - ExUnit.CaptureLog.capture_log([level: :warning], fn -> - assert_raise Sanity.Error, "%Mint.TransportError{reason: :timeout}", fn -> - Sanity.query("*") - |> Sanity.request(Keyword.merge(@request_config, max_attempts: 2, retry_delay: 5)) - end - end) - - assert log =~ - ~s'retrying failed request in 5ms: %Finch.Response{' - end end test "request!" do - Mox.expect(MockFinch, :request, fn %Finch.Request{}, Sanity.Finch, _ -> + Mox.expect(MockReq, :request, fn _ -> {:ok, - %Finch.Response{ - body: "{\"error\":{\"description\":\"The mutation(s) failed...\"}}", - headers: [{"content-type", "application/json; charset=utf-8"}], + %Req.Response{ + body: %{"error" => %{"description" => "The mutation(s) failed..."}}, status: 409 }} end) diff --git a/test/support/mock_finch.ex b/test/support/mock_finch.ex deleted file mode 100644 index 6a4308b..0000000 --- a/test/support/mock_finch.ex +++ /dev/null @@ -1,6 +0,0 @@ -defmodule Sanity.FinchBehavior do - @callback request(Finch.Request.t(), Finch.name(), keyword()) :: - {:ok, Finch.Response.t()} | {:error, Mint.Types.error()} -end - -Mox.defmock(Sanity.MockFinch, for: Sanity.FinchBehavior) diff --git a/test/support/mock_req.ex b/test/support/mock_req.ex new file mode 100644 index 0000000..fb66c03 --- /dev/null +++ b/test/support/mock_req.ex @@ -0,0 +1,5 @@ +defmodule Sanity.ReqBehavior do + @callback request(keyword()) :: {:ok, Req.Response.t()} | {:error, Exception.t()} +end + +Mox.defmock(Sanity.MockReq, for: Sanity.ReqBehavior) From d48a142d0dab08d8f4c3de29ea152f46bb3c3b97 Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Fri, 10 May 2024 15:48:34 -0600 Subject: [PATCH 04/10] drop support for elixir 1.12 --- .github/workflows/elixir.yml | 2 -- mix.exs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 70721eb..8b7cb2d 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -18,8 +18,6 @@ jobs: otp-version: "25" - elixir-version: "1.13" otp-version: "24" - - elixir-version: "1.12" - otp-version: "24" steps: - uses: actions/checkout@v4 diff --git a/mix.exs b/mix.exs index 0f992cb..f29f932 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule Sanity.MixProject do [ app: :sanity, version: @version, - elixir: "~> 1.12", + elixir: "~> 1.13", elixirc_paths: elixirc_paths(Mix.env()), description: "Client library for Sanity CMS.", start_permanent: Mix.env() == :prod, From bfa76879ed124a8562b9668f582aa78ea332040d Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Fri, 10 May 2024 15:53:38 -0600 Subject: [PATCH 05/10] CI --- .github/workflows/elixir-format.yml | 22 ++++++++++++++++++++++ .github/workflows/elixir.yml | 2 -- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/elixir-format.yml diff --git a/.github/workflows/elixir-format.yml b/.github/workflows/elixir-format.yml new file mode 100644 index 0000000..cd1ac51 --- /dev/null +++ b/.github/workflows/elixir-format.yml @@ -0,0 +1,22 @@ +name: Check Elixir format + +on: push + +jobs: + build: + name: Check Elixir format + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: "1.16" + otp-version: "26" + + - name: Install dependencies + run: mix deps.get + + - name: Check format + run: mix format --check-formatted diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 8b7cb2d..3b56a59 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -41,5 +41,3 @@ jobs: ELIXIR_SANITY_TEST_TOKEN: ${{ secrets.ELIXIR_SANITY_TEST_TOKEN }} if: env.ELIXIR_SANITY_TEST_TOKEN run: mix test --warnings-as-errors --only integration - - name: Check format - run: mix format --check-formatted From 00486fd30549f3dd923dc6f804af3a3b1cdedcba Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Fri, 10 May 2024 15:54:22 -0600 Subject: [PATCH 06/10] remove `finch` as direct dependency --- lib/sanity.ex | 6 ------ lib/sanity/application.ex | 19 ------------------- lib/sanity/error.ex | 2 +- mix.exs | 4 +--- 4 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 lib/sanity/application.ex diff --git a/lib/sanity.ex b/lib/sanity.ex index dbb9415..fdd2a00 100644 --- a/lib/sanity.ex +++ b/lib/sanity.ex @@ -36,12 +36,6 @@ defmodule Sanity do doc: "Sanity dataset.", required: true ], - # FIXME - finch_mod: [ - type: :atom, - default: Finch, - doc: false - ], http_options: [ type: :keyword_list, default: [receive_timeout: 30_000], diff --git a/lib/sanity/application.ex b/lib/sanity/application.ex deleted file mode 100644 index a2e736b..0000000 --- a/lib/sanity/application.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Sanity.Application do - # See https://hexdocs.pm/elixir/Application.html - # for more information on OTP Applications - @moduledoc false - - use Application - - @impl true - def start(_type, _args) do - children = [ - {Finch, name: Sanity.Finch} - ] - - # See https://hexdocs.pm/elixir/Supervisor.html - # for other strategies and supported options - opts = [strategy: :one_for_one, name: Sanity.Supervisor] - Supervisor.start_link(children, opts) - end -end diff --git a/lib/sanity/error.ex b/lib/sanity/error.ex index 4351893..0c236ed 100644 --- a/lib/sanity/error.ex +++ b/lib/sanity/error.ex @@ -3,7 +3,7 @@ defmodule Sanity.Error do Error that may occur while making a request to the Sanity API. The `source` field will be one of the following: - * `%Finch.Response{}` - If response with an unsupported HTTP status (like 5xx) is received. + * `%Req.Response{}` - If response with an unsupported HTTP status (like 5xx) is received. * `%Mint.TransportError{}` - If a network error such as a timeout occurred. * `%Sanity.Response{}` - If a 4xx response is received during a call to `Sanity.request!/2`. """ diff --git a/mix.exs b/mix.exs index f29f932..e989b2f 100644 --- a/mix.exs +++ b/mix.exs @@ -28,15 +28,13 @@ defmodule Sanity.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger], - mod: {Sanity.Application, []} + extra_applications: [:logger] ] end # Run "mix help deps" to learn about dependencies. defp deps do [ - {:finch, "~> 0.5"}, {:jason, "~> 1.2"}, {:nimble_options, "~> 0.5 or ~> 1.0"}, {:req, "~> 0.4"}, From b8f3c06ec3c693653bc7a1eeeaa0662b5528c91e Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Fri, 10 May 2024 16:01:59 -0600 Subject: [PATCH 07/10] remove retry of failed request; Req handles this --- lib/sanity.ex | 63 ++++++++++---------------------------------- test/sanity_test.exs | 4 +-- 2 files changed, 15 insertions(+), 52 deletions(-) diff --git a/lib/sanity.ex b/lib/sanity.ex index fdd2a00..7d7ffea 100644 --- a/lib/sanity.ex +++ b/lib/sanity.ex @@ -41,12 +41,6 @@ defmodule Sanity do default: [receive_timeout: 30_000], doc: "Options to be passed to `Req.request/2`." ], - max_attempts: [ - type: :pos_integer, - default: 1, - doc: - "Number of attempts to make before returning error. Requests receiving an HTTP status code of 4xx will not be retried." - ], project_id: [ type: :string, doc: "Sanity project ID.", @@ -57,12 +51,6 @@ defmodule Sanity do default: Req, doc: false ], - retry_delay: [ - type: :pos_integer, - default: 1_000, - doc: - "Delay in ms to wait before retrying after an error. Applies if `max_attempts` is greater than `1`." - ], token: [ type: :string, doc: "Sanity auth token." @@ -251,42 +239,23 @@ defmodule Sanity do ) do opts = NimbleOptions.validate!(opts, @request_options_schema) - result = - Keyword.merge(Keyword.fetch!(opts, :http_options), - body: body, - headers: headers(opts) ++ headers, - method: method, - url: "#{url_for(request, opts)}?#{URI.encode_query(query_params)}" - ) - |> Keyword.fetch!(opts, :req_mod).request() - - case {opts[:max_attempts], result} do - {_, {:ok, %Req.Response{body: body, headers: headers, status: status}}} + Keyword.merge(Keyword.fetch!(opts, :http_options), + body: body, + headers: headers(opts) ++ headers, + method: method, + url: "#{url_for(request, opts)}?#{URI.encode_query(query_params)}" + ) + |> Keyword.fetch!(opts, :req_mod).request() + |> case do + {:ok, %Req.Response{body: body, headers: headers, status: status}} when status in 200..299 -> {:ok, %Response{body: body, headers: headers, status: status}} - {_, {:ok, %Req.Response{body: body, headers: headers, status: status} = resp}} + {:ok, %Req.Response{body: %{} = body, headers: headers, status: status}} when status in 400..499 -> - case body do - %{} -> {:error, %Response{body: body, headers: headers, status: status}} - _ -> raise %Sanity.Error{source: resp} - end - - {max_attempts, {_, error_or_response}} when max_attempts > 1 -> - Logger.warning( - "retrying failed request in #{opts[:retry_delay]}ms: #{inspect(error_or_response)}" - ) - - :timer.sleep(opts[:retry_delay]) + {:error, %Response{body: body, headers: headers, status: status}} - opts = - opts - |> Keyword.update!(:max_attempts, &(&1 - 1)) - |> Keyword.update!(:retry_delay, &(&1 * 2)) - - request(request, opts) - - {_, {_, error_or_response}} -> + {_, error_or_response} -> raise %Sanity.Error{source: error_or_response} end end @@ -335,8 +304,7 @@ defmodule Sanity do request_opts: [ type: :keyword_list, required: true, - doc: - "Options to be passed to `request/2`. If `max_attempts` is omitted then it will default to `3`." + doc: "Options to be passed to `request/2`." ], variables: [ type: {:map, {:or, [:atom, :string]}, :any}, @@ -363,10 +331,7 @@ defmodule Sanity do @impl true @spec stream(Keyword.t()) :: Enumerable.t() def stream(opts) do - opts = - opts - |> NimbleOptions.validate!(@stream_options_schema) - |> Keyword.update!(:request_opts, &Keyword.put_new(&1, :max_attempts, 3)) + opts = NimbleOptions.validate!(opts, @stream_options_schema) case Map.take(opts[:variables], [:pagination_last_id, "pagination_last_id"]) |> Map.keys() do [] -> nil diff --git a/test/sanity_test.exs b/test/sanity_test.exs index 0f04a03..277b0cb 100644 --- a/test/sanity_test.exs +++ b/test/sanity_test.exs @@ -354,15 +354,13 @@ defmodule SanityTest do end test "query, projection, and variables options" do - Mox.expect(MockSanity, :request!, fn %Request{query_params: query_params}, request_opts -> + Mox.expect(MockSanity, :request!, fn %Request{query_params: query_params}, _request_opts -> assert query_params == %{ "$type" => "\"page\"", "query" => "*[(_type == $type) && (!(_id in path('drafts.**')))] | order(_id) [0..999] { _id, title }" } - assert request_opts[:max_attempts] == 3 - %Response{body: %{"result" => [%{"_id" => "a", "title" => "home"}]}} end) From fef16e13013b9954afb828a125c101a0ebd52791 Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Fri, 10 May 2024 16:10:01 -0600 Subject: [PATCH 08/10] integration test for timeout error --- test/integration_test.exs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/integration_test.exs b/test/integration_test.exs index db96662..f319be2 100644 --- a/test/integration_test.exs +++ b/test/integration_test.exs @@ -126,4 +126,13 @@ defmodule Sanity.MutateIntegrationTest do Sanity.query(~S<{"hello": "world"}>) |> Sanity.request(Keyword.put(config, :cdn, true)) end + + test "timeout error", %{config: config} do + config = Keyword.put(config, :http_options, receive_timeout: 1, retry_log_level: false) + + assert_raise Sanity.Error, "%Mint.TransportError{reason: :timeout}", fn -> + Sanity.query(~S<{"hello": "world"}>) + |> Sanity.request(config) + end + end end From 8f23c4a8a3dde6aad676be69f039737d6704243b Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Fri, 10 May 2024 16:10:56 -0600 Subject: [PATCH 09/10] Update integration_test.exs --- test/integration_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration_test.exs b/test/integration_test.exs index f319be2..21c30b3 100644 --- a/test/integration_test.exs +++ b/test/integration_test.exs @@ -128,7 +128,7 @@ defmodule Sanity.MutateIntegrationTest do end test "timeout error", %{config: config} do - config = Keyword.put(config, :http_options, receive_timeout: 1, retry_log_level: false) + config = Keyword.put(config, :http_options, receive_timeout: 0, retry_log_level: false) assert_raise Sanity.Error, "%Mint.TransportError{reason: :timeout}", fn -> Sanity.query(~S<{"hello": "world"}>) From 7e59662d14a2f385cb69a418715e0843145e0d76 Mon Sep 17 00:00:00 2001 From: Brian Alexander Date: Fri, 10 May 2024 16:49:37 -0600 Subject: [PATCH 10/10] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e795a9..94a4f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- (BREAKING) Switch HTTP client from `finch` to `req` (https://github.com/balexand/sanity/pull/81). This introduces the following breaking changes: + - The `headers` field of the `Sanity.Response` now returns a map instead of a list of tuples. See https://hexdocs.pm/req/changelog.html#change-headers-to-be-maps for details. + - The `:max_attempts` and `:retry_delay` options have been removed from `Sanity.request/2`. `Req` handles retries for us. + - The `source` field in the `Sanity.Error` exception may now contain a `Req.Response` struct instead of a `Finch.Response`. ## [1.3.0] - 2023-07-19 ### Changed