grape 1.3.2 → 1.5.2

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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -2
  3. data/LICENSE +1 -1
  4. data/README.md +120 -24
  5. data/UPGRADING.md +220 -39
  6. data/lib/grape.rb +3 -2
  7. data/lib/grape/api.rb +3 -3
  8. data/lib/grape/api/instance.rb +22 -25
  9. data/lib/grape/dsl/callbacks.rb +1 -1
  10. data/lib/grape/dsl/helpers.rb +1 -0
  11. data/lib/grape/dsl/inside_route.rb +70 -37
  12. data/lib/grape/dsl/parameters.rb +8 -4
  13. data/lib/grape/dsl/routing.rb +6 -7
  14. data/lib/grape/dsl/validations.rb +18 -1
  15. data/lib/grape/eager_load.rb +1 -1
  16. data/lib/grape/endpoint.rb +8 -6
  17. data/lib/grape/exceptions/validation.rb +1 -1
  18. data/lib/grape/exceptions/validation_errors.rb +1 -1
  19. data/lib/grape/middleware/auth/base.rb +3 -3
  20. data/lib/grape/middleware/base.rb +3 -2
  21. data/lib/grape/middleware/error.rb +11 -13
  22. data/lib/grape/middleware/formatter.rb +3 -3
  23. data/lib/grape/middleware/stack.rb +8 -1
  24. data/lib/grape/request.rb +1 -1
  25. data/lib/grape/router.rb +25 -39
  26. data/lib/grape/router/attribute_translator.rb +26 -5
  27. data/lib/grape/router/route.rb +1 -19
  28. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  29. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  30. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  31. data/lib/grape/util/base_inheritable.rb +2 -2
  32. data/lib/grape/util/lazy_value.rb +1 -0
  33. data/lib/grape/validations/attributes_iterator.rb +8 -0
  34. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  35. data/lib/grape/validations/params_scope.rb +9 -7
  36. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  37. data/lib/grape/validations/types.rb +1 -4
  38. data/lib/grape/validations/types/array_coercer.rb +14 -5
  39. data/lib/grape/validations/types/build_coercer.rb +1 -5
  40. data/lib/grape/validations/types/custom_type_coercer.rb +15 -1
  41. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  42. data/lib/grape/validations/types/invalid_value.rb +24 -0
  43. data/lib/grape/validations/types/primitive_coercer.rb +9 -3
  44. data/lib/grape/validations/types/set_coercer.rb +6 -4
  45. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  46. data/lib/grape/validations/validator_factory.rb +1 -1
  47. data/lib/grape/validations/validators/as.rb +1 -1
  48. data/lib/grape/validations/validators/base.rb +8 -8
  49. data/lib/grape/validations/validators/coerce.rb +8 -14
  50. data/lib/grape/validations/validators/default.rb +3 -5
  51. data/lib/grape/validations/validators/except_values.rb +1 -1
  52. data/lib/grape/validations/validators/multiple_params_base.rb +2 -1
  53. data/lib/grape/validations/validators/values.rb +1 -1
  54. data/lib/grape/version.rb +1 -1
  55. data/spec/grape/api/instance_spec.rb +50 -0
  56. data/spec/grape/api_remount_spec.rb +9 -4
  57. data/spec/grape/api_spec.rb +75 -0
  58. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  59. data/spec/grape/endpoint/declared_spec.rb +601 -0
  60. data/spec/grape/endpoint_spec.rb +0 -521
  61. data/spec/grape/entity_spec.rb +7 -1
  62. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  63. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  64. data/spec/grape/middleware/error_spec.rb +1 -1
  65. data/spec/grape/middleware/formatter_spec.rb +1 -1
  66. data/spec/grape/middleware/stack_spec.rb +1 -0
  67. data/spec/grape/request_spec.rb +1 -1
  68. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
  69. data/spec/grape/validations/params_scope_spec.rb +26 -0
  70. data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
  71. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  72. data/spec/grape/validations/types/primitive_coercer_spec.rb +65 -5
  73. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  74. data/spec/grape/validations/validators/coerce_spec.rb +223 -25
  75. data/spec/grape/validations/validators/default_spec.rb +170 -0
  76. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  77. data/spec/grape/validations/validators/values_spec.rb +1 -1
  78. data/spec/grape/validations_spec.rb +290 -18
  79. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  80. data/spec/shared/versioning_examples.rb +20 -20
  81. data/spec/spec_helper.rb +0 -10
  82. data/spec/support/chunks.rb +14 -0
  83. data/spec/support/versioned_helpers.rb +4 -6
  84. metadata +20 -9
