grape 3.2.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -0
- data/README.md +116 -43
- data/UPGRADING.md +336 -1
- data/grape.gemspec +5 -5
- data/lib/grape/api/instance.rb +7 -7
- data/lib/grape/api.rb +22 -25
- data/lib/grape/cookies.rb +2 -6
- data/lib/grape/declared_params_handler.rb +48 -50
- data/lib/grape/dsl/callbacks.rb +9 -3
- data/lib/grape/dsl/desc.rb +8 -2
- data/lib/grape/dsl/entity.rb +88 -0
- data/lib/grape/dsl/helpers.rb +27 -7
- data/lib/grape/dsl/inside_route.rb +38 -129
- data/lib/grape/dsl/logger.rb +3 -5
- data/lib/grape/dsl/parameters.rb +32 -38
- data/lib/grape/dsl/request_response.rb +53 -48
- data/lib/grape/dsl/rescue_options.rb +24 -0
- data/lib/grape/dsl/routing.rb +51 -35
- data/lib/grape/dsl/settings.rb +14 -8
- data/lib/grape/dsl/version_options.rb +23 -0
- data/lib/grape/endpoint/options.rb +19 -0
- data/lib/grape/endpoint.rb +96 -68
- data/lib/grape/env.rb +1 -3
- data/lib/grape/error_formatter/base.rb +23 -20
- data/lib/grape/error_formatter/json.rb +8 -4
- data/lib/grape/error_formatter/txt.rb +10 -10
- data/lib/grape/exceptions/base.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +45 -0
- data/lib/grape/exceptions/internal_server_error.rb +16 -0
- data/lib/grape/exceptions/validation.rb +14 -0
- data/lib/grape/exceptions/validation_array_errors.rb +4 -0
- data/lib/grape/exceptions/validation_errors.rb +12 -20
- data/lib/grape/formatter/serializable_hash.rb +5 -9
- data/lib/grape/json.rb +38 -2
- data/lib/grape/locale/en.yml +2 -0
- data/lib/grape/middleware/auth/base.rb +2 -3
- data/lib/grape/middleware/auth/dsl.rb +23 -8
- data/lib/grape/middleware/base.rb +22 -33
- data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
- data/lib/grape/middleware/error.rb +152 -62
- data/lib/grape/middleware/formatter.rb +66 -50
- data/lib/grape/middleware/precomputed_content_types.rb +46 -0
- data/lib/grape/middleware/stack.rb +5 -6
- data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
- data/lib/grape/middleware/versioner/base.rb +34 -38
- data/lib/grape/middleware/versioner/header.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +8 -3
- data/lib/grape/namespace.rb +3 -3
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
- data/lib/grape/parser/json.rb +1 -1
- data/lib/grape/path.rb +14 -17
- data/lib/grape/request.rb +15 -8
- data/lib/grape/router/mustermann_pattern.rb +44 -0
- data/lib/grape/router/pattern.rb +6 -10
- data/lib/grape/router.rb +28 -42
- data/lib/grape/serve_stream/file_body.rb +1 -0
- data/lib/grape/serve_stream/sendfile_response.rb +3 -5
- data/lib/grape/serve_stream/stream_response.rb +1 -0
- data/lib/grape/testing.rb +33 -0
- data/lib/grape/util/base_inheritable.rb +13 -16
- data/lib/grape/util/inheritable_setting.rb +44 -27
- data/lib/grape/util/inheritable_values.rb +7 -3
- data/lib/grape/util/lazy/base.rb +16 -0
- data/lib/grape/util/lazy/block.rb +2 -9
- data/lib/grape/util/lazy/value.rb +2 -9
- data/lib/grape/util/lazy/value_enumerable.rb +13 -16
- data/lib/grape/util/media_type.rb +1 -4
- data/lib/grape/util/path_normalizer.rb +34 -0
- data/lib/grape/util/registry.rb +1 -1
- data/lib/grape/util/stackable_values.rb +11 -8
- data/lib/grape/validations/attributes_iterator.rb +13 -13
- data/lib/grape/validations/coerce_options.rb +21 -0
- data/lib/grape/validations/oneof_collector.rb +39 -0
- data/lib/grape/validations/param_scope_tracker.rb +14 -9
- data/lib/grape/validations/params_documentation.rb +25 -23
- data/lib/grape/validations/params_scope.rb +54 -172
- data/lib/grape/validations/shared_options.rb +19 -0
- data/lib/grape/validations/types/array_coercer.rb +2 -2
- data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
- data/lib/grape/validations/types/primitive_coercer.rb +10 -5
- data/lib/grape/validations/types/set_coercer.rb +1 -1
- data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
- data/lib/grape/validations/types.rb +23 -30
- data/lib/grape/validations/validations_spec.rb +149 -0
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
- data/lib/grape/validations/validators/base.rb +39 -22
- data/lib/grape/validations/validators/coerce_validator.rb +5 -3
- data/lib/grape/validations/validators/default_validator.rb +7 -8
- data/lib/grape/validations/validators/except_values_validator.rb +3 -2
- data/lib/grape/validations/validators/length_validator.rb +1 -1
- data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
- data/lib/grape/validations/validators/oneof_validator.rb +49 -0
- data/lib/grape/validations/validators/values_validator.rb +5 -5
- data/lib/grape/version.rb +1 -1
- data/lib/grape/xml.rb +8 -1
- data/lib/grape.rb +6 -6
- metadata +34 -18
- data/lib/grape/middleware/globals.rb +0 -14
|
@@ -38,9 +38,10 @@ module Grape
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def self.attr_key(declared_param_attr)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
case declared_param_attr
|
|
42
|
+
when self
|
|
43
|
+
attr_key(declared_param_attr.key)
|
|
44
|
+
when Hash
|
|
44
45
|
declared_param_attr.transform_values { |value| attrs_keys(value) }
|
|
45
46
|
else
|
|
46
47
|
declared_param_attr
|
|
@@ -86,7 +87,8 @@ module Grape
|
|
|
86
87
|
end
|
|
87
88
|
|
|
88
89
|
def configuration
|
|
89
|
-
|
|
90
|
+
config = @api.configuration
|
|
91
|
+
config.is_a?(Grape::Util::Lazy::Base) ? config.evaluate : config
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
# @return [Boolean] whether or not this entire scope needs to be
|
|
@@ -195,13 +197,10 @@ module Grape
|
|
|
195
197
|
private
|
|
196
198
|
|
|
197
199
|
def build_full_path
|
|
198
|
-
if nested?
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
else
|
|
203
|
-
[]
|
|
204
|
-
end
|
|
200
|
+
return @parent.full_path + [@element] if nested?
|
|
201
|
+
return @parent.full_path if lateral?
|
|
202
|
+
|
|
203
|
+
[]
|
|
205
204
|
end
|
|
206
205
|
|
|
207
206
|
# Add a new parameter which should be renamed when using the +#declared+
|
|
@@ -315,12 +314,9 @@ module Grape
|
|
|
315
314
|
# push_declared_params cannot be called on the frozen scope later.
|
|
316
315
|
def configure_declared_params
|
|
317
316
|
push_renamed_param(full_path, @element_renamed) if @element_renamed
|
|
317
|
+
return @parent.push_declared_params [{ @element => @declared_params }] if nested?
|
|
318
318
|
|
|
319
|
-
|
|
320
|
-
@parent.push_declared_params [@element => @declared_params]
|
|
321
|
-
else
|
|
322
|
-
@api.inheritable_setting.namespace_stackable[:declared_params] = @declared_params
|
|
323
|
-
end
|
|
319
|
+
@api.inheritable_setting.namespace_stackable[:declared_params] = @declared_params
|
|
324
320
|
ensure
|
|
325
321
|
@declared_params = nil
|
|
326
322
|
end
|
|
@@ -332,139 +328,67 @@ module Grape
|
|
|
332
328
|
end
|
|
333
329
|
|
|
334
330
|
def validates(attrs, validations)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
default = validations[:default]
|
|
338
|
-
values = validations[:values].is_a?(Hash) ? validations.dig(:values, :value) : validations[:values]
|
|
339
|
-
except_values = validations[:except_values].is_a?(Hash) ? validations.dig(:except_values, :value) : validations[:except_values]
|
|
340
|
-
|
|
341
|
-
# NB. values and excepts should be nil, Proc, Array, or Range.
|
|
342
|
-
# Specifically, values should NOT be a Hash
|
|
343
|
-
# use values or excepts to guess coerce type when stated type is Array
|
|
344
|
-
coerce_type = guess_coerce_type(coerce_type, values, except_values)
|
|
345
|
-
|
|
346
|
-
# default value should be present in values array, if both exist and are not procs
|
|
347
|
-
check_incompatible_option_values(default, values, except_values)
|
|
331
|
+
process_oneof!(validations) if validations.key?(:oneof)
|
|
332
|
+
spec = ValidationsSpec.from(validations)
|
|
348
333
|
|
|
349
|
-
|
|
350
|
-
validate_value_coercion(coerce_type, values, except_values)
|
|
334
|
+
document_params(attrs, spec)
|
|
351
335
|
|
|
352
|
-
|
|
336
|
+
# Presence runs first — `required` is forwarded to every subsequent
|
|
337
|
+
# validator (some short-circuit on it).
|
|
338
|
+
validate_presence(spec, attrs)
|
|
353
339
|
|
|
354
|
-
|
|
340
|
+
# Coerce runs second — later validators see the typed value.
|
|
341
|
+
validate_coerce(spec, attrs)
|
|
355
342
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
# Before we run the rest of the validators, let's handle
|
|
360
|
-
# whatever coercion so that we are working with correctly
|
|
361
|
-
# type casted values
|
|
362
|
-
coerce_type validations.extract!(:coerce, :coerce_with, :coerce_message), attrs, required, opts
|
|
363
|
-
|
|
364
|
-
validations.each do |type, options|
|
|
365
|
-
# Don't try to look up validators for documentation params that don't have one.
|
|
366
|
-
next if RESERVED_DOCUMENTATION_KEYWORDS.include?(type)
|
|
367
|
-
|
|
368
|
-
validate(type, options, attrs, required, opts)
|
|
369
|
-
end
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
# Validate and comprehend the +:type+, +:types+, and +:coerce_with+
|
|
373
|
-
# options that have been supplied to the parameter declaration.
|
|
374
|
-
# The +:type+ and +:types+ options will be removed from the
|
|
375
|
-
# validations list, replaced appropriately with +:coerce+ and
|
|
376
|
-
# +:coerce_with+ options that will later be passed to
|
|
377
|
-
# {Validators::CoerceValidator}. The type that is returned may be
|
|
378
|
-
# used for documentation and further validation of parameter
|
|
379
|
-
# options.
|
|
380
|
-
#
|
|
381
|
-
# @param validations [Hash] list of validations supplied to the
|
|
382
|
-
# parameter declaration
|
|
383
|
-
# @return [class-like] type to which the parameter will be coerced
|
|
384
|
-
# @raise [ArgumentError] if the given type options are invalid
|
|
385
|
-
def infer_coercion(validations)
|
|
386
|
-
raise ArgumentError, ':type may not be supplied with :types' if validations.key?(:type) && validations.key?(:types)
|
|
387
|
-
|
|
388
|
-
validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type)
|
|
389
|
-
validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type)
|
|
390
|
-
validations[:coerce] = (options_key?(:types, :value, validations) ? validations[:types][:value] : validations[:types]) if validations.key?(:types)
|
|
391
|
-
validations[:coerce_message] = (options_key?(:types, :message, validations) ? validations[:types][:message] : nil) if validations.key?(:types)
|
|
392
|
-
|
|
393
|
-
validations.delete(:types) if validations.key?(:types)
|
|
394
|
-
|
|
395
|
-
coerce_type = validations[:coerce]
|
|
396
|
-
|
|
397
|
-
# Special case - when the argument is a single type that is a
|
|
398
|
-
# variant-type collection.
|
|
399
|
-
if Types.multiple?(coerce_type) && validations.key?(:type)
|
|
400
|
-
validations[:coerce] = Types::VariantCollectionCoercer.new(
|
|
401
|
-
coerce_type,
|
|
402
|
-
validations.delete(:coerce_with)
|
|
403
|
-
)
|
|
343
|
+
spec.validator_entries.each do |type, options|
|
|
344
|
+
validate(type, options, attrs, spec.required?, spec.shared_opts)
|
|
404
345
|
end
|
|
405
|
-
validations.delete(:type)
|
|
406
|
-
|
|
407
|
-
coerce_type
|
|
408
346
|
end
|
|
409
347
|
|
|
410
|
-
# Enforce correct usage of :coerce_with
|
|
411
|
-
# We do not allow coercion without a type, nor with
|
|
412
|
-
#
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
raise ArgumentError, 'must supply type for coerce_with' unless validations.key?(:coerce)
|
|
418
|
-
|
|
419
|
-
# but not special JSON types, which
|
|
420
|
-
# already imply coercion method
|
|
421
|
-
return unless SPECIAL_JSON.include?(validations[:coerce])
|
|
348
|
+
# Enforce correct usage of :coerce_with on a CoerceOptions.
|
|
349
|
+
# We do not allow coercion without a type, nor with +JSON+ as a type
|
|
350
|
+
# since that defines its own coercion method.
|
|
351
|
+
def check_coerce_with(coerce_options)
|
|
352
|
+
return unless coerce_options.coerce_method
|
|
353
|
+
raise ArgumentError, 'must supply type for coerce_with' unless coerce_options.type
|
|
354
|
+
return unless SPECIAL_JSON.include?(coerce_options.type)
|
|
422
355
|
|
|
423
356
|
raise ArgumentError, 'coerce_with disallowed for type: JSON'
|
|
424
357
|
end
|
|
425
358
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
# This validation has special handling since it is
|
|
429
|
-
# composited from more than one +requires+/+optional+
|
|
430
|
-
# parameter, and needs to be run before most other
|
|
431
|
-
# validations.
|
|
432
|
-
def coerce_type(validations, attrs, required, opts)
|
|
433
|
-
check_coerce_with(validations)
|
|
359
|
+
def validate_presence(spec, attrs)
|
|
360
|
+
return unless spec.required?
|
|
434
361
|
|
|
435
|
-
|
|
436
|
-
# evaluated on its base instance (no configuration supplied yet),
|
|
437
|
-
# configuration[:some_type] evaluates to nil. Skipping instantiation
|
|
438
|
-
# here is correct — the real mounted instance will replay this step with
|
|
439
|
-
# the actual type value.
|
|
440
|
-
return unless validations[:coerce]
|
|
441
|
-
|
|
442
|
-
coerce_options = {
|
|
443
|
-
type: validations[:coerce],
|
|
444
|
-
method: validations[:coerce_with],
|
|
445
|
-
message: validations[:coerce_message]
|
|
446
|
-
}
|
|
447
|
-
validate('coerce', coerce_options, attrs, required, opts)
|
|
362
|
+
validate('presence', spec.presence_options, attrs, true, spec.shared_opts)
|
|
448
363
|
end
|
|
449
364
|
|
|
450
|
-
def
|
|
451
|
-
|
|
365
|
+
def validate_coerce(spec, attrs)
|
|
366
|
+
coerce_options = spec.coerce_options
|
|
367
|
+
check_coerce_with(coerce_options)
|
|
368
|
+
# Falsy check is intentional: when a remountable API is first evaluated
|
|
369
|
+
# on its base instance (no configuration supplied yet),
|
|
370
|
+
# configuration[:some_type] evaluates to nil. Skipping instantiation
|
|
371
|
+
# here is correct — the real mounted instance will replay this step
|
|
372
|
+
# with the actual type value.
|
|
373
|
+
return unless coerce_options.type
|
|
452
374
|
|
|
453
|
-
|
|
454
|
-
next if !values || values.is_a?(Proc)
|
|
455
|
-
return values.first.class if values.is_a?(Range) || !values.empty?
|
|
456
|
-
end
|
|
457
|
-
coerce_type
|
|
375
|
+
validate('coerce', coerce_options, attrs, spec.required?, spec.shared_opts)
|
|
458
376
|
end
|
|
459
377
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
378
|
+
# Translate a `oneof: [proc, proc, ...]` declaration into a list of
|
|
379
|
+
# captured validator arrays — one array per variant. Each variant's
|
|
380
|
+
# block is evaluated in its own +ParamsScope+ backed by an
|
|
381
|
+
# {OneofCollector} so the full params DSL is available inside variants
|
|
382
|
+
# and the resulting validators are kept out of the real API's
|
|
383
|
+
# registration list.
|
|
384
|
+
def process_oneof!(validations)
|
|
385
|
+
raise ArgumentError, 'oneof: requires type: Hash' unless validations[:type] == Hash
|
|
464
386
|
|
|
465
|
-
|
|
387
|
+
variants = validations[:oneof]
|
|
388
|
+
raise ArgumentError, 'oneof: must be a non-empty Array of blocks' unless variants.is_a?(Array) && variants.any?
|
|
389
|
+
raise ArgumentError, 'oneof: each variant must be a Proc' unless variants.all?(Proc)
|
|
466
390
|
|
|
467
|
-
|
|
391
|
+
validations[:oneof] = variants.map { |block| OneofCollector.collect(block) }
|
|
468
392
|
end
|
|
469
393
|
|
|
470
394
|
def validate(type, options, attrs, required, opts)
|
|
@@ -479,51 +403,9 @@ module Grape
|
|
|
479
403
|
@api.inheritable_setting.namespace_stackable[:validations] = validator_instance
|
|
480
404
|
end
|
|
481
405
|
|
|
482
|
-
def validate_value_coercion(coerce_type, *values_list)
|
|
483
|
-
return unless coerce_type
|
|
484
|
-
|
|
485
|
-
coerce_type = coerce_type.first if coerce_type.is_a?(Enumerable)
|
|
486
|
-
values_list.each do |values|
|
|
487
|
-
next if !values || values.is_a?(Proc)
|
|
488
|
-
|
|
489
|
-
value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values
|
|
490
|
-
value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean
|
|
491
|
-
raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type)
|
|
492
|
-
end
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
def extract_message_option(attrs)
|
|
496
|
-
return nil unless attrs.is_a?(Array)
|
|
497
|
-
|
|
498
|
-
opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
|
|
499
|
-
opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
def options_key?(type, key, validations)
|
|
503
|
-
validations[type].respond_to?(:key?) && validations[type].key?(key) && !validations[type][key].nil?
|
|
504
|
-
end
|
|
505
|
-
|
|
506
406
|
def all_element_blank?(scoped_params)
|
|
507
407
|
scoped_params.respond_to?(:all?) && scoped_params.all?(&:blank?)
|
|
508
408
|
end
|
|
509
|
-
|
|
510
|
-
# Validators don't have access to each other and they don't need, however,
|
|
511
|
-
# some validators might influence others, so their options should be shared
|
|
512
|
-
def derive_validator_options(validations)
|
|
513
|
-
allow_blank = validations[:allow_blank]
|
|
514
|
-
|
|
515
|
-
{
|
|
516
|
-
allow_blank: allow_blank.is_a?(Hash) ? allow_blank[:value] : allow_blank,
|
|
517
|
-
fail_fast: validations.delete(:fail_fast) || false
|
|
518
|
-
}
|
|
519
|
-
end
|
|
520
|
-
|
|
521
|
-
def validates_presence(validations, attrs, opts)
|
|
522
|
-
return unless validations.key?(:presence) && validations[:presence]
|
|
523
|
-
|
|
524
|
-
validate('presence', validations.delete(:presence), attrs, true, opts)
|
|
525
|
-
validations.delete(:message) if validations.key?(:message)
|
|
526
|
-
end
|
|
527
409
|
end
|
|
528
410
|
end
|
|
529
411
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Validations
|
|
5
|
+
# Immutable value object holding the two options every validator reads at
|
|
6
|
+
# construction time: +allow_blank+ and +fail_fast+. Internal to
|
|
7
|
+
# {Validators::Base}, which builds it from the +opts+ Hash so the public
|
|
8
|
+
# 5th-argument contract stays a plain Hash — not part of any wire contract.
|
|
9
|
+
#
|
|
10
|
+
# Defaults mirror the prior +opts.values_at+ behaviour: +allow_blank+ is
|
|
11
|
+
# +nil+ when the declaration didn't supply it (validators treat nil as
|
|
12
|
+
# "not set"), +fail_fast+ defaults to +false+.
|
|
13
|
+
SharedOptions = Data.define(:allow_blank, :fail_fast) do
|
|
14
|
+
def initialize(allow_blank: nil, fail_fast: false)
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -12,7 +12,7 @@ module Grape
|
|
|
12
12
|
# behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
|
|
13
13
|
# maintains Virtus behavior in coercing.
|
|
14
14
|
class ArrayCoercer < DryTypeCoercer
|
|
15
|
-
def initialize(type, strict
|
|
15
|
+
def initialize(type, strict: false)
|
|
16
16
|
super
|
|
17
17
|
@coercer = strict ? DryTypes::Strict::Array : DryTypes::Params::Array
|
|
18
18
|
@subtype = type.first
|
|
@@ -52,7 +52,7 @@ module Grape
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def elem_coercer
|
|
55
|
-
@elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict)
|
|
55
|
+
@elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict:)
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
end
|
|
@@ -33,6 +33,10 @@ module Grape
|
|
|
33
33
|
# contract as +coerced?+, and must be supplied with a coercion
|
|
34
34
|
# +method+.
|
|
35
35
|
class CustomTypeCoercer
|
|
36
|
+
TYPE_CHECK_METHODS = %i[coerced? parsed?].freeze
|
|
37
|
+
COLLECTION_TYPES = [Array, Set].freeze
|
|
38
|
+
private_constant :TYPE_CHECK_METHODS, :COLLECTION_TYPES
|
|
39
|
+
|
|
36
40
|
# A new coercer for the given type specification
|
|
37
41
|
# and coercion method.
|
|
38
42
|
#
|
|
@@ -41,8 +45,7 @@ module Grape
|
|
|
41
45
|
# @param method [#parse,#call]
|
|
42
46
|
# optional coercion method. See class docs.
|
|
43
47
|
def initialize(type, method = nil)
|
|
44
|
-
|
|
45
|
-
@method = enforce_symbolized_keys type, coercion_method
|
|
48
|
+
@method = build_coercion_method(type, method)
|
|
46
49
|
@type_check = infer_type_check(type)
|
|
47
50
|
end
|
|
48
51
|
|
|
@@ -66,97 +69,50 @@ module Grape
|
|
|
66
69
|
|
|
67
70
|
private
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
def build_coercion_method(type, method)
|
|
73
|
+
coercion_method = infer_coercion_method(type, method)
|
|
74
|
+
return hash_symbolizer(coercion_method) if type == Hash
|
|
75
|
+
return collection_symbolizer(coercion_method) if COLLECTION_TYPES.include?(type)
|
|
76
|
+
|
|
77
|
+
coercion_method
|
|
78
|
+
end
|
|
79
|
+
|
|
75
80
|
def infer_coercion_method(type, method)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
return type.method(:parse) unless method
|
|
82
|
+
return method unless method.respond_to?(:parse)
|
|
83
|
+
|
|
84
|
+
method.method(:parse)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def hash_symbolizer(method)
|
|
88
|
+
->(val) { method.call(val).deep_symbolize_keys }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def collection_symbolizer(method)
|
|
92
|
+
->(val) { method.call(val).map! { |item| symbolize_if_hash(item) } }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def symbolize_if_hash(item)
|
|
96
|
+
item.is_a?(Hash) ? item.deep_symbolize_keys : item
|
|
87
97
|
end
|
|
88
98
|
|
|
89
|
-
# Determine how the type validity of a coerced
|
|
90
|
-
# value should be decided.
|
|
91
|
-
#
|
|
92
|
-
# @param type see #new
|
|
93
|
-
# @return [#call] a procedure which accepts a single parameter
|
|
94
|
-
# and returns +true+ if the passed object is of the correct type.
|
|
95
99
|
def infer_type_check(type)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# Arbitrary proc passed for type validation.
|
|
103
|
-
# Note that this will fail unless a method is also
|
|
104
|
-
# passed, or if the type also implements a parse() method.
|
|
105
|
-
type
|
|
106
|
-
elsif type.is_a?(Enumerable)
|
|
107
|
-
lambda do |value|
|
|
108
|
-
value.is_a?(Enumerable) && value.all? do |val|
|
|
109
|
-
recursive_type_check(type.first, val)
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
else
|
|
113
|
-
# By default, do a simple type check
|
|
114
|
-
->(value) { value.is_a? type }
|
|
115
|
-
end
|
|
100
|
+
method_name = TYPE_CHECK_METHODS.detect { |m| type.respond_to?(m) }
|
|
101
|
+
return type.method(method_name) if method_name
|
|
102
|
+
return type if type.respond_to?(:call)
|
|
103
|
+
return enumerable_type_check(type) if type.is_a?(Enumerable)
|
|
104
|
+
|
|
105
|
+
->(value) { value.is_a? type }
|
|
116
106
|
end
|
|
117
107
|
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
value.all? { |val| recursive_type_check(type.first, val) }
|
|
121
|
-
else
|
|
122
|
-
!type.is_a?(Enumerable) && value.is_a?(type)
|
|
123
|
-
end
|
|
108
|
+
def enumerable_type_check(type)
|
|
109
|
+
->(value) { value.is_a?(Enumerable) && value.all? { |val| recursive_type_check(type.first, val) } }
|
|
124
110
|
end
|
|
125
111
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
# This helps common libs such as JSON to work easily.
|
|
131
|
-
#
|
|
132
|
-
# @param type see #new
|
|
133
|
-
# @param method see #infer_coercion_method
|
|
134
|
-
# @return [#call] +method+ wrapped in an additional
|
|
135
|
-
# key-conversion step, or just returns +method+
|
|
136
|
-
# itself if no conversion is deemed to be
|
|
137
|
-
# necessary.
|
|
138
|
-
def enforce_symbolized_keys(type, method)
|
|
139
|
-
# Collections have all values processed individually
|
|
140
|
-
if [Array, Set].include?(type)
|
|
141
|
-
lambda do |val|
|
|
142
|
-
method.call(val).tap do |new_val|
|
|
143
|
-
new_val.map do |item|
|
|
144
|
-
item.is_a?(Hash) ? item.deep_symbolize_keys : item
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Hash objects are processed directly
|
|
150
|
-
elsif type == Hash
|
|
151
|
-
lambda do |val|
|
|
152
|
-
method.call(val).deep_symbolize_keys
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Simple types are not processed.
|
|
156
|
-
# This includes Array<primitive> types.
|
|
157
|
-
else
|
|
158
|
-
method
|
|
159
|
-
end
|
|
112
|
+
def recursive_type_check(type, value)
|
|
113
|
+
return value.all? { |val| recursive_type_check(type.first, val) } if type.is_a?(Enumerable) && value.is_a?(Enumerable)
|
|
114
|
+
|
|
115
|
+
!type.is_a?(Enumerable) && value.is_a?(type)
|
|
160
116
|
end
|
|
161
117
|
end
|
|
162
118
|
end
|
|
@@ -29,7 +29,7 @@ module Grape
|
|
|
29
29
|
# @param set [Boolean]
|
|
30
30
|
# when true, a +Set+ will be returned by {#call} instead
|
|
31
31
|
# of an +Array+ and duplicate items will be discarded.
|
|
32
|
-
def initialize(type, set
|
|
32
|
+
def initialize(type, set: false)
|
|
33
33
|
super(type)
|
|
34
34
|
@set = set
|
|
35
35
|
end
|
|
@@ -26,13 +26,13 @@ module Grape
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Returns an instance of a coercer for a given type
|
|
29
|
-
def coercer_instance_for(type, strict
|
|
29
|
+
def coercer_instance_for(type, strict: false)
|
|
30
30
|
klass = type.instance_of?(Class) ? PrimitiveCoercer : collection_coercer_for(type)
|
|
31
|
-
klass.new(type, strict)
|
|
31
|
+
klass.new(type, strict:)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def initialize(type, strict
|
|
35
|
+
def initialize(type, strict: false)
|
|
36
36
|
@type = type
|
|
37
37
|
@strict = strict
|
|
38
38
|
@cache_coercer = strict ? DryTypes::StrictCache : DryTypes::ParamsCache
|
|
@@ -7,7 +7,7 @@ module Grape
|
|
|
7
7
|
# initialization. When +strict+ is true, it doesn't coerce a value but check
|
|
8
8
|
# that it has the proper type.
|
|
9
9
|
class PrimitiveCoercer < DryTypeCoercer
|
|
10
|
-
def initialize(type, strict
|
|
10
|
+
def initialize(type, strict: false)
|
|
11
11
|
super
|
|
12
12
|
|
|
13
13
|
@coercer = cache_coercer[type]
|
|
@@ -15,7 +15,7 @@ module Grape
|
|
|
15
15
|
|
|
16
16
|
def call(val)
|
|
17
17
|
return InvalidValue.new if reject?(val)
|
|
18
|
-
return
|
|
18
|
+
return if val.nil? || treat_as_nil?(val)
|
|
19
19
|
|
|
20
20
|
super
|
|
21
21
|
end
|
|
@@ -29,9 +29,14 @@ module Grape
|
|
|
29
29
|
# but Virtus wouldn't accept it. So, this method only exists to not introduce
|
|
30
30
|
# breaking changes.
|
|
31
31
|
def reject?(val)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
case val
|
|
33
|
+
when Array, Hash
|
|
34
|
+
type == String
|
|
35
|
+
when String
|
|
36
|
+
type == Hash
|
|
37
|
+
else
|
|
38
|
+
false
|
|
39
|
+
end
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
# Dry-Types treats an empty string as invalid. However, Grape considers an empty string as
|
|
@@ -26,6 +26,14 @@ module Grape
|
|
|
26
26
|
@member_coercer = MultipleTypeCoercer.new types, method
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Returns the Grape DSL notation for this coercer, e.g. "Array[Integer, String]".
|
|
30
|
+
# Distinct from the plain-array string "[Integer, String]" produced by the
|
|
31
|
+
# +types:+ keyword, which lets documentation tools tell the two apart.
|
|
32
|
+
def to_s
|
|
33
|
+
container = @types.is_a?(Set) ? 'Set' : 'Array'
|
|
34
|
+
"#{container}[#{@types.join(', ')}]"
|
|
35
|
+
end
|
|
36
|
+
|
|
29
37
|
# Coerce the given value.
|
|
30
38
|
#
|
|
31
39
|
# @param value [Array<String>] collection of values to be coerced
|
|
@@ -75,7 +75,7 @@ module Grape
|
|
|
75
75
|
# @return [Boolean] +true+ if the given value will be treated as
|
|
76
76
|
# a list of types.
|
|
77
77
|
def multiple?(type)
|
|
78
|
-
|
|
78
|
+
array_or_set?(type) && type.size > 1
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# Does Grape provide special coercion and validation
|
|
@@ -98,17 +98,18 @@ module Grape
|
|
|
98
98
|
GROUPS.include? type
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
def array_or_set?(type)
|
|
102
|
+
type.is_a?(Array) || type.is_a?(Set)
|
|
103
|
+
end
|
|
104
|
+
|
|
101
105
|
# A valid custom type must implement a class-level `parse` method, taking
|
|
102
106
|
# one String argument and returning the parsed value in its correct type.
|
|
103
107
|
#
|
|
104
108
|
# @param type [Class] type to check
|
|
105
109
|
# @return [Boolean] whether or not the type can be used as a custom type
|
|
106
110
|
def custom?(type)
|
|
107
|
-
!primitive?(type) &&
|
|
108
|
-
|
|
109
|
-
!multiple?(type) &&
|
|
110
|
-
type.respond_to?(:parse) &&
|
|
111
|
-
type.method(:parse).arity == 1
|
|
111
|
+
!(primitive?(type) || structure?(type) || multiple?(type)) &&
|
|
112
|
+
type.respond_to?(:parse) && type.method(:parse).arity == 1
|
|
112
113
|
end
|
|
113
114
|
|
|
114
115
|
# Is the declared type an +Array+ or +Set+ of a {#custom?} type?
|
|
@@ -117,9 +118,7 @@ module Grape
|
|
|
117
118
|
# @return [Boolean] true if +type+ is a collection of a type that implements
|
|
118
119
|
# its own +#parse+ method.
|
|
119
120
|
def collection_of_custom?(type)
|
|
120
|
-
(type.
|
|
121
|
-
type.length == 1 &&
|
|
122
|
-
(custom?(type.first) || special?(type.first))
|
|
121
|
+
array_or_set?(type) && type.length == 1 && (custom?(type.first) || special?(type.first))
|
|
123
122
|
end
|
|
124
123
|
|
|
125
124
|
def map_special(type)
|
|
@@ -162,27 +161,21 @@ module Grape
|
|
|
162
161
|
end
|
|
163
162
|
|
|
164
163
|
def create_coercer_instance(type, method, strict)
|
|
165
|
-
#
|
|
166
|
-
type =
|
|
167
|
-
|
|
168
|
-
#
|
|
169
|
-
if
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
Types::CustomTypeCollectionCoercer.new(
|
|
181
|
-
Types.map_special(type.first), type.is_a?(Set)
|
|
182
|
-
)
|
|
183
|
-
else
|
|
184
|
-
DryTypeCoercer.coercer_instance_for(type, strict)
|
|
185
|
-
end
|
|
164
|
+
# map_special doesn't recurse into collections — applied only to the top-level type here.
|
|
165
|
+
type = map_special(type)
|
|
166
|
+
|
|
167
|
+
# Multiply-typed parameters, e.g. types: [Integer, String].
|
|
168
|
+
return MultipleTypeCoercer.new(type, method) if multiple?(type)
|
|
169
|
+
|
|
170
|
+
# User-supplied coercion method, or a custom type with its own #parse.
|
|
171
|
+
return CustomTypeCoercer.new(type, method) if method || custom?(type)
|
|
172
|
+
|
|
173
|
+
# Array/Set of a custom type — CustomTypeCoercer already handles single
|
|
174
|
+
# custom types when an explicit coercion method is supplied.
|
|
175
|
+
return CustomTypeCollectionCoercer.new(map_special(type.first), set: type.is_a?(Set)) if collection_of_custom?(type)
|
|
176
|
+
|
|
177
|
+
# Fallback: let dry-types handle primitives, structures, and known specials.
|
|
178
|
+
DryTypeCoercer.coercer_instance_for(type, strict:)
|
|
186
179
|
end
|
|
187
180
|
|
|
188
181
|
class CoercerCache < Grape::Util::Cache
|