grape 1.3.1 → 1.5.1

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -1
  3. data/LICENSE +1 -1
  4. data/README.md +104 -21
  5. data/UPGRADING.md +265 -39
  6. data/lib/grape.rb +2 -2
  7. data/lib/grape/api.rb +2 -2
  8. data/lib/grape/api/instance.rb +29 -28
  9. data/lib/grape/dsl/helpers.rb +1 -0
  10. data/lib/grape/dsl/inside_route.rb +69 -36
  11. data/lib/grape/dsl/parameters.rb +7 -3
  12. data/lib/grape/dsl/routing.rb +2 -4
  13. data/lib/grape/dsl/validations.rb +18 -1
  14. data/lib/grape/eager_load.rb +1 -1
  15. data/lib/grape/endpoint.rb +8 -6
  16. data/lib/grape/http/headers.rb +1 -0
  17. data/lib/grape/middleware/base.rb +2 -1
  18. data/lib/grape/middleware/error.rb +10 -12
  19. data/lib/grape/middleware/formatter.rb +3 -3
  20. data/lib/grape/middleware/stack.rb +21 -8
  21. data/lib/grape/middleware/versioner/header.rb +1 -1
  22. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  23. data/lib/grape/path.rb +2 -2
  24. data/lib/grape/request.rb +1 -1
  25. data/lib/grape/router.rb +30 -43
  26. data/lib/grape/router/attribute_translator.rb +26 -5
  27. data/lib/grape/router/route.rb +3 -22
  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 +11 -8
  32. data/lib/grape/util/lazy_value.rb +1 -0
  33. data/lib/grape/util/reverse_stackable_values.rb +3 -1
  34. data/lib/grape/util/stackable_values.rb +3 -1
  35. data/lib/grape/validations/attributes_iterator.rb +8 -0
  36. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  37. data/lib/grape/validations/params_scope.rb +8 -6
  38. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  39. data/lib/grape/validations/types.rb +6 -5
  40. data/lib/grape/validations/types/array_coercer.rb +14 -5
  41. data/lib/grape/validations/types/build_coercer.rb +5 -8
  42. data/lib/grape/validations/types/custom_type_coercer.rb +14 -2
  43. data/lib/grape/validations/types/dry_type_coercer.rb +36 -1
  44. data/lib/grape/validations/types/file.rb +15 -13
  45. data/lib/grape/validations/types/json.rb +40 -36
  46. data/lib/grape/validations/types/primitive_coercer.rb +11 -5
  47. data/lib/grape/validations/types/set_coercer.rb +6 -4
  48. data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
  49. data/lib/grape/validations/validator_factory.rb +1 -1
  50. data/lib/grape/validations/validators/as.rb +1 -1
  51. data/lib/grape/validations/validators/base.rb +4 -5
  52. data/lib/grape/validations/validators/coerce.rb +3 -10
  53. data/lib/grape/validations/validators/default.rb +3 -5
  54. data/lib/grape/validations/validators/except_values.rb +1 -1
  55. data/lib/grape/validations/validators/multiple_params_base.rb +2 -1
  56. data/lib/grape/validations/validators/regexp.rb +1 -1
  57. data/lib/grape/validations/validators/values.rb +1 -1
  58. data/lib/grape/version.rb +1 -1
  59. data/spec/grape/api/instance_spec.rb +50 -0
  60. data/spec/grape/api_spec.rb +75 -0
  61. data/spec/grape/dsl/inside_route_spec.rb +182 -33
  62. data/spec/grape/endpoint/declared_spec.rb +601 -0
  63. data/spec/grape/endpoint_spec.rb +0 -521
  64. data/spec/grape/entity_spec.rb +6 -0
  65. data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
  66. data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
  67. data/spec/grape/middleware/error_spec.rb +1 -1
  68. data/spec/grape/middleware/formatter_spec.rb +1 -1
  69. data/spec/grape/middleware/stack_spec.rb +3 -1
  70. data/spec/grape/path_spec.rb +4 -4
  71. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
  72. data/spec/grape/validations/params_scope_spec.rb +26 -0
  73. data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
  74. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  75. data/spec/grape/validations/types/primitive_coercer_spec.rb +65 -5
  76. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  77. data/spec/grape/validations/types_spec.rb +1 -1
  78. data/spec/grape/validations/validators/coerce_spec.rb +317 -29
  79. data/spec/grape/validations/validators/default_spec.rb +170 -0
  80. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  81. data/spec/grape/validations/validators/values_spec.rb +1 -1
  82. data/spec/grape/validations_spec.rb +290 -18
  83. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  84. data/spec/spec_helper.rb +0 -10
  85. data/spec/support/chunks.rb +14 -0
  86. data/spec/support/versioned_helpers.rb +3 -5
  87. metadata +18 -8
