object_interfaces_must_be_valid.ex (3169B)
1 defmodule Absinthe.Phase.Schema.Validation.ObjectInterfacesMustBeValid do 2 @moduledoc false 3 4 use Absinthe.Phase 5 alias Absinthe.Blueprint 6 7 def run(bp, _) do 8 bp = Blueprint.prewalk(bp, &handle_schemas/1) 9 {:ok, bp} 10 end 11 12 defp handle_schemas(%Blueprint.Schema.SchemaDefinition{} = schema) do 13 ifaces = 14 schema.type_definitions 15 |> Enum.filter(&match?(%Blueprint.Schema.InterfaceTypeDefinition{}, &1)) 16 |> Map.new(&{&1.identifier, &1}) 17 18 schema = Blueprint.prewalk(schema, &validate_objects(&1, ifaces)) 19 {:halt, schema} 20 end 21 22 defp handle_schemas(obj) do 23 obj 24 end 25 26 defp validate_objects(%struct{} = object, all_interfaces) 27 when struct in [ 28 Blueprint.Schema.ObjectTypeDefinition, 29 Blueprint.Schema.InterfaceTypeDefinition 30 ] do 31 check_transitive_interfaces(object, object.interfaces, all_interfaces, nil, []) 32 end 33 34 defp validate_objects(type, _) do 35 type 36 end 37 38 # check that the object declares it implements all interfaces up the 39 # hierarchy chain as per spec https://github.com/graphql/graphql-spec/blame/October2021/spec/Section%203%20--%20Type%20System.md#L1158-L1161 40 defp check_transitive_interfaces( 41 object, 42 [object_interface | tail], 43 all_interfaces, 44 implemented_by, 45 visited 46 ) do 47 current_interface = all_interfaces[object_interface] 48 49 if current_interface && current_interface.identifier in object.interfaces do 50 case current_interface do 51 %{interfaces: interfaces} = interface -> 52 # to prevent walking in cycles we need to filter out visited interfaces 53 interfaces = Enum.filter(interfaces, &(&1 not in visited)) 54 55 check_transitive_interfaces(object, tail ++ interfaces, all_interfaces, interface, [ 56 object_interface | visited 57 ]) 58 59 _ -> 60 check_transitive_interfaces(object, tail, all_interfaces, implemented_by, [ 61 object_interface | visited 62 ]) 63 end 64 else 65 detail = %{ 66 object: object.identifier, 67 interface: object_interface, 68 implemented_by: implemented_by 69 } 70 71 object |> put_error(error(object, detail)) 72 end 73 end 74 75 defp check_transitive_interfaces(object, [], _, _, _) do 76 object 77 end 78 79 defp error(object, data) do 80 %Absinthe.Phase.Error{ 81 message: explanation(data), 82 locations: [object.__reference__.location], 83 phase: __MODULE__, 84 extra: data 85 } 86 end 87 88 @description """ 89 Only interfaces may be present in an Object's interface list. 90 91 Reference: https://github.com/facebook/graphql/blob/master/spec/Section%203%20--%20Type%20System.md#interfaces 92 """ 93 94 def explanation(%{object: obj, interface: interface, implemented_by: nil}) do 95 """ 96 Type "#{obj}" must implement interface type "#{interface}" 97 98 #{@description} 99 """ 100 end 101 102 def explanation(%{object: obj, interface: interface, implemented_by: implemented}) do 103 """ 104 Type "#{obj}" must implement interface type "#{interface}" because it is implemented by "#{ 105 implemented.identifier 106 }". 107 108 #{@description} 109 """ 110 end 111 end