@@ -48,6 +48,14 @@ module Grape
48
48
  def yield_attributes(_resource_params, _attrs)
49
49
  raise NotImplementedError
50
50
  end
51
+
52
+ # This is a special case so that we can ignore tree's where option
53
+ # values are missing lower down. Unfortunately we can remove this
54
+ # are the parameter parsing stage as they are required to ensure
55
+ # the correct indexing is maintained
56
+ def skip?(val)
57
+ val == Grape::DSL::Parameters::EmptyOptionalValue
58
+ end
51
59
  end
52
60
  end
53
61
  end
@@ -6,7 +6,7 @@ module Grape
6
6
  private
7
7
 
8
8
  def yield_attributes(resource_params, _attrs)
9
- yield resource_params
9
+ yield resource_params, skip?(resource_params)
10
10
  end
11
11
  end
12
12
  end
@@ -39,7 +39,7 @@ module Grape
39
39
  end
40
40
 
41
41
  def configuration
42
- @api.configuration.evaluate
42
+ @api.configuration.respond_to?(:evaluate) ? @api.configuration.evaluate : @api.configuration
43
43
  end
44
44
 
45
45
  # @return [Boolean] whether or not this entire scope needs to be
@@ -54,14 +54,16 @@ module Grape
54
54
  end
55
55
 
56
56
  def meets_dependency?(params, request_params)
57
+ return true unless @dependent_on
58
+
57
59
  if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
58
60
  return false
59
61
  end
60
62
 
61
- return true unless @dependent_on
62
63
  return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array)
63
- return false unless params.respond_to?(:with_indifferent_access)
64
- params = params.with_indifferent_access
64
+
65
+ # params might be anything what looks like a hash, so it must implement a `key?` method
66
+ return false unless params.respond_to?(:key?)
65
67
 
66
68
  @dependent_on.each do |dependency|
67
69
  if dependency.is_a?(Hash)
@@ -237,10 +239,10 @@ module Grape
237
239
  @parent.push_declared_params [element => @declared_params]
238
240
  else
239
241
  @api.namespace_stackable(:declared_params, @declared_params)
240
-
241
- @api.route_setting(:declared_params, []) unless @api.route_setting(:declared_params)
242
- @api.route_setting(:declared_params, @api.namespace_stackable(:declared_params).flatten)
243
242
  end
243
+
244
+ # params were stored in settings, it can be cleaned from the params scope
245
+ @declared_params = nil
244
246
  end
245
247
 
246
248
  def validates(attrs, validations)
@@ -7,7 +7,7 @@ module Grape
7
7
 
8
8
  def yield_attributes(val, attrs)
9
9
  attrs.each do |attr_name|
10
- yield val, attr_name, empty?(val)
10
+ yield val, attr_name, empty?(val), skip?(val)
11
11
  end
12
12
  end
13
13
 
@@ -7,6 +7,7 @@ require_relative 'types/multiple_type_coercer'
7
7
  require_relative 'types/variant_collection_coercer'
8
8
  require_relative 'types/json'
9
9
  require_relative 'types/file'
10
+ require_relative 'types/invalid_value'
10
11
 
11
12
  module Grape
12
13
  module Validations
@@ -21,10 +22,6 @@ module Grape
21
22
  # and {Grape::Dsl::Parameters#optional}. The main
22
23
  # entry point for this process is {Types.build_coercer}.
23
24
  module Types
24
- # Instances of this class may be used as tokens to denote that
25
- # a parameter value could not be coerced.
26
- class InvalidValue; end
27
-
28
25
  # Types representing a single value, which are coerced.
