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 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
|
# frozen_string_literal: true
module GraphQL
class Schema
class InputObject < GraphQL::Schema::Member
extend Forwardable
extend GraphQL::Schema::Member::HasArguments
extend GraphQL::Schema::Member::HasArguments::ArgumentObjectLoader
extend GraphQL::Schema::Member::ValidatesInput
extend GraphQL::Schema::Member::HasValidators
include GraphQL::Dig
# @return [GraphQL::Query::Context] The context for this query
attr_reader :context
# @return [GraphQL::Execution::Interpereter::Arguments] The underlying arguments instance
attr_reader :arguments
# Ruby-like hash behaviors, read-only
def_delegators :@ruby_style_hash, :keys, :values, :each, :map, :any?, :empty?
def initialize(arguments, ruby_kwargs:, context:, defaults_used:)
@context = context
@ruby_style_hash = ruby_kwargs
@arguments = arguments
# Apply prepares, not great to have it duplicated here.
self.class.arguments(context).each_value do |arg_defn|
ruby_kwargs_key = arg_defn.keyword
if @ruby_style_hash.key?(ruby_kwargs_key)
# Weirdly, procs are applied during coercion, but not methods.
# Probably because these methods require a `self`.
if arg_defn.prepare.is_a?(Symbol) || context.nil?
prepared_value = arg_defn.prepare_value(self, @ruby_style_hash[ruby_kwargs_key])
overwrite_argument(ruby_kwargs_key, prepared_value)
end
end
end
end
def to_h
unwrap_value(@ruby_style_hash)
end
def to_hash
to_h
end
def prepare
if @context
object = @context[:current_object]
# Pass this object's class with `as` so that messages are rendered correctly from inherited validators
Schema::Validator.validate!(self.class.validators, object, @context, @ruby_style_hash, as: self.class)
self
else
self
end
end
def self.authorized?(obj, value, ctx)
# Authorize each argument (but this doesn't apply if `prepare` is implemented):
if value.respond_to?(:key?)
arguments(ctx).each do |_name, input_obj_arg|
if value.key?(input_obj_arg.keyword) &&
!input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx)
return false
end
end
end
# It didn't early-return false:
true
end
def self.one_of
if !one_of?
if all_argument_definitions.any? { |arg| arg.type.non_null? }
raise ArgumentError, "`one_of` may not be used with required arguments -- add `required: false` to argument definitions to use `one_of`"
end
directive(GraphQL::Schema::Directive::OneOf)
end
end
def self.one_of?
false # Re-defined when `OneOf` is added
end
def unwrap_value(value)
case value
when Array
value.map { |item| unwrap_value(item) }
when Hash
value.reduce({}) do |h, (key, value)|
h.merge!(key => unwrap_value(value))
end
when InputObject
value.to_h
else
value
end
end
# Lookup a key on this object, it accepts new-style underscored symbols
# Or old-style camelized identifiers.
# @param key [Symbol, String]
def [](key)
if @ruby_style_hash.key?(key)
@ruby_style_hash[key]
elsif @arguments
@arguments[key]
else
nil
end
end
def key?(key)
@ruby_style_hash.key?(key) || (@arguments && @arguments.key?(key)) || false
end
# A copy of the Ruby-style hash
def to_kwargs
@ruby_style_hash.dup
end
class << self
def argument(*args, **kwargs, &block)
argument_defn = super(*args, **kwargs, &block)
if one_of?
if argument_defn.type.non_null?
raise ArgumentError, "Argument '#{argument_defn.path}' must be nullable because it is part of a OneOf type, add `required: false`."
end
if argument_defn.default_value?
raise ArgumentError, "Argument '#{argument_defn.path}' cannot have a default value because it is part of a OneOf type, remove `default_value: ...`."
end
end
# Add a method access
method_name = argument_defn.keyword
class_eval <<-RUBY, __FILE__, __LINE__
def #{method_name}
self[#{method_name.inspect}]
end
RUBY
argument_defn
end
def kind
GraphQL::TypeKinds::INPUT_OBJECT
end
# @api private
INVALID_OBJECT_MESSAGE = "Expected %{object} to be a key-value object."
def validate_non_null_input(input, ctx, max_errors: nil)
warden = ctx.warden
if input.is_a?(Array)
return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input, quirks_mode: true) })
end
if !(input.respond_to?(:to_h) || input.respond_to?(:to_unsafe_h))
# We're not sure it'll act like a hash, so reject it:
return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input, quirks_mode: true) })
end
# Inject missing required arguments
missing_required_inputs = self.arguments(ctx).reduce({}) do |m, (argument_name, argument)|
if !input.key?(argument_name) && argument.type.non_null? && warden.get_argument(self, argument_name)
m[argument_name] = nil
end
m
end
result = nil
[input, missing_required_inputs].each do |args_to_validate|
args_to_validate.each do |argument_name, value|
argument = warden.get_argument(self, argument_name)
# Items in the input that are unexpected
if argument.nil?
result ||= Query::InputValidationResult.new
result.add_problem("Field is not defined on #{self.graphql_name}", [argument_name])
else
# Items in the input that are expected, but have invalid values
argument_result = argument.type.validate_input(value, ctx)
result ||= Query::InputValidationResult.new
if !argument_result.valid?
result.merge_result!(argument_name, argument_result)
end
end
end
end
if one_of?
if input.size == 1
input.each do |name, value|
if value.nil?
result ||= Query::InputValidationResult.new
result.add_problem("'#{graphql_name}' requires exactly one argument, but '#{name}' was `null`.")
end
end
else
result ||= Query::InputValidationResult.new
result.add_problem("'#{graphql_name}' requires exactly one argument, but #{input.size} were provided.")
end
end
result
end
def coerce_input(value, ctx)
if value.nil?
return nil
end
arguments = coerce_arguments(nil, value, ctx)
ctx.query.after_lazy(arguments) do |resolved_arguments|
if resolved_arguments.is_a?(GraphQL::Error)
raise resolved_arguments
else
input_obj_instance = self.new(resolved_arguments, ruby_kwargs: resolved_arguments.keyword_arguments, context: ctx, defaults_used: nil)
input_obj_instance.prepare
end
end
end
# It's funny to think of a _result_ of an input object.
# This is used for rendering the default value in introspection responses.
def coerce_result(value, ctx)
# Allow the application to provide values as :snake_symbols, and convert them to the camelStrings
value = value.reduce({}) { |memo, (k, v)| memo[Member::BuildType.camelize(k.to_s)] = v; memo }
result = {}
arguments(ctx).each do |input_key, input_field_defn|
input_value = value[input_key]
if value.key?(input_key)
result[input_key] = if input_value.nil?
nil
else
input_field_defn.type.coerce_result(input_value, ctx)
end
end
end
result
end
end
private
def overwrite_argument(key, value)
# Argument keywords come in frozen from the interpreter, dup them before modifying them.
if @ruby_style_hash.frozen?
@ruby_style_hash = @ruby_style_hash.dup
end
@ruby_style_hash[key] = value
end
end
end
end
|