lib/graphql/query/variables.rb



# frozen_string_literal: true
module GraphQL
  class Query
    # Read-only access to query variables, applying default values if needed.
    class Variables
      extend Forwardable

      # @return [Array<GraphQL::Query::VariableValidationError>]  Any errors encountered when parsing the provided variables and literal values
      attr_reader :errors

      attr_reader :context

      def initialize(ctx, ast_variables, provided_variables)
        schema = ctx.schema
        @context = ctx

        @provided_variables = deep_stringify(provided_variables)
        @errors = []
        @storage = ast_variables.each_with_object({}) do |ast_variable, memo|
          if schema.validate_max_errors && schema.validate_max_errors <= @errors.count
            add_max_errors_reached_message
            break
          end
          # Find the right value for this variable:
          # - First, use the value provided at runtime
          # - Then, fall back to the default value from the query string
          # If it's still nil, raise an error if it's required.
          variable_type = schema.type_from_ast(ast_variable.type, context: ctx)
          if variable_type.nil? || !variable_type.unwrap.kind.input?
            # Pass -- it will get handled by a validator
          else
            variable_name = ast_variable.name
            default_value = ast_variable.default_value
            provided_value = @provided_variables[variable_name]
            value_was_provided =  @provided_variables.key?(variable_name)
            max_errors = schema.validate_max_errors - @errors.count if schema.validate_max_errors
            begin
              validation_result = variable_type.validate_input(provided_value, ctx, max_errors: max_errors)
              if validation_result.valid?
                if value_was_provided
                  # Add the variable if a value was provided
                  memo[variable_name] = provided_value
                elsif default_value != nil
                  memo[variable_name] = if default_value.is_a?(Language::Nodes::NullValue)
                    nil
                  else
                    default_value
                  end
                end
              end
            rescue GraphQL::ExecutionError => ex
              # TODO: This should really include the path to the problematic node in the variable value
              # like InputValidationResults generated by validate_non_null_input but unfortunately we don't
              # have this information available in the coerce_input call chain. Note this path is the path
              # that appears under errors.extensions.problems.path and NOT the result path under errors.path.
              validation_result = GraphQL::Query::InputValidationResult.from_problem(ex.message)
            end

            if !validation_result.valid?
              @errors << GraphQL::Query::VariableValidationError.new(ast_variable, variable_type, provided_value, validation_result)
            end
          end
        end
      end

      def_delegators :@storage, :length, :key?, :[], :fetch, :to_h

      private

      def deep_stringify(val)
        case val
        when Array
          val.map { |v| deep_stringify(v) }
        when Hash
          new_val = {}
          val.each do |k, v|
            new_val[k.to_s] = deep_stringify(v)
          end
          new_val
        else
          val
        end
      end

      def add_max_errors_reached_message
        message = "Too many errors processing variables, max validation error limit reached. Execution aborted"
        validation_result = GraphQL::Query::InputValidationResult.from_problem(message)
        errors << GraphQL::Query::VariableValidationError.new(nil, nil, nil, validation_result, msg: message)
      end
    end
  end
end