@@ -5,13 +5,12 @@ module Grape
5
5
  # Base for classes which need to operate with own values kept
6
6
  # in the hash and inherited values kept in a Hash-like object.
7
7
  class BaseInheritable
8
- attr_accessor :inherited_values
9
- attr_accessor :new_values
8
+ attr_accessor :inherited_values, :new_values
10
9
 
11
10
  # @param inherited_values [Object] An object implementing an interface
12
11
  # of the Hash class.
13
- def initialize(inherited_values = {})
14
- @inherited_values = inherited_values
12
+ def initialize(inherited_values = nil)
13
+ @inherited_values = inherited_values || {}
15
14
  @new_values = {}
16
15
  end
17
16
 
@@ -26,10 +25,14 @@ module Grape
26
25
  end
27
26
 
28
27
  def keys
29
- combined = inherited_values.keys
30
- combined.concat(new_values.keys)
31
- combined.uniq!
32
- combined
28
+ if new_values.any?
29
+ combined = inherited_values.keys
30
+ combined.concat(new_values.keys)
31
+ combined.uniq!
32
+ combined
33
+ else
34
+ inherited_values.keys
35
+ end
33
36
  end
34
37
 
35
38
  def key?(name)
@@ -4,6 +4,7 @@ module Grape
4
4
  module Util
5
5
  class LazyValue
6
6
  attr_reader :access_keys
7
+
7
8
  def initialize(value, access_keys = [])
8
9
  @value = value
9
10
  @access_keys = access_keys
@@ -8,8 +8,10 @@ module Grape
8
8
  protected
9
9
 
10
10
  def concat_values(inherited_value, new_value)
11
+ return inherited_value unless new_value
12
+
11
13
  [].tap do |value|
12
- value.concat(new_value) if new_value
14
+ value.concat(new_value)
13
15
  value.concat(inherited_value)
14
16
  end
15
17
  end
@@ -29,9 +29,11 @@ module Grape
29
29
  protected
30
30
 
31
31
  def concat_values(inherited_value, new_value)
32
+ return inherited_value unless new_value
33
+
32
34
  [].tap do |value|
33
35
  value.concat(inherited_value)
34
- value.concat(new_value) if new_value
36
+ value.concat(new_value)
35
37
  end
36
38
  end
37
39
  end
@@ -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
@@ -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
 
@@ -42,7 +42,6 @@ module Grape
42
42
  Grape::API::Boolean,
43
43
  String,
44
44
  Symbol,
45
- Rack::Multipart::UploadedFile,
46
45
  TrueClass,
47
46
  FalseClass
48
47
  ].freeze
@@ -54,8 +53,7 @@ module Grape
54
53
  Set
55
54
  ].freeze
56
55
 
