object_must_implement_interfaces.ex (6673B)
1 defmodule Absinthe.Phase.Schema.Validation.ObjectMustImplementInterfaces do 2 use Absinthe.Phase 3 alias Absinthe.Blueprint 4 5 def run(bp, _) do 6 bp = Blueprint.prewalk(bp, &handle_schemas/1) 7 {:ok, bp} 8 end 9 10 defp handle_schemas(%Blueprint.Schema.SchemaDefinition{} = schema) do 11 ifaces = 12 schema.type_definitions 13 |> Enum.filter(&match?(%Blueprint.Schema.InterfaceTypeDefinition{}, &1)) 14 |> Map.new(&{&1.identifier, &1}) 15 16 types = 17 schema.type_definitions 18 |> Map.new(&{&1.identifier, &1}) 19 20 schema = Blueprint.prewalk(schema, &validate_objects(&1, ifaces, types)) 21 {:halt, schema} 22 end 23 24 defp handle_schemas(obj) do 25 obj 26 end 27 28 @interface_types [ 29 Blueprint.Schema.ObjectTypeDefinition, 30 Blueprint.Schema.InterfaceTypeDefinition 31 ] 32 33 defp validate_objects(%struct{} = object, ifaces, types) when struct in @interface_types do 34 Enum.reduce(object.interfaces, object, fn ident, object -> 35 case Map.fetch(ifaces, ident) do 36 {:ok, iface} -> validate_object(object, iface, types) 37 _ -> object 38 end 39 end) 40 end 41 42 defp validate_objects(type, _, _) do 43 type 44 end 45 46 def validate_object(object, iface, types) do 47 case check_implements(iface, object, types) do 48 :ok -> 49 object 50 51 {:error, invalid_fields} -> 52 detail = %{ 53 object: object.identifier, 54 interface: iface.identifier, 55 fields: invalid_fields 56 } 57 58 object |> put_error(error(object, detail)) 59 end 60 end 61 62 defp error(object, data) do 63 %Absinthe.Phase.Error{ 64 message: explanation(data), 65 locations: [object.__reference__.location], 66 phase: __MODULE__, 67 extra: data 68 } 69 end 70 71 @moduledoc false 72 73 @description """ 74 An object type must be a super-set of all interfaces it implements. 75 76 * The object type must include a field of the same name for every field 77 defined in an interface. 78 * The object field must be of a type which is equal to or a sub-type of 79 the interface field (covariant). 80 * An object field type is a valid sub-type if it is equal to (the same 81 type as) the interface field type. 82 * An object field type is a valid sub-type if it is an Object type and the 83 interface field type is either an Interface type or a Union type and the 84 object field type is a possible type of the interface field type. 85 * An object field type is a valid sub-type if it is a List type and the 86 interface field type is also a List type and the list-item type of the 87 object field type is a valid sub-type of the list-item type of the 88 interface field type. 89 * An object field type is a valid sub-type if it is a Non-Null variant of a 90 valid sub-type of the interface field type. 91 * The object field must include an argument of the same name for every 92 argument defined in the interface field. 93 * The object field argument must accept the same type (invariant) as the 94 interface field argument. 95 * The object field may include additional arguments not defined in the 96 interface field, but any additional argument must not be required. 97 98 Reference: https://github.com/facebook/graphql/blob/master/spec/Section%203%20--%20Type%20System.md#object-type-validation 99 """ 100 101 def explanation(%{object: obj, interface: interface, fields: fields}) do 102 """ 103 Type "#{obj}" does not fully implement interface type "#{interface}" \ 104 for fields #{inspect(fields)} 105 106 #{@description} 107 """ 108 end 109 110 def check_implements(interface, type, types) do 111 check_covariant(interface, type, nil, types) 112 end 113 114 defp check_covariant( 115 %Blueprint.Schema.InterfaceTypeDefinition{fields: ifields}, 116 %{fields: type_fields}, 117 _field_ident, 118 types 119 ) do 120 Enum.reduce(ifields, [], fn %{identifier: ifield_ident} = ifield, invalid_fields -> 121 case Enum.find(type_fields, &(&1.identifier == ifield_ident)) do 122 nil -> 123 [ifield_ident | invalid_fields] 124 125 field -> 126 case check_covariant(ifield.type, field.type, ifield_ident, types) do 127 :ok -> 128 invalid_fields 129 130 {:error, invalid_field} -> 131 [invalid_field | invalid_fields] 132 end 133 end 134 end) 135 |> case do 136 [] -> 137 :ok 138 139 invalid_fields -> 140 {:error, invalid_fields} 141 end 142 end 143 144 defp check_covariant( 145 %Blueprint.Schema.InterfaceTypeDefinition{identifier: interface_ident}, 146 interface_ident, 147 _field_ident, 148 _types 149 ) do 150 :ok 151 end 152 153 defp check_covariant( 154 %Blueprint.Schema.InterfaceTypeDefinition{identifier: interface_ident}, 155 field_type, 156 field_ident, 157 types 158 ) do 159 %{interfaces: field_type_interfaces} = Map.get(types, field_type) 160 (interface_ident in field_type_interfaces && :ok) || {:error, field_ident} 161 end 162 163 defp check_covariant( 164 %wrapper{of_type: inner_type1}, 165 %wrapper{of_type: inner_type2}, 166 field_ident, 167 types 168 ) do 169 check_covariant(inner_type1, inner_type2, field_ident, types) 170 end 171 172 defp check_covariant( 173 itype, 174 %Absinthe.Blueprint.TypeReference.NonNull{of_type: inner_type}, 175 field_ident, 176 types 177 ) do 178 check_covariant(itype, inner_type, field_ident, types) 179 end 180 181 defp check_covariant(%{identifier: identifier}, %{identifier: identifier}, _field_ident, _types) do 182 :ok 183 end 184 185 defp check_covariant( 186 %Blueprint.TypeReference.Name{name: name}, 187 %Blueprint.TypeReference.Name{name: name}, 188 _field_ident, 189 _types 190 ) do 191 :ok 192 end 193 194 defp check_covariant( 195 %Blueprint.TypeReference.Name{name: iface_name}, 196 %Blueprint.TypeReference.Name{name: type_name}, 197 field_ident, 198 types 199 ) do 200 {_, itype} = Enum.find(types, fn {_, %{name: name}} -> name == iface_name end) 201 {_, type} = Enum.find(types, fn {_, %{name: name}} -> name == type_name end) 202 203 check_covariant(itype, type, field_ident, types) 204 end 205 206 defp check_covariant(nil, _, field_ident, _), do: {:error, field_ident} 207 defp check_covariant(_, nil, field_ident, _), do: {:error, field_ident} 208 209 defp check_covariant(itype, type, field_ident, types) when is_atom(itype) do 210 itype = Map.get(types, itype) 211 check_covariant(itype, type, field_ident, types) 212 end 213 214 defp check_covariant(itype, type, field_ident, types) when is_atom(type) do 215 type = Map.get(types, type) 216 check_covariant(itype, type, field_ident, types) 217 end 218 219 defp check_covariant(_, _, field_ident, _types) do 220 {:error, field_ident} 221 end 222 end