grape 1.3.1 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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