diff --git a/lib/absinthe/phase/schema/validation/unique_field_names.ex b/lib/absinthe/phase/schema/validation/unique_field_names.ex new file mode 100644 index 0000000000..54c4f73499 --- /dev/null +++ b/lib/absinthe/phase/schema/validation/unique_field_names.ex @@ -0,0 +1,66 @@ +defmodule Absinthe.Phase.Schema.Validation.UniqueFieldNames do + @moduledoc false + + @behaviour Absinthe.Phase + alias Absinthe.Blueprint + + def run(bp, _) do + bp = + bp + |> Blueprint.prewalk(&handle_schemas(&1, :name)) + + {:ok, bp} + end + + defp handle_schemas(%Blueprint.Schema.SchemaDefinition{} = schema, key) do + schema = Blueprint.prewalk(schema, &validate_types(&1, key)) + {:halt, schema} + end + + defp handle_schemas(obj, _) do + obj + end + + defp validate_types(%type{} = object, key) + when type in [ + Blueprint.Schema.InputObjectTypeDefinition, + Blueprint.Schema.InterfaceTypeDefinition, + Blueprint.Schema.ObjectTypeDefinition + ] do + fields = + for field <- object.fields do + if duplicate?(object.fields, field, key) do + Absinthe.Phase.put_error(field, error(field, object)) + else + field + end + end + + %{object | fields: fields} + end + + defp validate_types(type, _) do + type + end + + defp duplicate?(fields, field, key) do + Enum.count(fields, &(Map.get(&1, key) == Map.get(field, key))) > 1 + end + + defp error(field, object) do + %Absinthe.Phase.Error{ + message: explanation(field, object), + locations: [field.__reference__.location], + phase: __MODULE__, + extra: field + } + end + + def explanation(field, object) do + """ + The field #{inspect(field.name)} is not unique in type #{inspect(object.name)}. + + The field must have a unique name within that Object type; no two fields may share the same name. + """ + end +end diff --git a/lib/absinthe/pipeline.ex b/lib/absinthe/pipeline.ex index 52d12859bd..2260ca5780 100644 --- a/lib/absinthe/pipeline.ex +++ b/lib/absinthe/pipeline.ex @@ -151,6 +151,7 @@ defmodule Absinthe.Pipeline do Phase.Schema.Validation.NoInterfaceCyles, Phase.Schema.Validation.QueryTypeMustBeObject, Phase.Schema.Validation.NamesMustBeValid, + Phase.Schema.Validation.UniqueFieldNames, Phase.Schema.RegisterTriggers, Phase.Schema.MarkReferenced, Phase.Schema.ReformatDescriptions, diff --git a/test/absinthe/schema/rule/unique_field_names_test.exs b/test/absinthe/schema/rule/unique_field_names_test.exs new file mode 100644 index 0000000000..8e4415ab77 --- /dev/null +++ b/test/absinthe/schema/rule/unique_field_names_test.exs @@ -0,0 +1,75 @@ +defmodule Absinthe.Schema.Rule.UniqueFieldNamesTest do + use Absinthe.Case, async: true + + @duplicate_object_fields ~S( + defmodule DuplicateObjectFields do + use Absinthe.Schema + + query do + end + + import_sdl """ + type Dog { + name: String! + name: String + } + """ + end + ) + + @duplicate_interface_fields ~S( + defmodule DuplicateInterfaceFields do + use Absinthe.Schema + + query do + end + + import_sdl """ + interface Animal { + tail: Boolean + tail: Boolean + } + """ + end + ) + + @duplicate_input_fields ~S( + defmodule DuplicateInputFields do + use Absinthe.Schema + + query do + end + + import_sdl """ + input AnimalInput { + species: String! + species: String! + } + """ + end + ) + + test "errors on non unique object field names" do + error = ~r/The field \"name\" is not unique in type \"Dog\"./ + + assert_raise(Absinthe.Schema.Error, error, fn -> + Code.eval_string(@duplicate_object_fields) + end) + end + + test "errors on non unique interface field names" do + error = ~r/The field \"tail\" is not unique in type \"Animal\"./ + + assert_raise(Absinthe.Schema.Error, error, fn -> + Code.eval_string(@duplicate_interface_fields) + end) + end + + test "errors on non unique input field names" do + error = ~r/The field \"species\" is not unique in type \"AnimalInput\"./ + + assert_raise(Absinthe.Schema.Error, error, fn -> + Code.eval_string(@duplicate_input_fields) + end) + end +end