-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
360 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
defmodule AbsintheOneOf.Directive do | ||
use Absinthe.Schema.Prototype | ||
|
||
directive :one_of do | ||
on([:input_object]) | ||
expand(&expand_one_of/2) | ||
end | ||
|
||
@spec expand_one_of( | ||
arguments :: %{}, | ||
node :: Absinthe.Blueprint.Schema.InputObjectTypeDefinition.t() | ||
) :: Absinthe.Blueprint.Schema.InputObjectTypeDefinition.t() | ||
defp expand_one_of(_arguments, %Absinthe.Blueprint.Schema.InputObjectTypeDefinition{} = node) do | ||
%{node | __private__: Keyword.put(node.__private__, :one_of, true)} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
defmodule AbsintheOneOf.Phase do | ||
@behaviour Absinthe.Phase | ||
|
||
@impl true | ||
@spec run( | ||
blueprint :: Absinthe.Blueprint.node_t(), | ||
config :: any() | ||
) :: {:ok, Absinthe.Blueprint.node_t()} | ||
def run(blueprint, _config) do | ||
{:ok, Absinthe.Blueprint.prewalk(blueprint, &prewalk/1)} | ||
end | ||
|
||
@spec prewalk(node :: Absinthe.Blueprint.node_t()) :: Absinthe.Blueprint.node_t() | ||
defp prewalk(%Absinthe.Blueprint.Input.Argument{} = node) do | ||
case find_invalid(node) do | ||
{nil, 0} -> | ||
node | ||
|
||
{invalid_node, count} -> | ||
Absinthe.Phase.put_error(node, error(invalid_node, count)) | ||
end | ||
end | ||
|
||
defp prewalk(node), do: node | ||
|
||
@spec find_invalid(node :: Absinthe.Blueprint.node_t()) :: | ||
{nil | Absinthe.Blueprint.node_t(), integer()} | ||
defp find_invalid( | ||
%{ | ||
input_value: %Absinthe.Blueprint.Input.Value{ | ||
normalized: %Absinthe.Blueprint.Input.Object{fields: fields} | ||
} | ||
} = node | ||
) do | ||
Enum.reduce_while( | ||
fields, | ||
if valid?(node) do | ||
{nil, 0} | ||
else | ||
{node, Enum.count(fields)} | ||
end, | ||
fn field, {invalid_node, count} -> | ||
if invalid_node do | ||
{:halt, {invalid_node, count}} | ||
else | ||
{:cont, find_invalid(field)} | ||
end | ||
end | ||
) | ||
end | ||
|
||
defp find_invalid(_node), do: {nil, 0} | ||
|
||
@spec valid?(node :: Absinthe.Blueprint.node_t()) :: boolean() | ||
defp valid?( | ||
%{ | ||
input_value: %Absinthe.Blueprint.Input.Value{ | ||
normalized: %Absinthe.Blueprint.Input.Object{ | ||
schema_node: %{} = schema_node, | ||
fields: fields | ||
} | ||
} | ||
} = _node | ||
) do | ||
schema_node = Absinthe.Type.unwrap(schema_node) | ||
private = Map.get(schema_node, :__private__, []) | ||
!(Keyword.get(private, :one_of, false) && Enum.count(fields) != 1) | ||
end | ||
|
||
defp valid?(_node), do: true | ||
|
||
@spec error(node :: Absinthe.Blueprint.node_t(), count :: integer()) :: Absinthe.Phase.Error.t() | ||
defp error(node, count) do | ||
%Absinthe.Phase.Error{ | ||
phase: __MODULE__, | ||
message: | ||
"OneOf Object \"#{node.name}\" must have exactly one non-null field but got #{count}." | ||
} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,18 @@ | ||
%{ | ||
"absinthe": {:hex, :absinthe, "1.7.6", "0b897365f98d068cfcb4533c0200a8e58825a4aeeae6ec33633ebed6de11773b", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7626951ca5eec627da960615b51009f3a774765406ff02722b1d818f17e5778"}, | ||
"absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.3", "74e0862f280424b7bc290f6f69e133268bce0b4e7db0218c7e129c5c2b1d3fd4", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "caffaea03c17ea7419fe07e4bc04c2399c47f0d8736900623dbf4749a826fd2c"}, | ||
"absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"}, | ||
"castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, | ||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, | ||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, | ||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, | ||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, | ||
"phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, | ||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, | ||
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, | ||
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, | ||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, | ||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, | ||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, | ||
"websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
defmodule AbsintheOneOfTest do | ||
use ExUnit.Case, async: true | ||
use Plug.Test | ||
|
||
import Phoenix.ConnTest, only: [json_response: 2] | ||
|
||
defmodule Pet do | ||
defmodule Cat do | ||
defstruct [:name, :number_of_lives] | ||
end | ||
|
||
defmodule Dog do | ||
defstruct [:name, :wags_tail] | ||
end | ||
|
||
defmodule Fish do | ||
defstruct [:name, :body_length_in_mm] | ||
end | ||
|
||
def pets(_, _, _) do | ||
{:ok, []} | ||
end | ||
|
||
def add(_, %{pet: %{cat: %{name: name, number_of_lives: number_of_lives}}}, _) do | ||
{:ok, %Cat{name: name, number_of_lives: number_of_lives}} | ||
end | ||
|
||
def add(_, %{pet: %{dog: %{name: name, wags_tail: wags_tail}}}, _) do | ||
{:ok, %Dog{name: name, wags_tail: wags_tail}} | ||
end | ||
|
||
def add(_, %{pet: %{fish: %{name: name, body_length_in_mm: body_length_in_mm}}}, _) do | ||
{:ok, %Fish{name: name, body_length_in_mm: body_length_in_mm}} | ||
end | ||
end | ||
|
||
defmodule TestSchema do | ||
use Absinthe.Schema | ||
|
||
@prototype_schema AbsintheOneOf.Directive | ||
|
||
query do | ||
field(:pets, list_of(:pet)) do | ||
resolve(&AbsintheOneOfTest.Pet.pets/3) | ||
end | ||
end | ||
|
||
mutation do | ||
field(:add, :pet) do | ||
arg(:pet, non_null(:pet_input)) | ||
resolve(&AbsintheOneOfTest.Pet.add/3) | ||
end | ||
end | ||
|
||
union(:pet) do | ||
types([:cat, :dog, :fish]) | ||
|
||
resolve_type(fn | ||
%AbsintheOneOfTest.Pet.Cat{}, _ -> :cat | ||
%AbsintheOneOfTest.Pet.Dog{}, _ -> :dog | ||
%AbsintheOneOfTest.Pet.Fish{}, _ -> :fish | ||
end) | ||
end | ||
|
||
object(:cat) do | ||
field(:name, non_null(:string)) | ||
field(:number_of_lives, :integer) | ||
end | ||
|
||
object(:dog) do | ||
field(:name, non_null(:string)) | ||
field(:wags_tail, :boolean) | ||
end | ||
|
||
object(:fish) do | ||
field(:name, non_null(:string)) | ||
field(:body_length_in_mm, :integer) | ||
end | ||
|
||
input_object(:pet_input) do | ||
directive(:one_of) | ||
field(:cat, :cat_input) | ||
field(:dog, :dog_input) | ||
field(:fish, :fish_input) | ||
end | ||
|
||
input_object(:cat_input) do | ||
field(:name, non_null(:string)) | ||
field(:number_of_lives, :integer) | ||
end | ||
|
||
input_object(:dog_input) do | ||
field(:name, non_null(:string)) | ||
field(:wags_tail, :boolean) | ||
end | ||
|
||
input_object(:fish_input) do | ||
field(:name, non_null(:string)) | ||
field(:body_length_in_mm, :integer) | ||
end | ||
end | ||
|
||
def pipeline(config, pipeline_opts) do | ||
config.schema_mod | ||
|> Absinthe.Pipeline.for_document(pipeline_opts) | ||
|> Absinthe.Pipeline.insert_after( | ||
Absinthe.Phase.Document.Validation.OnlyOneSubscription, | ||
AbsintheOneOf.Phase | ||
) | ||
end | ||
|
||
def call(conn) do | ||
conn | ||
|> Plug.Parsers.call( | ||
Plug.Parsers.init( | ||
parsers: [ | ||
:urlencoded, | ||
:multipart, | ||
:json, | ||
Absinthe.Plug.Parser | ||
], | ||
json_decoder: Jason | ||
) | ||
) | ||
|> Absinthe.Plug.call( | ||
Absinthe.Plug.init( | ||
schema: TestSchema, | ||
pipeline: {__MODULE__, :pipeline} | ||
) | ||
) | ||
end | ||
|
||
@mutation """ | ||
mutation add($pet: PetInput!) { | ||
add(pet: $pet) { | ||
... on Cat { | ||
name | ||
numberOfLives | ||
} | ||
... on Dog { | ||
name | ||
wagsTail | ||
} | ||
... on Fish { | ||
name | ||
bodyLengthInMm | ||
} | ||
__typename | ||
} | ||
} | ||
""" | ||
|
||
test "one_of 0 input" do | ||
variables = %{ | ||
"pet" => %{} | ||
} | ||
|
||
assert %{ | ||
"errors" => [ | ||
%{ | ||
"message" => | ||
"OneOf Object \"pet\" must have exactly one non-null field but got 0." | ||
} | ||
] | ||
} == | ||
conn(:post, "/", %{query: @mutation, variables: variables}) | ||
|> call() | ||
|> json_response(200) | ||
end | ||
|
||
test "one_of 1 input" do | ||
variables = %{ | ||
"pet" => %{ | ||
"cat" => %{ | ||
"name" => "Garfield", | ||
"numberOfLives" => 9 | ||
} | ||
} | ||
} | ||
|
||
assert %{ | ||
"data" => %{ | ||
"add" => %{"__typename" => "Cat", "name" => "Garfield", "numberOfLives" => 9} | ||
} | ||
} == | ||
conn(:post, "/", %{query: @mutation, variables: variables}) | ||
|> call() | ||
|> json_response(200) | ||
end | ||
|
||
test "one_of 2 input" do | ||
variables = %{ | ||
"pet" => %{ | ||
"cat" => %{ | ||
"name" => "Garfield", | ||
"numberOfLives" => 9 | ||
}, | ||
"dog" => %{ | ||
"name" => "Odie", | ||
"wagsTail" => true | ||
} | ||
} | ||
} | ||
|
||
assert %{ | ||
"errors" => [ | ||
%{ | ||
"message" => | ||
"OneOf Object \"pet\" must have exactly one non-null field but got 2." | ||
} | ||
] | ||
} == | ||
conn(:post, "/", %{query: @mutation, variables: variables}) | ||
|> call() | ||
|> json_response(200) | ||
end | ||
|
||
test "one_of 3 input" do | ||
variables = %{ | ||
"pet" => %{ | ||
"cat" => %{ | ||
"name" => "Garfield", | ||
"numberOfLives" => 9 | ||
}, | ||
"dog" => %{ | ||
"name" => "Odie", | ||
"wagsTail" => true | ||
}, | ||
"fish" => %{ | ||
"name" => "Nemo", | ||
"bodyLengthInMm" => 100 | ||
} | ||
} | ||
} | ||
|
||
assert %{ | ||
"errors" => [ | ||
%{ | ||
"message" => | ||
"OneOf Object \"pet\" must have exactly one non-null field but got 3." | ||
} | ||
] | ||
} == | ||
conn(:post, "/", %{query: @mutation, variables: variables}) | ||
|> call() | ||
|> json_response(200) | ||
end | ||
end |