29
26
  PRIMITIVES = [
30
27
  # Numerical
@@ -6,7 +6,7 @@ module Grape
6
6
  module Validations
7
7
  module Types
8
8
  # Coerces elements in an array. It might be an array of strings or integers or
9
- # anything else.
9
+ # an array of arrays of integers.
10
10
  #
11
11
  # It could've been possible to use an +of+
12
12
  # method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
@@ -14,16 +14,17 @@ module Grape
14
14
  # behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
15
15
  # maintains Virtus behavior in coercing.
16
16
  class ArrayCoercer < DryTypeCoercer
17
+ register_collection Array
18
+
17
19
  def initialize(type, strict = false)
18
20
  super
19
21
 
20
22
  @coercer = scope::Array
21
- @elem_coercer = PrimitiveCoercer.new(type.first, strict)
23
+ @subtype = type.first
22
24
  end
23
25
 
24
26
  def call(_val)
25
27
  collection = super
26
-
27
28
  return collection if collection.is_a?(InvalidValue)
28
29
 
29
30
  coerce_elements collection
@@ -31,11 +32,15 @@ module Grape
31
32
 
32
33
  protected
33
34
 
35
+ attr_reader :subtype
36
+
34
37
  def coerce_elements(collection)
38
+ return if collection.nil?
39
+
35
40
  collection.each_with_index do |elem, index|
36
41
  return InvalidValue.new if reject?(elem)
37
42
 
38
- coerced_elem = @elem_coercer.call(elem)
43
+ coerced_elem = elem_coercer.call(elem)
39
44
 
40
45
  return coerced_elem if coerced_elem.is_a?(InvalidValue)
41
46
 
@@ -45,11 +50,15 @@ module Grape
45
50
  collection
46
51
  end
47
52
 
48
- # This method maintaine logic which was defined by Virtus for arrays.
53
+ # This method maintains logic which was defined by Virtus for arrays.
49
54
  # Virtus doesn't allow nil in arrays.
50
55
  def reject?(val)
51
56
  val.nil?
52
57
  end
58
+
59
+ def elem_coercer
60
+ @elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict)
61
+ end
53
62
  end
54
63
  end
55
64
  end
@@ -60,12 +60,8 @@ module Grape
60
60
  Types::CustomTypeCollectionCoercer.new(
61
61
  Types.map_special(type.first), type.is_a?(Set)
62
62
  )
63
- elsif type.is_a?(Array)
64
- ArrayCoercer.new type, strict
65
- elsif type.is_a?(Set)
66
- SetCoercer.new type, strict
67
63
  else
68
- PrimitiveCoercer.new type, strict
64
+ DryTypeCoercer.coercer_instance_for(type, strict)
69
65
  end
70
66
  end
71
67
 
@@ -55,6 +55,8 @@ module Grape
55
55
  return if val.nil?
56
56
 
57
57
  coerced_val = @method.call(val)
58
+
59
+ return coerced_val if coerced_val.is_a?(InvalidValue)
58
60
  return InvalidValue.new unless coerced?(coerced_val)
59
61
  coerced_val
60
62
  end
@@ -103,13 +105,25 @@ module Grape
103
105
  # passed, or if the type also implements a parse() method.
104
106
  type
105
107
  elsif type.is_a?(Enumerable)
106
- ->(value) { value.respond_to?(:all?) && value.all? { |item| item.is_a? type[0] } }
108
+ lambda do |value|
109
+ value.is_a?(Enumerable) && value.all? do |val|
110
+ recursive_type_check(type.first, val)
111
+ end
112
+ end
107
113
  else
108
114
  # By default, do a simple type check
109
115
  ->(value) { value.is_a? type }
110
116
  end
111
117
  end
112
118
 
119
+ def recursive_type_check(type, value)
120
+ if type.is_a?(Enumerable) && value.is_a?(Enumerable)
121
+ value.all? { |val| recursive_type_check(type.first, val) }
122
+ else
123
+ !type.is_a?(Enumerable) && value.is_a?(type)
124
+ end
125
+ end
126
+
113
127
  # Enforce symbolized keys for complex types
114
128
  # by wrapping the coercion method such that
115
129
  # any Hash objects in the immediate heirarchy
@@ -17,8 +17,41 @@ module Grape
17
17
  # but check its type. More information there
18
18
  # https://dry-rb.org/gems/dry-types/1.2/built-in-types/
19
19
  class DryTypeCoercer
20
+ class << self
21
+ # Registers a collection coercer which could be found by a type,
22
+ # see +collection_coercer_for+ method below. This method is meant for inheritors.
23
+ def register_collection(type)
24
+ DryTypeCoercer.collection_coercers[type] = self
25
+ end
26
+
27
+ # Returns a collection coercer which corresponds to a given type.
28
+ # Example:
29
+ #
30
+ # collection_coercer_for(Array)
31
+ # #=> Grape::Validations::Types::ArrayCoercer
32
+ def collection_coercer_for(type)
33
+ collection_coercers[type]
34
+ end
35
+
36
+ # Returns an instance of a coercer for a given type
37
+ def coercer_instance_for(type, strict = false)
38
+ return PrimitiveCoercer.new(type, strict) if type.class == Class
39
+
40
+ # in case of a collection (Array[Integer]) the type is an instance of a collection,
41
+ # so we need to figure out the actual type
42
+ collection_coercer_for(type.class).new(type, strict)
43
+ end
44
+
45
+ protected
46
+
47
+ def collection_coercers
48
+ @collection_coercers ||= {}
49
+ end
50
+ end
51
+
20
52
  def initialize(type, strict = false)
