Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for interface implementing other interfaces #1012

Merged
merged 11 commits into from
Dec 29, 2020
11 changes: 9 additions & 2 deletions lib/absinthe/blueprint/schema/interface_type_definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ defmodule Absinthe.Blueprint.Schema.InterfaceTypeDefinition do
description: nil,
fields: [],
directives: [],
interfaces: [],
interface_blueprints: [],
source_location: nil,
# Added by phases
flags: %{},
Expand All @@ -27,6 +29,8 @@ defmodule Absinthe.Blueprint.Schema.InterfaceTypeDefinition do
description: nil | String.t(),
fields: [Blueprint.Schema.FieldDefinition.t()],
directives: [Blueprint.Directive.t()],
interfaces: [String.t()],
interface_blueprints: [Blueprint.Draft.t()],
source_location: nil | Blueprint.SourceLocation.t(),
# Added by phases
flags: Blueprint.flags_t(),
Expand All @@ -40,12 +44,15 @@ defmodule Absinthe.Blueprint.Schema.InterfaceTypeDefinition do
fields: Blueprint.Schema.ObjectTypeDefinition.build_fields(type_def, schema),
identifier: type_def.identifier,
resolve_type: type_def.resolve_type,
definition: type_def.module
definition: type_def.module,
interfaces: type_def.interfaces
}
end

@interface_types [Schema.ObjectTypeDefinition, Schema.InterfaceTypeDefinition]

def find_implementors(iface, type_defs) do
for %Schema.ObjectTypeDefinition{} = obj <- type_defs,
for %struct{} = obj when struct in @interface_types <- type_defs,
iface.identifier in obj.interfaces,
do: obj.identifier
end
Expand Down
2 changes: 1 addition & 1 deletion lib/absinthe/blueprint/transform.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ defmodule Absinthe.Blueprint.Transform do
Blueprint.Schema.FieldDefinition => [:type, :arguments, :directives],
Blueprint.Schema.InputObjectTypeDefinition => [:fields, :directives],
Blueprint.Schema.InputValueDefinition => [:type, :default_value, :directives],
Blueprint.Schema.InterfaceTypeDefinition => [:fields, :directives],
Blueprint.Schema.InterfaceTypeDefinition => [:interfaces, :fields, :directives],
Blueprint.Schema.ObjectTypeDefinition => [:interfaces, :fields, :directives],
Blueprint.Schema.ScalarTypeDefinition => [:directives],
Blueprint.Schema.SchemaDefinition => [:directive_definitions, :type_definitions, :directives],
Expand Down
10 changes: 10 additions & 0 deletions lib/absinthe/language/interface_type_definition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ defmodule Absinthe.Language.InterfaceTypeDefinition do
description: nil,
fields: [],
directives: [],
interfaces: [],
loc: %{line: nil}

@type t :: %__MODULE__{
name: String.t(),
description: nil | String.t(),
fields: [Language.FieldDefinition.t()],
directives: [Language.Directive.t()],
interfaces: [Language.NamedType.t()],
loc: Language.loc_t()
}

Expand All @@ -25,10 +27,18 @@ defmodule Absinthe.Language.InterfaceTypeDefinition do
identifier: Macro.underscore(node.name) |> String.to_atom(),
fields: Absinthe.Blueprint.Draft.convert(node.fields, doc),
directives: Absinthe.Blueprint.Draft.convert(node.directives, doc),
interfaces: interfaces(node.interfaces, doc),
interface_blueprints: Absinthe.Blueprint.Draft.convert(node.interfaces, doc),
source_location: source_location(node)
}
end

defp interfaces(interfaces, doc) do
interfaces
|> Absinthe.Blueprint.Draft.convert(doc)
|> Enum.map(&(&1.name |> Macro.underscore() |> String.to_atom()))
end

defp source_location(%{loc: nil}), do: nil
defp source_location(%{loc: loc}), do: Blueprint.SourceLocation.at(loc)
end
Expand Down
78 changes: 78 additions & 0 deletions lib/absinthe/phase/schema/validation/no_interface_cycles.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
defmodule Absinthe.Phase.Schema.Validation.NoInterfaceCyles do
@moduledoc false

use Absinthe.Phase
alias Absinthe.Blueprint
alias Absinthe.Blueprint.Schema

def run(blueprint, _opts) do
blueprint = check(blueprint)

{:ok, blueprint}
end

defp check(blueprint) do
graph = :digraph.new([:cyclic])

try do
_ = build_interface_graph(blueprint, graph)

Blueprint.prewalk(blueprint, &validate_schema(&1, graph))
after
:digraph.delete(graph)
end
end