57
- # Types for which Grape provides special coercion
58
- # and type-checking logic.
56
+ # Special custom types provided by Grape.
59
57
  SPECIAL = {
60
58
  JSON => Json,
61
59
  Array[JSON] => JsonArray,
@@ -130,7 +128,6 @@ module Grape
130
128
  !primitive?(type) &&
131
129
  !structure?(type) &&
132
130
  !multiple?(type) &&
133
- !special?(type) &&
134
131
  type.respond_to?(:parse) &&
135
132
  type.method(:parse).arity == 1
136
133
  end
@@ -143,7 +140,11 @@ module Grape
143
140
  def self.collection_of_custom?(type)
144
141
  (type.is_a?(Array) || type.is_a?(Set)) &&
145
142
  type.length == 1 &&
146
- custom?(type.first)
143
+ (custom?(type.first) || special?(type.first))
144
+ end
145
+
146
+ def self.map_special(type)
147
+ SPECIAL.fetch(type, type)
147
148
  end
148
149
  end
149
150
  end
@@ -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
@@ -42,6 +42,9 @@ module Grape
42
42
  end
43
43
 
44
44
  def self.create_coercer_instance(type, method, strict)
45
+ # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!!
46
+ type = Types.map_special(type)
47
+
45
48
  # Use a special coercer for multiply-typed parameters.
46
49
  if Types.multiple?(type)
47
50
  MultipleTypeCoercer.new(type, method)
@@ -55,16 +58,10 @@ module Grape
55
58
  # method is supplied.
56
59
  elsif Types.collection_of_custom?(type)
57
60
  Types::CustomTypeCollectionCoercer.new(
58
- type.first, type.is_a?(Set)
61
+ Types.map_special(type.first), type.is_a?(Set)
59
62
  )
60
- elsif Types.special?(type)
61
- Types::SPECIAL[type].new
62
- elsif type.is_a?(Array)
63
- ArrayCoercer.new type, strict
64
- elsif type.is_a?(Set)
65
- SetCoercer.new type, strict
66
63
  else
67
- PrimitiveCoercer.new type, strict
64
+ DryTypeCoercer.coercer_instance_for(type, strict)
68
65
  end
69
66
  end
70
67
 
@@ -60,7 +60,7 @@ module Grape
60
60
  end
61
61
 
62
62
  def coerced?(val)
63
- @type_check.call val
63
+ val.nil? || @type_check.call(val)
64
64
  end
65
65
 
66
66
  private
@@ -103,13 +103,25 @@ module Grape
103
103
  # passed, or if the type also implements a parse() method.
104
104
  type
105
105
  elsif type.is_a?(Enumerable)
106
- ->(value) { value.respond_to?(:all?) && value.all? { |item| item.is_a? type[0] } }
106
+ lambda do |value|
107
+ value.is_a?(Enumerable) && value.all? do |val|
108
+ recursive_type_check(type.first, val)
109
+ end
110
+ end
107
111
  else
108
112
  # By default, do a simple type check
109
113
  ->(value) { value.is_a? type }
110
114
  end
111
115
  end
112
116
 
117
+ def recursive_type_check(type, value)
118
+ if type.is_a?(Enumerable) && value.is_a?(Enumerable)
119
+ value.all? { |val| recursive_type_check(type.first, val) }
120
+ else
121
+ !type.is_a?(Enumerable) && value.is_a?(type)
122
+ end
123
+ end
124
+
113
125
  # Enforce symbolized keys for complex types
114
126
  # by wrapping the coercion method such that
115
127
  # 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
@@ -7,21 +7,23 @@ module Grape
7
7
  # Actual handling of these objects is provided by +Rack::Request+;
8
8
  # this class is here only to assert that rack's handling has succeeded.
9
9
  class File
10
- def call(input)
11
- return if input.nil?
12
- return InvalidValue.new unless coerced?(input)
10
+ class << self
11
+ def parse(input)
12
+ return if input.nil?
13
+ return InvalidValue.new unless parsed?(input)
13
14
 
14
- # Processing of multipart file objects
15
- # is already taken care of by Rack::Request.
16
- # Nothing to do here.
17
- input
18
- end
15
+ # Processing of multipart file objects
16
+ # is already taken care of by Rack::Request.
17
+ # Nothing to do here.
18
+ input
19
+ end
19
20
 
20
- def coerced?(value)
21
- # Rack::Request creates a Hash with filename,
22
- # content type and an IO object. Do a bit of basic
23
- # duck-typing.
24
- value.is_a?(::Hash) && value.key?(:tempfile) && value[:tempfile].is_a?(Tempfile)
21
+ def parsed?(value)
22
+ # Rack::Request creates a Hash with filename,
23
+ # content type and an IO object. Do a bit of basic
24
+ # duck-typing.
25
+ value.is_a?(::Hash) && value.key?(:tempfile) && value[:tempfile].is_a?(Tempfile)
26
+ end
25
27
  end
26
28
  end
27
29
  end
@@ -12,35 +12,37 @@ module Grape
12
12
  # validation system will apply nested validation rules to
13
13
  # all returned objects.
14
14
  class Json
15
- # Coerce the input into a JSON-like data structure.
16
- #
17
- # @param input [String] a JSON-encoded parameter value
18
- # @return [Hash,Array<Hash>,nil]
19
- def call(input)
20
- return input if coerced?(input)
15
+ class << self
16
+ # Coerce the input into a JSON-like data structure.
17
+ #
18
+ # @param input [String] a JSON-encoded parameter value
19
+ # @return [Hash,Array<Hash>,nil]
20
+ def parse(input)
21
+ return input if parsed?(input)
21
22
 
22
- # Allow nulls and blank strings
23
- return if input.nil? || input =~ /^\s*$/
24
- JSON.parse(input, symbolize_names: true)
25
- end
23
+ # Allow nulls and blank strings
24
+ return if input.nil? || input.match?(/^\s*$/)
25
+ JSON.parse(input, symbolize_names: true)
26
+ end
26
27
 
27
- # Checks that the input was parsed successfully
28
- # and isn't something odd such as an array of primitives.
29
- #
30
- # @param value [Object] result of {#coerce}
31
- # @return [true,false]
32
- def coerced?(value)
33
- value.is_a?(::Hash) || coerced_collection?(value)
34
- end
28
+ # Checks that the input was parsed successfully
29
+ # and isn't something odd such as an array of primitives.
30
+ #
31
+ # @param value [Object] result of {#parse}
32
+ # @return [true,false]
33
+ def parsed?(value)
34
+ value.is_a?(::Hash) || coerced_collection?(value)
35
+ end
35
36
 
36
- protected
37
+ protected
37
38
 
38
- # Is the value an array of JSON-like objects?
39
- #
40
- # @param value [Object] result of {#coerce}
41
- # @return [true,false]
42
- def coerced_collection?(value)
43
- value.is_a?(::Array) && value.all? { |i| i.is_a? ::Hash }
39
+ # Is the value an array of JSON-like objects?
40
+ #
41
+ # @param value [Object] result of {#parse}
42
+ # @return [true,false]
43
+ def coerced_collection?(value)
44
+ value.is_a?(::Array) && value.all? { |i| i.is_a? ::Hash }
45
+ end
44
46
  end
45
47
  end
46
48
 
@@ -49,18 +51,20 @@ module Grape
49
51
  # objects and arrays of objects, but wraps single objects
50
52
  # in an Array.
51
53
  class JsonArray < Json
52
- # See {Json#coerce}. Wraps single objects in an array.
53
- #
54
- # @param input [String] JSON-encoded parameter value
55
- # @return [Array<Hash>]
56
- def call(input)
57
- json = super
58
- Array.wrap(json) unless json.nil?
59
- end
54
+ class << self
55
+ # See {Json#parse}. Wraps single objects in an array.
56
+ #
57
+ # @param input [String] JSON-encoded parameter value
58
+ # @return [Array<Hash>]
59
+ def parse(input)
60
+ json = super
61
+ Array.wrap(json) unless json.nil?
62
+ end
60
63
 
61
- # See {Json#coerced_collection?}
62
- def coerced?(value)
63
- coerced_collection? value
64
+ # See {Json#coerced_collection?}
65
+ def parsed?(value)
66
+ coerced_collection? value
67
+ end
64
68
  end
65
69
  end
66
70
  end