From 5bded58b8ede2e25dc27a67ceada7d8b8e0890a2 Mon Sep 17 00:00:00 2001 From: Tim Gremore Date: Tue, 27 Apr 2021 07:44:28 -0500 Subject: [PATCH] Temporarily remove compilation check for Absinthe Assertions conditionally adds helpers for use with Absinthe. `Assertions.Absinthe` utilizes `Code.ensure_compiled/1` to manage this condition. However, `undefined function assert_response_equals/4` is raised when compiling the all dependencies. Recompiling assertions after compiling all dependencies satisfies the call to `Code.ensure_compiled/1` and the exception is avoided. This removes the conditional check until a proper solution can be found and since we are using Absinthe. --- lib/assertions/absinthe.ex | 412 +++++++++++++++++++------------------ mix.exs | 4 +- mix.lock | 8 +- 3 files changed, 218 insertions(+), 206 deletions(-) diff --git a/lib/assertions/absinthe.ex b/lib/assertions/absinthe.ex index 3e18bc1..c9793cc 100644 --- a/lib/assertions/absinthe.ex +++ b/lib/assertions/absinthe.ex @@ -19,242 +19,254 @@ defmodule Assertions.Absinthe do and then all functions in this module will not need the schema passed explicitly into it. """ - if match?({:module, _}, Code.ensure_compiled(Absinthe)) do - require Assertions - - require ExUnit.Assertions - - # We need to unwrap non_null and list sub-fields - @doc """ - Returns all fields in a type and any sub-types down to a limited depth of nesting (default `3`). - - This is helpful for converting a struct or map into an expected response that is a bare map - and which can be used in some of the other assertions below. - """ - @spec fields_for(module(), atom(), non_neg_integer()) :: list(fields) | atom() - when fields: atom() | {atom(), list(fields)} - def fields_for(schema, %{of_type: type}, nesting) do - fields_for(schema, type, nesting) - end + # if match?({:module, _}, Code.ensure_compiled(Absinthe)) do + # Code.ensure_compiled(Absinthe) - def fields_for(schema, type, nesting) do - type - |> schema.__absinthe_type__() - |> get_fields(schema, nesting) - end + # if Code.ensure_loaded?(Absinthe) do + require Assertions - @doc """ - Returns a document containing the fields in a type and any sub-types down to a limited depth of - nesting (default `3`). - - This is helpful for generating a document to use for testing your GraphQL API. This function - will always return all fields in the given type, ensuring that there aren't any accidental - fields with resolver functions that aren't tested in at least some fashion. - - ## Example - - iex> document_for(:user, 2) - \""" - name - age - posts { - title - subtitle - } - comments { - body - } - \""" - """ - @spec document_for(module(), atom(), non_neg_integer(), Keyword.t()) :: String.t() - def document_for(schema, type, nesting, overrides) do - schema - |> fields_for(type, nesting) - |> merge_overrides(overrides) - |> format_fields(type, 10, schema) - |> List.to_string() - end + require ExUnit.Assertions - @doc ~S""" - Assert that the response for sending `document` equals `expected_response`. + # We need to unwrap non_null and list sub-fields + @doc """ + Returns all fields in a type and any sub-types down to a limited depth of nesting (default `3`). - This is helpful when you want to exhaustively test something by asserting equality on every - field in the response. + This is helpful for converting a struct or map into an expected response that is a bare map + and which can be used in some of the other assertions below. + """ + @spec fields_for(module(), atom(), non_neg_integer()) :: list(fields) | atom() + when fields: atom() | {atom(), list(fields)} + def fields_for(schema, %{of_type: type}, nesting) do + fields_for(schema, type, nesting) + end - ## Example + def fields_for(schema, type, nesting) do + type + |> schema.__absinthe_type__() + |> get_fields(schema, nesting) + end - iex> query = "{ user { #{document_for(:user, 2)} } }" - iex> expected = %{"user" => %{"name" => "Bob", "posts" => [%{"title" => "A post"}]}} - iex> assert_response_equals(query, expected) - """ - @spec assert_response_equals(module(), String.t(), map(), Keyword.t()) :: :ok | no_return() - def assert_response_equals(schema, document, expected_response, options) do - ExUnit.Assertions.assert {:ok, %{data: response}} = Absinthe.run(document, schema, options) - Assertions.assert_maps_equal(response, expected_response, Map.keys(response)) - end + @doc """ + Returns a document containing the fields in a type and any sub-types down to a limited depth of + nesting (default `3`). + + This is helpful for generating a document to use for testing your GraphQL API. This function + will always return all fields in the given type, ensuring that there aren't any accidental + fields with resolver functions that aren't tested in at least some fashion. + + ## Example + + iex> document_for(:user, 2) + \""" + name + age + posts { + title + subtitle + } + comments { + body + } + \""" + """ + @spec document_for(module(), atom(), non_neg_integer(), Keyword.t()) :: String.t() + def document_for(schema, type, nesting, overrides) do + schema + |> fields_for(type, nesting) + |> merge_overrides(overrides) + |> format_fields(type, 10, schema) + |> List.to_string() + end - @doc ~S""" - Assert that the response for sending `document` matches `expr`. + @doc ~S""" + Assert that the response for sending `document` equals `expected_response`. - This is helpful when you want to test some but not all fields in the returned response, or - would like to break up your assertions by binding variables in the body of the match and then - making separate assertions further down in your test. + This is helpful when you want to exhaustively test something by asserting equality on every + field in the response. - ## Example + ## Example - iex> query = "{ user { #{document_for(:user, 2)} } }" - iex> assert_response_matches(query) do - %{"user" => %{"name" => "B" <> _, "posts" => posts}} - end - iex> assert length(posts) == 1 - """ - @spec assert_response_matches(module(), String.t(), Keyword.t(), Macro.expr()) :: - :ok | no_return() - defmacro assert_response_matches(schema, document, options, do: expr) do - quote do - ExUnit.Assertions.assert {:ok, %{data: unquote(expr)}} = - Absinthe.run(unquote(document), unquote(schema), unquote(options)) - end - end + iex> query = "{ user { #{document_for(:user, 2)} } }" + iex> expected = %{"user" => %{"name" => "Bob", "posts" => [%{"title" => "A post"}]}} + iex> assert_response_equals(query, expected) + """ + @spec assert_response_equals(module(), String.t(), map(), Keyword.t()) :: :ok | no_return() + def assert_response_equals(schema, document, expected_response, options) do + ExUnit.Assertions.assert({:ok, %{data: response}} = Absinthe.run(document, schema, options)) + Assertions.assert_maps_equal(response, expected_response, Map.keys(response)) + end - # We don't include any other objects in the list when we've reached the end of our nesting, - # otherwise the resulting document would be invalid because we need to select sub-fields of - # all objects. - defp get_fields(%{fields: _}, _, 0) do - :reject - end + @doc ~S""" + Assert that the response for sending `document` matches `expr`. - # We can't use the struct expansion directly here, because then it becomes a compile-time - # dependency and will make compilation fail for projects that doesn't use Absinthe. - defp get_fields(%struct{fields: fields} = type, schema, nesting) - when struct == Absinthe.Type.Interface do - interface_fields = - Enum.reduce(fields, [], fn {_, value}, acc -> - case fields_for(schema, value.type, nesting - 1) do - :reject -> acc - :scalar -> [String.to_atom(value.name) | acc] - list -> [{String.to_atom(value.name), list} | acc] - end - end) - - implementors = Map.get(schema.__absinthe_interface_implementors__(), type.identifier) - - implementor_fields = - Enum.map(implementors, fn type -> - {type, fields_for(schema, type, nesting) -- interface_fields -- [:__typename]} - end) - - {interface_fields, implementor_fields} - end + This is helpful when you want to test some but not all fields in the returned response, or + would like to break up your assertions by binding variables in the body of the match and then + making separate assertions further down in your test. - defp get_fields(%struct{types: types}, schema, nesting) when struct == Absinthe.Type.Union do - {[], Enum.map(types, &{&1, fields_for(schema, &1, nesting)})} + ## Example + + iex> query = "{ user { #{document_for(:user, 2)} } }" + iex> assert_response_matches(query) do + %{"user" => %{"name" => "B" <> _, "posts" => posts}} + end + iex> assert length(posts) == 1 + """ + @spec assert_response_matches(module(), String.t(), Keyword.t(), Macro.expr()) :: + :ok | no_return() + defmacro assert_response_matches(schema, document, options, do: expr) do + quote do + ExUnit.Assertions.assert( + {:ok, %{data: unquote(expr)}} = + Absinthe.run(unquote(document), unquote(schema), unquote(options)) + ) end + end - defp get_fields(%{fields: fields}, schema, nesting) do + # We don't include any other objects in the list when we've reached the end of our nesting, + # otherwise the resulting document would be invalid because we need to select sub-fields of + # all objects. + defp get_fields(%{fields: _}, _, 0) do + :reject + end + + # We can't use the struct expansion directly here, because then it becomes a compile-time + # dependency and will make compilation fail for projects that doesn't use Absinthe. + defp get_fields(%struct{fields: fields} = type, schema, nesting) + when struct == Absinthe.Type.Interface do + interface_fields = Enum.reduce(fields, [], fn {_, value}, acc -> case fields_for(schema, value.type, nesting - 1) do :reject -> acc :scalar -> [String.to_atom(value.name) | acc] - list when is_list(list) -> [{String.to_atom(value.name), list} | acc] - tuple -> [{String.to_atom(value.name), tuple} | acc] + list -> [{String.to_atom(value.name), list} | acc] end end) - end - defp get_fields(_, _, _) do - :scalar - end + implementors = Map.get(schema.__absinthe_interface_implementors__(), type.identifier) - defp format_fields({interface_fields, implementor_fields}, _, 10, schema) do - interface_fields = - interface_fields - |> Enum.reduce({[], 12}, &do_format_fields(&1, &2, schema)) - |> elem(0) - - implementor_fields = - implementor_fields - |> Enum.map(fn {type, fields} -> - type_info = schema.__absinthe_type__(type) - [_ | rest] = format_fields(fields, type, 12, schema) - fields = ["...on #{type_info.name} {\n" | rest] - [padding(12), fields] - end) - - Enum.reverse([implementor_fields | interface_fields]) - end - - defp format_fields(fields, _, 10, schema) do - fields = - fields - |> Enum.reduce({[], 12}, &do_format_fields(&1, &2, schema)) - |> elem(0) + implementor_fields = + Enum.map(implementors, fn type -> + {type, fields_for(schema, type, nesting) -- interface_fields -- [:__typename]} + end) - Enum.reverse(fields) - end + {interface_fields, implementor_fields} + end - defp format_fields({interface_fields, implementor_fields}, type, left_pad, schema) - when is_list(interface_fields) do - interface_fields = - interface_fields - |> Enum.reduce({["#{camelize(type)} {\n"], left_pad + 2}, &do_format_fields(&1, &2, schema)) - |> elem(0) - - implementor_fields = - implementor_fields - |> Enum.map(fn {type, fields} -> - type_info = schema.__absinthe_type__(type) - [_ | rest] = format_fields(fields, type, left_pad + 2, schema) - fields = ["...on #{type_info.name} {\n" | rest] - [padding(left_pad + 2), fields] - end) - - Enum.reverse(["}\n", padding(left_pad), implementor_fields | interface_fields]) - end + defp get_fields(%struct{types: types}, schema, nesting) when struct == Absinthe.Type.Union do + {[], Enum.map(types, &{&1, fields_for(schema, &1, nesting)})} + end - defp format_fields(fields, type, left_pad, schema) do - fields = - fields - |> Enum.reduce({["#{camelize(type)} {\n"], left_pad + 2}, &do_format_fields(&1, &2, schema)) - |> elem(0) + defp get_fields(%{fields: fields}, schema, nesting) do + Enum.reduce(fields, [], fn {_, value}, acc -> + case fields_for(schema, value.type, nesting - 1) do + :reject -> acc + :scalar -> [String.to_atom(value.name) | acc] + list when is_list(list) -> [{String.to_atom(value.name), list} | acc] + tuple -> [{String.to_atom(value.name), tuple} | acc] + end + end) + end - Enum.reverse(["}\n", padding(left_pad) | fields]) - end + defp get_fields(_, _, _) do + :scalar + end - defp do_format_fields({type, sub_fields}, {acc, left_pad}, schema) do - {[format_fields(sub_fields, type, left_pad, schema), padding(left_pad) | acc], left_pad} - end + defp format_fields({interface_fields, implementor_fields}, _, 10, schema) do + interface_fields = + interface_fields + |> Enum.reduce({[], 12}, &do_format_fields(&1, &2, schema)) + |> elem(0) + + implementor_fields = + implementor_fields + |> Enum.map(fn {type, fields} -> + type_info = schema.__absinthe_type__(type) + [_ | rest] = format_fields(fields, type, 12, schema) + fields = ["...on #{type_info.name} {\n" | rest] + [padding(12), fields] + end) - defp do_format_fields(type, {acc, left_pad}, _) do - {["\n", camelize(type), padding(left_pad) | acc], left_pad} - end + Enum.reverse([implementor_fields | interface_fields]) + end - defp padding(0), do: "" - defp padding(left_pad), do: Enum.map(1..left_pad, fn _ -> " " end) + defp format_fields(fields, _, 10, schema) do + fields = + fields + |> Enum.reduce({[], 12}, &do_format_fields(&1, &2, schema)) + |> elem(0) - defp camelize(type), do: Absinthe.Utils.camelize(to_string(type), lower: true) + Enum.reverse(fields) + end - defp merge_overrides({key, values}, fields) when is_atom(key) and is_list(values) do - Keyword.update!(fields, key, fn field_value -> - Enum.reduce(values, field_value, &merge_overrides/2) + defp format_fields({interface_fields, implementor_fields}, type, left_pad, schema) + when is_list(interface_fields) do + interface_fields = + interface_fields + |> Enum.reduce( + {["#{camelize(type)} {\n"], left_pad + 2}, + &do_format_fields(&1, &2, schema) + ) + |> elem(0) + + implementor_fields = + implementor_fields + |> Enum.map(fn {type, fields} -> + type_info = schema.__absinthe_type__(type) + [_ | rest] = format_fields(fields, type, left_pad + 2, schema) + fields = ["...on #{type_info.name} {\n" | rest] + [padding(left_pad + 2), fields] end) - end - defp merge_overrides({key, replacement_key}, fields) - when is_atom(key) and is_binary(replacement_key) do - Enum.map(fields, fn - ^key -> replacement_key - {^key, value} -> {replacement_key, value} - value -> value - end) - end + Enum.reverse(["}\n", padding(left_pad), implementor_fields | interface_fields]) + end - defp merge_overrides(fields, []) do + defp format_fields(fields, type, left_pad, schema) do + fields = fields - end + |> Enum.reduce( + {["#{camelize(type)} {\n"], left_pad + 2}, + &do_format_fields(&1, &2, schema) + ) + |> elem(0) - defp merge_overrides(fields, overrides) do - Enum.reduce(overrides, fields, &merge_overrides/2) - end + Enum.reverse(["}\n", padding(left_pad) | fields]) end + + defp do_format_fields({type, sub_fields}, {acc, left_pad}, schema) do + {[format_fields(sub_fields, type, left_pad, schema), padding(left_pad) | acc], left_pad} + end + + defp do_format_fields(type, {acc, left_pad}, _) do + {["\n", camelize(type), padding(left_pad) | acc], left_pad} + end + + defp padding(0), do: "" + defp padding(left_pad), do: Enum.map(1..left_pad, fn _ -> " " end) + + defp camelize(type), do: Absinthe.Utils.camelize(to_string(type), lower: true) + + defp merge_overrides({key, values}, fields) when is_atom(key) and is_list(values) do + Keyword.update!(fields, key, fn field_value -> + Enum.reduce(values, field_value, &merge_overrides/2) + end) + end + + defp merge_overrides({key, replacement_key}, fields) + when is_atom(key) and is_binary(replacement_key) do + Enum.map(fields, fn + ^key -> replacement_key + {^key, value} -> {replacement_key, value} + value -> value + end) + end + + defp merge_overrides(fields, []) do + fields + end + + defp merge_overrides(fields, overrides) do + Enum.reduce(overrides, fields, &merge_overrides/2) + end + + # end end diff --git a/mix.exs b/mix.exs index 62a49f8..373f66b 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Assertions.MixProject do def project do [ app: :assertions, - version: "0.18.1", + version: "0.18.2", elixir: "~> 1.7", deps: deps(), description: description(), @@ -39,7 +39,7 @@ defmodule Assertions.MixProject do [ {:ex_doc, "~> 0.19", only: [:dev, :test], runtime: false}, {:ecto, "~> 3.3", only: [:dev, :test], runtime: false}, - {:absinthe, "~> 1.5.0-rc.5", only: [:dev, :test], runtime: false} + {:absinthe, "~> 1.6.0", only: [:dev, :test], runtime: false} ] end end diff --git a/mix.lock b/mix.lock index 822f8f4..962177e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ - "absinthe": {:hex, :absinthe, "1.5.0-rc.5", "90b6335d452bfe72532257bb60c54793f6b8c4992b35b6861dc9adea5d01f463", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7caf5b2d7d4be5a401dc4a17d219d44650af5dce4fe635687ca543c1823047a6"}, - "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, + "absinthe": {:hex, :absinthe, "1.6.4", "d2958908b72ce146698de8ccbc03622630471eb0e354e06823aaef183e5067bd", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e9c1cf36d86c704cb9a9c78db62d1c2676b03e0f61a28a23fc42749e8cd41ae"}, + "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, "ecto": {:hex, :ecto, "3.3.1", "82ab74298065bf0c64ca299f6c6785e68ea5d6b980883ee80b044499df35aba1", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "e6c614dfe3bcff2d575ce16d815dbd43f4ee1844599a83de1eea81976a31c174"}, "ex_doc": {:hex, :ex_doc, "0.19.2", "6f4081ccd9ed081b6dc0bd5af97a41e87f5554de469e7d76025fba535180565f", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "4eae888633d2937e0a8839ae6002536d459c22976743c9dc98dd05941a06c016"}, @@ -8,8 +8,8 @@ "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5fbc8e549aa9afeea2847c0769e3970537ed302f93a23ac612602e805d9d1e7f"}, "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "adf0218695e22caeda2820eaba703fa46c91820d53813a2223413da3ef4ba515"}, "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm", "5c040b8469c1ff1b10093d3186e2e10dbe483cd73d79ec017993fb3985b8a9b3"}, - "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, + "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "testmetrics_elixir_client": {:hex, :testmetrics_elixir_client, "1.1.0", "85b80aed72f538bb0421af83fd65fd4b638f7730426145dfa9b5e17040309a18", [:mix], [{:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, ">= 1.1.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm"}, }