defp validate_schema(%Schema.InterfaceTypeDefinition{} = interface, graph) do
if cycle = :digraph.get_cycle(graph, interface.identifier) do
interface |> put_error(error(interface, cycle))
else
interface
end
end

defp validate_schema(node, _graph) do
node
end

defp build_interface_graph(blueprint, graph) do
_ = Blueprint.prewalk(blueprint, &vertex(&1, graph))
end

defp vertex(%Schema.InterfaceTypeDefinition{} = implementor, graph) do
:digraph.add_vertex(graph, implementor.identifier)

for interface <- implementor.interfaces do
edge(implementor, interface, graph)
end

implementor
end

defp vertex(implementor, _graph) do
implementor
end

# Add an edge, modeling the relationship between two interfaces
defp edge(implementor, interface, graph) do
:digraph.add_vertex(graph, interface)

:digraph.add_edge(graph, implementor.identifier, interface)

true
end

defp error(type, deps) do
%Absinthe.Phase.Error{
message:
String.trim("""
Interface Cycle Error

Interface `#{type.identifier}' forms a cycle via: (#{inspect(deps)})
"""),
locations: [type.__reference__.location],
phase: __MODULE__,
extra: type.identifier
}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ defmodule Absinthe.Phase.Schema.Validation.ObjectMustImplementInterfaces do
obj
end

defp validate_objects(%Blueprint.Schema.ObjectTypeDefinition{} = object, ifaces, types) do
@interface_types [
Blueprint.Schema.ObjectTypeDefinition,
Blueprint.Schema.InterfaceTypeDefinition
]

defp validate_objects(%struct{} = object, ifaces, types) when struct in @interface_types do
Enum.reduce(object.interfaces, object, fn ident, object ->
case Map.fetch(ifaces, ident) do
{:ok, iface} -> validate_object(object, iface, types)
Expand Down
1 change: 1 addition & 0 deletions lib/absinthe/pipeline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ defmodule Absinthe.Pipeline do
Phase.Schema.Validation.InterfacesMustResolveTypes,
Phase.Schema.Validation.ObjectInterfacesMustBeValid,
Phase.Schema.Validation.ObjectMustImplementInterfaces,
Phase.Schema.Validation.NoInterfaceCyles,
Phase.Schema.Validation.QueryTypeMustBeObject,
Phase.Schema.Validation.NamesMustBeValid,
Phase.Schema.RegisterTriggers,
Expand Down
4 changes: 2 additions & 2 deletions lib/absinthe/schema/notation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ defmodule Absinthe.Schema.Notation do
)
end

