Skip to content

Commit

Permalink
Add support for interface implementing other interfaces (#1012)
Browse files Browse the repository at this point in the history
* Add support in parser for interfaces implementing interfaces

* Walk interfaces on interfaces in AST

* Validate interface implementations on interfaces

* Add interfaces on interfaces support in macro schema

* Add interface info on interface type

* Add validation disallowing interface cycles

* Add no interface cycle validation to schema pipeline

* Test SDL interfaces

* Add interface implements to sdl render

* Improve tests for interfaces implementing interfaces

* Fix comment
  • Loading branch information
maartenvanvliet committed Dec 29, 2020
1 parent b4e946c commit c707638
Show file tree
Hide file tree
Showing 15 changed files with 277 additions and 12 deletions.
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

0 comments on commit c707638

Please sign in to comment.