File: literal_validator.rb

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (156 lines) | stat: -rw-r--r-- 5,922 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# frozen_string_literal: true
module GraphQL
  module StaticValidation
    # Test whether `ast_value` is a valid input for `type`
    class LiteralValidator
      def initialize(context:)
        @context = context
        @warden = context.warden
        @invalid_response = GraphQL::Query::InputValidationResult.new(valid: false, problems: [])
        @valid_response = GraphQL::Query::InputValidationResult.new(valid: true, problems: [])
      end

      def validate(ast_value, type)
        catch(:invalid) do
          recursively_validate(ast_value, type)
        end
      end

      private

      def replace_nulls_in(ast_value)
        case ast_value
        when Array
          ast_value.map { |v| replace_nulls_in(v) }
        when GraphQL::Language::Nodes::InputObject
          ast_value.to_h
        when GraphQL::Language::Nodes::NullValue
          nil
        else
          ast_value
        end
      end

      def recursively_validate(ast_value, type)
        if type.nil?
          # this means we're an undefined argument, see #present_input_field_values_are_valid
          maybe_raise_if_invalid(ast_value) do
            @invalid_response
          end
        elsif ast_value.is_a?(GraphQL::Language::Nodes::NullValue)
          maybe_raise_if_invalid(ast_value) do
            type.kind.non_null? ? @invalid_response : @valid_response
          end
        elsif type.kind.non_null?
          maybe_raise_if_invalid(ast_value) do
            ast_value.nil? ?
              @invalid_response :
              recursively_validate(ast_value, type.of_type)
          end
        elsif type.kind.list?
          item_type = type.of_type
          results = ensure_array(ast_value).map { |val| recursively_validate(val, item_type) }
          merge_results(results)
        elsif ast_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
          @valid_response
        elsif type.kind.scalar? && constant_scalar?(ast_value)
          maybe_raise_if_invalid(ast_value) do
            ruby_value = replace_nulls_in(ast_value)
            type.validate_input(ruby_value, @context)
          end
        elsif type.kind.enum?
          maybe_raise_if_invalid(ast_value) do
            if ast_value.is_a?(GraphQL::Language::Nodes::Enum)
              type.validate_input(ast_value.name, @context)
            else
              # if our ast_value isn't an Enum it's going to be invalid so return false
              @invalid_response
            end
          end
        elsif type.kind.input_object? && ast_value.is_a?(GraphQL::Language::Nodes::InputObject)
          maybe_raise_if_invalid(ast_value) do
            merge_results([
              required_input_fields_are_present(type, ast_value),
              present_input_field_values_are_valid(type, ast_value)
            ])
          end
        else
          maybe_raise_if_invalid(ast_value) do
            @invalid_response
          end
        end
      end

      # When `error_bubbling` is false, we want to bail on the first failure that we find.
      # Use `throw` to escape the current call stack, returning the invalid response.
      def maybe_raise_if_invalid(ast_value)
        ret = yield
        if !@context.schema.error_bubbling && !ret.valid?
          throw(:invalid, ret)
        else
          ret
        end
      end

      # The GraphQL grammar supports variables embedded within scalars but graphql.js
      # doesn't support it so we won't either for simplicity
      def constant_scalar?(ast_value)
        if ast_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
          false
        elsif ast_value.is_a?(Array)
          ast_value.all? { |element| constant_scalar?(element) }
        elsif ast_value.is_a?(GraphQL::Language::Nodes::InputObject)
          ast_value.arguments.all? { |arg| constant_scalar?(arg.value) }
        else
          true
        end
      end

      def required_input_fields_are_present(type, ast_node)
        # TODO - would be nice to use these to create an error message so the caller knows
        # that required fields are missing
        required_field_names = @warden.arguments(type)
          .select { |argument| argument.type.kind.non_null? && !argument.default_value? }
          .map!(&:name)

        present_field_names = ast_node.arguments.map(&:name)
        missing_required_field_names = required_field_names - present_field_names
        if @context.schema.error_bubbling
          missing_required_field_names.empty? ? @valid_response : @invalid_response
        else
          results = missing_required_field_names.map do |name|
            arg_type = @warden.get_argument(type, name).type
            recursively_validate(GraphQL::Language::Nodes::NullValue.new(name: name), arg_type)
          end
          if type.one_of? && ast_node.arguments.size != 1
            results << Query::InputValidationResult.from_problem("`#{type.graphql_name}` is a OneOf type, so only one argument may be given (instead of #{ast_node.arguments.size})")
          end
          merge_results(results)
        end
      end

      def present_input_field_values_are_valid(type, ast_node)
        results = ast_node.arguments.map do |value|
          field = @warden.get_argument(type, value.name)
          # we want to call validate on an argument even if it's an invalid one
          # so that our raise exception is on it instead of the entire InputObject
          field_type = field && field.type
          recursively_validate(value.value, field_type)
        end
        merge_results(results)
      end

      def ensure_array(value)
        value.is_a?(Array) ? value : [value]
      end

      def merge_results(results_list)
        merged_result = Query::InputValidationResult.new
        results_list.each do |inner_result|
          merged_result.merge_result!([], inner_result)
        end
        merged_result
      end
    end
  end
end