@placement {:interfaces, [under: [:object]]}
@placement {:interfaces, [under: [:object, :interface]]}
@doc """
Declare implemented interfaces for an object.

Expand Down Expand Up @@ -299,7 +299,7 @@ defmodule Absinthe.Schema.Notation do
end
```
"""
@placement {:interface_attribute, [under: [:object]]}
@placement {:interface_attribute, [under: [:object, :interface]]}
defmacro interface(identifier) do
__CALLER__
|> recordable!(:interface_attribute, @placement[:interface_attribute])
Expand Down
1 change: 1 addition & 0 deletions lib/absinthe/schema/notation/sdl_render.ex
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ defmodule Absinthe.Schema.Notation.SDL.Render do
"interface",
concat([
string(interface_type.name),
implements(interface_type, type_definitions),
directives(interface_type.directives, type_definitions)
]),
render_list(interface_type.fields, type_definitions)
Expand Down
2 changes: 2 additions & 0 deletions lib/absinthe/type/interface.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ defmodule Absinthe.Type.Interface do
description: binary,
fields: map,
identifier: atom,
interfaces: [Absinthe.Type.Interface.t()],
__private__: Keyword.t(),
definition: module,
__reference__: Type.Reference.t()
Expand All @@ -73,6 +74,7 @@ defmodule Absinthe.Type.Interface do
fields: nil,
identifier: nil,
resolve_type: nil,
interfaces: [],
__private__: [],
definition: nil,
__reference__: nil
Expand Down
5 changes: 5 additions & 0 deletions src/absinthe_parser.yrl
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ InterfaceTypeDefinition -> 'interface' Name '{' FieldDefinitionList '}' :
build_ast_node('InterfaceTypeDefinition', #{'name' => extract_binary('$2'), 'fields' => '$4'}, extract_location('$1')).
InterfaceTypeDefinition -> 'interface' Name Directives '{' FieldDefinitionList '}' :
build_ast_node('InterfaceTypeDefinition', #{'name' => extract_binary('$2'), 'directives' => '$3', 'fields' => '$5'}, extract_location('$1')).
InterfaceTypeDefinition -> 'interface' Name ImplementsInterfaces '{' FieldDefinitionList '}' :
build_ast_node('InterfaceTypeDefinition', #{'name' => extract_binary('$2'), 'interfaces' => '$3', 'fields' => '$5'}, extract_location('$1')).
InterfaceTypeDefinition -> 'interface' Name ImplementsInterfaces Directives '{' FieldDefinitionList '}' :
build_ast_node('InterfaceTypeDefinition', #{'name' => extract_binary('$2'), 'interfaces' => '$3', 'directives' => '$4', 'fields' => '$6'}, extract_location('$1')).


UnionTypeDefinition -> 'union' Name :
build_ast_node('UnionTypeDefinition', #{'name' => extract_binary('$2')}, extract_location('$1')).
Expand Down
19 changes: 16 additions & 3 deletions test/absinthe/schema/notation_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,14 @@ defmodule Absinthe.Schema.NotationTest do
interface :foo
end
""",
"Invalid schema notation: `interface_attribute` must only be used within `object`"
"Invalid schema notation: `interface_attribute` must only be used within `object`, `interface`"
)
end
end

describe "interfaces" do
test "can be under object as an attribute" do
assert_no_notation_error("InterfacesValid", """
assert_no_notation_error("ObjectInterfacesValid", """
interface :bar do
field :name, :string
resolve_type fn _, _ -> :foo end
Expand All @@ -223,6 +223,19 @@ defmodule Absinthe.Schema.NotationTest do
""")
end

test "can be under interface as an attribute" do
assert_no_notation_error("InterfaceInterfacesValid", """
interface :bar do
field :name, :string
resolve_type fn _, _ -> :foo end
end
interface :foo do
field :name, :string
interfaces [:bar]
end
""")
end

test "cannot be toplevel" do
assert_notation_error(
"InterfacesInvalid",
Expand All @@ -232,7 +245,7 @@ defmodule Absinthe.Schema.NotationTest do
end
interfaces [:bar]
""",
"Invalid schema notation: `interfaces` must only be used within `object`"
"Invalid schema notation: `interfaces` must only be used within `object`, `interface`"
)
end
end
Expand Down
36 changes: 36 additions & 0 deletions test/absinthe/schema/rule/no_interface_cycles_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule Absinthe.Schema.Rule.NoInterfacecyclesTest do
use Absinthe.Case, async: true

describe "rule" do
test "is enforced" do
assert_schema_error("interface_cycle_schema", [
%{
extra: :named,
locations: [
%{
file: "test/support/fixtures/dynamic/interface_cycle_schema.exs",
line: 24
}
],
message:
"Interface Cycle Error\n\nInterface `named' forms a cycle via: ([:named, :node, :named])",
path: [],
phase: Absinthe.Phase.Schema.Validation.NoInterfaceCyles
},
%{
extra: :node,
locations: [
%{
file: "test/support/fixtures/dynamic/interface_cycle_schema.exs",
line: 24
}
],
message:
"Interface Cycle Error\n\nInterface `node' forms a cycle via: ([:node, :named, :node])",
path: [],
phase: Absinthe.Phase.Schema.Validation.NoInterfaceCyles
}
])
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ defmodule Absinthe.Schema.Rule.ObjectMustImplementInterfacesTest do
use Absinthe.Schema
import_types Types

interface :parented do
field :parent, :named
field :another_parent, :named
end

interface :named do
interface :parented
field :name, :string
field :parent, :named
field :another_parent, :named
Expand Down Expand Up @@ -80,10 +86,48 @@ defmodule Absinthe.Schema.Rule.ObjectMustImplementInterfacesTest do
end

test "interfaces are propogated across type imports" do
assert %{named: [:cat, :dog, :user], favorite_foods: [:cat, :dog, :user]} ==
assert %{
named: [:cat, :dog, :user],
favorite_foods: [:cat, :dog, :user],
parented: [:named]
} ==
Schema.__absinthe_interface_implementors__()
end

defmodule InterfaceImplementsInterfaces do
use Absinthe.Schema

import_sdl """
interface Node {
id: ID!
}

interface Resource implements Node {
id: ID!
url: String
}

interface Image implements Resource & Node {
id: ID!
url: String
thumbnail: String
}

"""

query do
end
end

test "interfaces are set from sdl" do
assert %{
image: [],
node: [:image, :resource],
resource: [:image]
} ==
InterfaceImplementsInterfaces.__absinthe_interface_implementors__()
end

test "is enforced" do
assert_schema_error("invalid_interface_types", [
%{
Expand Down
Loading