-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathinput_object.rb
309 lines (268 loc) · 10.8 KB
/
input_object.rb
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
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# 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
# Raised when an InputObject doesn't have any arguments defined and hasn't explicitly opted out of this requirement
class ArgumentsAreRequiredError < GraphQL::Error
def initialize(input_object_type)
message = "Input Object types must have arguments, but #{input_object_type.graphql_name} doesn't have any. Define an argument for this type, remove it from your schema, or add `has_no_arguments(true)` to its definition."
super(message)
end
end
# @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.
arg_defns = context ? context.types.arguments(self.class) : self.class.arguments(context).each_value
arg_defns.each 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 deconstruct_keys(keys = nil)
if keys.nil?
@ruby_style_hash
else
new_h = {}
keys.each { |k| @ruby_style_hash.key?(k) && new_h[k] = @ruby_style_hash[k] }
new_h
end
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 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 authorized?(obj, value, ctx)
# Authorize each argument (but this doesn't apply if `prepare` is implemented):
if value.respond_to?(:key?)
ctx.types.arguments(self).each do |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 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 one_of?
false # Re-defined when `OneOf` is added
end
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
suppress_redefinition_warning do
class_eval <<-RUBY, __FILE__, __LINE__
def #{method_name}
self[#{method_name.inspect}]
end
alias_method :#{method_name}, :#{method_name}
RUBY
end
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)
types = ctx.types
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 = ctx.types.arguments(self).reduce({}) do |m, (argument)|
if !input.key?(argument.graphql_name) && argument.type.non_null? && !argument.default_value? && types.argument(self, argument.graphql_name)
m[argument.graphql_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 = types.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
self.new(resolved_arguments, ruby_kwargs: resolved_arguments.keyword_arguments, context: ctx, defaults_used: nil)
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
# @param new_has_no_arguments [Boolean] Call with `true` to make this InputObject type ignore the requirement to have any defined arguments.
# @return [void]
def has_no_arguments(new_has_no_arguments)
@has_no_arguments = new_has_no_arguments
nil
end
# @return [Boolean] `true` if `has_no_arguments(true)` was configued
def has_no_arguments?
@has_no_arguments
end
def arguments(context = GraphQL::Query::NullContext.instance, require_defined_arguments = true)
if require_defined_arguments && !has_no_arguments? && !any_arguments?
warn(GraphQL::Schema::InputObject::ArgumentsAreRequiredError.new(self).message + "\n\nThis will raise an error in a future GraphQL-Ruby version.")
end
super(context, false)
end
private
# Suppress redefinition warning for objectId arguments
def suppress_redefinition_warning
verbose = $VERBOSE
$VERBOSE = nil
yield
ensure
$VERBOSE = verbose
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