zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

variable_types_match.ex (5476B)


      1 defmodule Absinthe.Phase.Document.Arguments.VariableTypesMatch do
      2   @moduledoc false
      3 
      4   # Implements: 5.8.5. All Variable Usages are Allowed
      5   # Specifically, it implements "Variable usages must be compatible with the arguments they are passed to."
      6   # See relevant counter-example: https://spec.graphql.org/draft/#example-2028e
      7 
      8   use Absinthe.Phase
      9 
     10   alias Absinthe.Blueprint
     11   alias Absinthe.Blueprint.Document.{Operation, Fragment}
     12   alias Absinthe.Type
     13 
     14   def run(blueprint, _) do
     15     blueprint =
     16       blueprint
     17       |> check_operations()
     18       |> check_fragments()
     19 
     20     {:ok, blueprint}
     21   end
     22 
     23   def check_operations(%Blueprint{} = blueprint) do
     24     blueprint
     25     |> Map.update!(:operations, fn operations ->
     26       Enum.map(operations, &check_variable_types/1)
     27     end)
     28   end
     29 
     30   # A single fragment may be used by multiple operations.
     31   # Each operation may define its own variables.
     32   # This checks that each fragment is simultaneously consistent with the
     33   # variables defined in each of the operations which use that fragment.
     34   def check_fragments(%Blueprint{} = blueprint) do
     35     blueprint
     36     |> Map.update!(:fragments, fn fragments ->
     37       fragments
     38       |> Enum.map(fn fragment ->
     39         blueprint.operations
     40         |> Enum.filter(&Operation.uses?(&1, fragment))
     41         |> Enum.reduce(fragment, fn operation, fragment_acc ->
     42           check_variable_types(operation, fragment_acc)
     43         end)
     44       end)
     45     end)
     46   end
     47 
     48   def check_variable_types(%Operation{} = op) do
     49     variable_defs = Map.new(op.variable_definitions, &{&1.name, &1})
     50     Blueprint.prewalk(op, &check_variable_type(&1, op.name, variable_defs))
     51   end
     52 
     53   def check_variable_types(%Operation{} = op, %Fragment.Named{} = fragment) do
     54     variable_defs = Map.new(op.variable_definitions, &{&1.name, &1})
     55     Blueprint.prewalk(fragment, &check_variable_type(&1, op.name, variable_defs))
     56   end
     57 
     58   defp check_variable_type(%{schema_node: nil} = node, _, _) do
     59     {:halt, node}
     60   end
     61 
     62   defp check_variable_type(
     63          %Absinthe.Blueprint.Input.Argument{
     64            input_value: %Blueprint.Input.Value{
     65              raw: %{content: %Blueprint.Input.Variable{} = variable}
     66            }
     67          } = node,
     68          operation_name,
     69          variable_defs
     70        ) do
     71     location_type = node.input_value.schema_node
     72     location_definition = node.schema_node
     73 
     74     case Map.get(variable_defs, variable.name) do
     75       %{schema_node: variable_type} = variable_definition ->
     76         if types_compatible?(
     77              variable_type,
     78              location_type,
     79              variable_definition,
     80              location_definition
     81            ) do
     82           node
     83         else
     84           variable =
     85             put_error(
     86               variable,
     87               error(operation_name, variable, variable_definition, location_type)
     88             )
     89 
     90           {:halt, put_in(node.input_value.raw.content, variable)}
     91         end
     92 
     93       _ ->
     94         node
     95     end
     96   end
     97 
     98   defp check_variable_type(node, _, _) do
     99     node
    100   end
    101 
    102   def types_compatible?(type, type, _, _) do
    103     true
    104   end
    105 
    106   def types_compatible?(
    107         %Type.NonNull{of_type: nullable_variable_type},
    108         location_type,
    109         variable_definition,
    110         location_definition
    111       ) do
    112     types_compatible?(
    113       nullable_variable_type,
    114       location_type,
    115       variable_definition,
    116       location_definition
    117     )
    118   end
    119 
    120   def types_compatible?(
    121         %Type.List{of_type: item_variable_type},
    122         %Type.List{
    123           of_type: item_location_type
    124         },
    125         variable_definition,
    126         location_definition
    127       ) do
    128     types_compatible?(
    129       item_variable_type,
    130       item_location_type,
    131       variable_definition,
    132       location_definition
    133     )
    134   end
    135 
    136   # https://github.com/graphql/graphql-spec/blame/October2021/spec/Section%205%20--%20Validation.md#L1885-L1893
    137   # if argument has default value the variable can be nullable
    138   def types_compatible?(nullable_type, %Type.NonNull{of_type: nullable_type}, _, %{
    139         default_value: default_value
    140       })
    141       when not is_nil(default_value) do
    142     true
    143   end
    144 
    145   # https://github.com/graphql/graphql-spec/blame/main/spec/Section%205%20--%20Validation.md#L2000-L2005
    146   # This behavior is explicitly supported for compatibility with earlier editions of this specification.
    147   def types_compatible?(
    148         nullable_type,
    149         %Type.NonNull{of_type: nullable_type},
    150         %{
    151           default_value: value
    152         },
    153         _
    154       )
    155       when is_struct(value) do
    156     true
    157   end
    158 
    159   def types_compatible?(_, _, _, _) do
    160     false
    161   end
    162 
    163   defp error(operation_name, variable, variable_definition, location_type) do
    164     # need to rely on the type reference here, since the schema node may not be available
    165     # as the type could not exist in the schema
    166     variable_name = Absinthe.Blueprint.TypeReference.name(variable_definition.type)
    167 
    168     %Absinthe.Phase.Error{
    169       phase: __MODULE__,
    170       message:
    171         error_message(
    172           operation_name,
    173           variable,
    174           variable_name,
    175           Absinthe.Type.name(location_type)
    176         ),
    177       locations: [variable.source_location]
    178     }
    179   end
    180 
    181   def error_message(op, variable, variable_name, location_type) do
    182     start =
    183       case op || "" do
    184         "" -> "Variable"
    185         op -> "In operation `#{op}`, variable"
    186       end
    187 
    188     "#{start} `#{Blueprint.Input.inspect(variable)}` of type `#{variable_name}` found as input to argument of type `#{
    189       location_type
    190     }`."
    191   end
    192 end