21
53
  @type = type
54
+ @strict = strict
22
55
  @scope = strict ? DryTypes::Strict : DryTypes::Params
23
56
  end
24
57
 
@@ -27,6 +60,8 @@ module Grape
27
60
  #
28
61
  # @param val [Object]
29
62
  def call(val)
63
+ return if val.nil?
64
+
30
65
  @coercer[val]
31
66
  rescue Dry::Types::CoercionError => _e
32
67
  InvalidValue.new
@@ -34,7 +69,7 @@ module Grape
34
69
 
35
70
  protected
36
71
 
37
- attr_reader :scope, :type
72
+ attr_reader :scope, :type, :strict
38
73
  end
39
74
  end
40
75
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # Instances of this class may be used as tokens to denote that a parameter value could not be
7
+ # coerced. The given message will be used as a validation error.
8
+ class InvalidValue
9
+ attr_reader :message
10
+
11
+ def initialize(message = nil)
12
+ @message = message
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ # only exists to make it shorter for external use
20
+ module Grape
21
+ module Types
22
+ InvalidValue = Class.new(Grape::Validations::Types::InvalidValue)
23
+ end
24
+ end
@@ -36,8 +36,7 @@ module Grape
36
36
 
37
37
  def call(val)
38
38
  return InvalidValue.new if reject?(val)
39
- return nil if val.nil?
40
- return '' if val == ''
39
+ return nil if val.nil? || treat_as_nil?(val)
41
40
 
42
41
  super
43
42
  end
@@ -46,7 +45,7 @@ module Grape
46
45
 
47
46
  attr_reader :type
48
47
 
49
- # This method maintaine logic which was defined by Virtus. For example,
48
+ # This method maintains logic which was defined by Virtus. For example,
50
49
  # dry-types is ok to convert an array or a hash to a string, it is supported,
51
50
  # but Virtus wouldn't accept it. So, this method only exists to not introduce
52
51
  # breaking changes.
@@ -55,6 +54,13 @@ module Grape
55
54
  (val.is_a?(String) && type == Hash) ||
56
55
  (val.is_a?(Hash) && type == String)
57
56
  end
57
+
58
+ # Dry-Types treats an empty string as invalid. However, Grape considers an empty string as
59
+ # absence of a value and coerces it into nil. See a discussion there
60
+ # https://github.com/ruby-grape/grape/pull/2045
61
+ def treat_as_nil?(val)
62
+ val == '' && type != String
63
+ end
58
64
  end
59
65
  end
60
66
  end
@@ -1,18 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
- require_relative 'dry_type_coercer'
4
+ require_relative 'array_coercer'
5
5
 
6
6
  module Grape
7
7
  module Validations
8
8
  module Types
9
9
  # Takes the given array and converts it to a set. Every element of the set
10
10
  # is also coerced.
11
- class SetCoercer < DryTypeCoercer
11
+ class SetCoercer < ArrayCoercer
12
+ register_collection Set
13
+
12
14
  def initialize(type, strict = false)
13
15
  super
14
16
 
15
- @elem_coercer = PrimitiveCoercer.new(type.first, strict)
17
+ @coercer = nil
16
18
  end
17
19
 
18
20
  def call(value)
@@ -25,7 +27,7 @@ module Grape
25
27
 
26
28
  def coerce_elements(collection)
27
29
  collection.each_with_object(Set.new) do |elem, memo|
28
- coerced_elem = @elem_coercer.call(elem)
30
+ coerced_elem = elem_coercer.call(elem)
29
31
 
30
32
  return coerced_elem if coerced_elem.is_a?(InvalidValue)
31
33
 
@@ -33,7 +33,7 @@ module Grape
33
33
  # the coerced result, or an instance
34
34
  # of {InvalidValue} if the value could not be coerced.
35
35
  def call(value)
36
- return InvalidValue.new unless value.is_a? Array
36
+ return unless value.is_a? Array
37
37
 
