Current section

Files

Jump to
absinthe lib absinthe phase schema validation object_must_implement_interfaces.ex
Raw

lib/absinthe/phase/schema/validation/object_must_implement_interfaces.ex

defmodule Absinthe.Phase.Schema.Validation.ObjectMustImplementInterfaces do
use Absinthe.Phase
alias Absinthe.Blueprint
def run(bp, _) do
bp = Blueprint.prewalk(bp, &handle_schemas/1)
{:ok, bp}
end
defp handle_schemas(%Blueprint.Schema.SchemaDefinition{} = schema) do
ifaces =
schema.type_definitions
|> Enum.filter(&match?(%Blueprint.Schema.InterfaceTypeDefinition{}, &1))
|> Map.new(&{&1.identifier, &1})
types =
schema.type_definitions
|> Map.new(&{&1.identifier, &1})
schema = Blueprint.prewalk(schema, &validate_objects(&1, ifaces, types))
{:halt, schema}
end
defp handle_schemas(obj) do
obj
end
@interface_types [
Blueprint.Schema.ObjectTypeDefinition,
Blueprint.Schema.InterfaceTypeDefinition
]
# Do not walk type extension definition as they are allowed to be invalid
defp validate_objects(%Blueprint.Schema.TypeExtensionDefinition{} = extension, _, _) do
{:halt, extension}
end
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)
_ -> object
end
end)
end
defp validate_objects(type, _, _) do
type
end
def validate_object(object, iface, types) do
case check_implements(iface, object, types) do
:ok ->
object
{:error, invalid_fields} ->
detail = %{
object: object.identifier,
interface: iface.identifier,
fields: invalid_fields
}
object |> put_error(error(object, detail))
end
end
defp error(object, data) do
%Absinthe.Phase.Error{
message: explanation(data),
locations: [object.__reference__.location],
phase: __MODULE__,
extra: data
}
end
@moduledoc false
@description """
An object type must be a super-set of all interfaces it implements.
* The object type must include a field of the same name for every field
defined in an interface.
* The object field must be of a type which is equal to or a sub-type of
the interface field (covariant).
* An object field type is a valid sub-type if it is equal to (the same
type as) the interface field type.
* An object field type is a valid sub-type if it is an Object type and the
interface field type is either an Interface type or a Union type and the
object field type is a possible type of the interface field type.
* An object field type is a valid sub-type if it is a List type and the
interface field type is also a List type and the list-item type of the
object field type is a valid sub-type of the list-item type of the
interface field type.
* An object field type is a valid sub-type if it is a Non-Null variant of a
valid sub-type of the interface field type.
* The object field must include an argument of the same name for every
argument defined in the interface field.
* The object field argument must accept the same type (invariant) as the
interface field argument.
* The object field may include additional arguments not defined in the
interface field, but any additional argument must not be required.
Reference: https://github.com/facebook/graphql/blob/master/spec/Section%203%20--%20Type%20System.md#object-type-validation
"""
def explanation(%{object: obj, interface: interface, fields: fields}) do
"""
Type "#{obj}" does not fully implement interface type "#{interface}" \
for fields #{inspect(fields)}
#{@description}
"""
end
def check_implements(interface, type, types) do
check_covariant(interface, type, nil, types)
end
defp check_covariant(
%Blueprint.Schema.InterfaceTypeDefinition{fields: ifields},
%{fields: type_fields},
_field_ident,
types
) do
Enum.reduce(ifields, [], fn %{identifier: ifield_ident} = ifield, invalid_fields ->
case Enum.find(type_fields, &(&1.identifier == ifield_ident)) do
nil ->
[ifield_ident | invalid_fields]
field ->
case check_covariant(ifield.type, field.type, ifield_ident, types) do
:ok ->
invalid_fields
{:error, invalid_field} ->
[invalid_field | invalid_fields]
end
end
end)
|> case do
[] ->
:ok
invalid_fields ->
{:error, invalid_fields}
end
end
defp check_covariant(
%Blueprint.Schema.InterfaceTypeDefinition{identifier: interface_ident},
interface_ident,
_field_ident,
_types
) do
:ok
end
defp check_covariant(
%Blueprint.Schema.InterfaceTypeDefinition{identifier: interface_ident},
field_type,
field_ident,
types
) do
%{interfaces: field_type_interfaces} = Map.get(types, field_type)
(interface_ident in field_type_interfaces && :ok) || {:error, field_ident}
end
defp check_covariant(
%wrapper{of_type: inner_type1},
%wrapper{of_type: inner_type2},
field_ident,
types
) do
check_covariant(inner_type1, inner_type2, field_ident, types)
end
defp check_covariant(
itype,
%Absinthe.Blueprint.TypeReference.NonNull{of_type: inner_type},
field_ident,
types
) do
check_covariant(itype, inner_type, field_ident, types)
end
defp check_covariant(%{identifier: identifier}, %{identifier: identifier}, _field_ident, _types) do
:ok
end
defp check_covariant(
%Blueprint.TypeReference.Name{name: name},
%Blueprint.TypeReference.Name{name: name},
_field_ident,
_types
) do
:ok
end
defp check_covariant(
%Blueprint.TypeReference.Name{name: iface_name},
%Blueprint.TypeReference.Name{name: type_name},
field_ident,
types
) do
{_, itype} = Enum.find(types, fn {_, %{name: name}} -> name == iface_name end)
{_, type} = Enum.find(types, fn {_, %{name: name}} -> name == type_name end)
check_covariant(itype, type, field_ident, types)
end
defp check_covariant(nil, _, field_ident, _), do: {:error, field_ident}
defp check_covariant(_, nil, field_ident, _), do: {:error, field_ident}
defp check_covariant(itype, type, field_ident, types) when is_atom(itype) do
itype = Map.get(types, itype)
check_covariant(itype, type, field_ident, types)
end
defp check_covariant(itype, type, field_ident, types) when is_atom(type) do
type = Map.get(types, type)
check_covariant(itype, type, field_ident, types)
end
defp check_covariant(_, _, field_ident, _types) do
{:error, field_ident}
end
end