38
38
  value =
39
39
  if @method
@@ -8,7 +8,7 @@ module Grape
8
8
  options[:options],
9
9
  options[:required],
10
10
  options[:params_scope],
11
- options[:opts])
11
+ **options[:opts])
12
12
  end
13
13
  end
14
14
  end
@@ -3,7 +3,7 @@
3
3
  module Grape
4
4
  module Validations
5
5
  class AsValidator < Base
6
- def initialize(attrs, options, required, scope, opts = {})
6
+ def initialize(attrs, options, required, scope, **opts)
7
7
  @renamed_options = options
8
8
  super
9
9
  end
@@ -12,14 +12,15 @@ module Grape
12
12
  # @param options [Object] implementation-dependent Validator options
13
13
  # @param required [Boolean] attribute(s) are required or optional
14
14
  # @param scope [ParamsScope] parent scope for this Validator
15
- # @param opts [Hash] additional validation options
16
- def initialize(attrs, options, required, scope, opts = {})
15
+ # @param opts [Array] additional validation options
16
+ def initialize(attrs, options, required, scope, *opts)
17
17
  @attrs = Array(attrs)
18
18
  @option = options
19
19
  @required = required
20
20
  @scope = scope
21
- @fail_fast = opts[:fail_fast] || false
22
- @allow_blank = opts[:allow_blank] || false
21
+ opts = opts.any? ? opts.shift : {}
22
+ @fail_fast = opts.fetch(:fail_fast, false)
23
+ @allow_blank = opts.fetch(:allow_blank, false)
23
24
  end
24
25
 
25
26
  # Validates a given request.
@@ -43,13 +44,12 @@ module Grape
43
44
  # there may be more than one error per field
44
45
  array_errors = []
45
46
 
46
- attributes.each do |val, attr_name, empty_val|
47
+ attributes.each do |val, attr_name, empty_val, skip_value|
48
+ next if skip_value
47
49
  next if !@scope.required? && empty_val
48
50
  next unless @scope.meets_dependency?(val, params)
49
51
  begin
50
- if @required || val.respond_to?(:key?) && val.key?(attr_name)
51
- validate_param!(attr_name, val)
52
- end
52
+ validate_param!(attr_name, val) if @required || val.respond_to?(:key?) && val.key?(attr_name)
53
53
  rescue Grape::Exceptions::Validation => e
54
54
  array_errors << e
55
55
  end
@@ -17,7 +17,7 @@ module Grape
17
17
 
18
18
  module Validations
19
19
  class CoerceValidator < Base
20
- def initialize(*_args)
20
+ def initialize(attrs, options, required, scope, **opts)
21
21
  super
22
22
 
23
23
  @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
@@ -36,7 +36,7 @@ module Grape
36
36
 
37
37
  new_value = coerce_value(params[attr_name])
38
38
 
39
- raise validation_exception(attr_name) unless valid_type?(new_value)
39
+ raise validation_exception(attr_name, new_value.message) unless valid_type?(new_value)
40
40
 
41
41
  # Don't assign a value if it is identical. It fixes a problem with Hashie::Mash
42
42
  # which looses wrappers for hashes and arrays after reassigning values
@@ -67,21 +67,12 @@ module Grape
67
67
  end
68
68
 
69
69
  def coerce_value(val)
70
- val.nil? ? coerce_nil(val) : converter.call(val)
70
+ converter.call(val)
71
71
  # Some custom types might fail, so it should be treated as an invalid value
72
72
  rescue StandardError
73
73
  Types::InvalidValue.new
74
74
  end
75
75
 
76
- def coerce_nil(val)
77
- # define default values for structures, the dry-types lib which is used
78
- # for coercion doesn't accept nil as a value, so it would fail
79
- return [] if type == Array
80
- return Set.new if type == Set
81
- return {} if type == Hash
82
- val
83
- end
84
-
85
76
  # Type to which the parameter will be coerced.
86
77
  #
87
78
  # @return [Class]
@@ -89,8 +80,11 @@ module Grape
89
80
  @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type]
90
81
  end
91
82
 
92
- def validation_exception(attr_name)
93
- Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:coerce))
83
+ def validation_exception(attr_name, custom_msg = nil)
84
+ Grape::Exceptions::Validation.new(
85
+ params: [@scope.full_name(attr_name)],
86
+ message: custom_msg || message(:coerce)
87
+ )
94
88
  end
95
89
  end
96
90
  end