grape 0.12.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of grape might be problematic. Click here for more details.

Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +9 -4
  3. data/CHANGELOG.md +265 -215
  4. data/CONTRIBUTING.md +4 -4
  5. data/Gemfile +0 -1
  6. data/Gemfile.lock +166 -0
  7. data/README.md +426 -161
  8. data/RELEASING.md +14 -6
  9. data/Rakefile +30 -33
  10. data/UPGRADING.md +54 -23
  11. data/benchmark/simple.rb +27 -0
  12. data/gemfiles/rack_1.5.2.gemfile +13 -0
  13. data/gemfiles/rails_3.gemfile +2 -2
  14. data/gemfiles/rails_4.gemfile +1 -2
  15. data/grape.gemspec +6 -7
  16. data/lib/grape/api.rb +24 -4
  17. data/lib/grape/dsl/callbacks.rb +20 -0
  18. data/lib/grape/dsl/configuration.rb +59 -2
  19. data/lib/grape/dsl/helpers.rb +8 -3
  20. data/lib/grape/dsl/inside_route.rb +100 -45
  21. data/lib/grape/dsl/parameters.rb +96 -7
  22. data/lib/grape/dsl/request_response.rb +1 -1
  23. data/lib/grape/dsl/routing.rb +17 -4
  24. data/lib/grape/dsl/settings.rb +36 -1
  25. data/lib/grape/dsl/validations.rb +7 -5
  26. data/lib/grape/endpoint.rb +102 -57
  27. data/lib/grape/error_formatter/base.rb +6 -6
  28. data/lib/grape/exceptions/base.rb +5 -5
  29. data/lib/grape/exceptions/invalid_version_header.rb +10 -0
  30. data/lib/grape/exceptions/unknown_parameter.rb +10 -0
  31. data/lib/grape/exceptions/validation_errors.rb +4 -3
  32. data/lib/grape/formatter/serializable_hash.rb +3 -2
  33. data/lib/grape/http/headers.rb +0 -1
  34. data/lib/grape/locale/en.yml +5 -1
  35. data/lib/grape/middleware/auth/base.rb +2 -2
  36. data/lib/grape/middleware/auth/dsl.rb +1 -1
  37. data/lib/grape/middleware/auth/strategies.rb +1 -1
  38. data/lib/grape/middleware/base.rb +8 -4
  39. data/lib/grape/middleware/error.rb +3 -2
  40. data/lib/grape/middleware/filter.rb +1 -1
  41. data/lib/grape/middleware/formatter.rb +64 -45
  42. data/lib/grape/middleware/globals.rb +3 -3
  43. data/lib/grape/middleware/versioner/accept_version_header.rb +5 -7
  44. data/lib/grape/middleware/versioner/header.rb +113 -50
  45. data/lib/grape/middleware/versioner/param.rb +5 -8
  46. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +20 -0
  47. data/lib/grape/middleware/versioner/path.rb +3 -6
  48. data/lib/grape/namespace.rb +13 -2
  49. data/lib/grape/path.rb +4 -3
  50. data/lib/grape/request.rb +40 -0
  51. data/lib/grape/route.rb +5 -0
  52. data/lib/grape/util/content_types.rb +9 -9
  53. data/lib/grape/util/env.rb +22 -0
  54. data/lib/grape/util/file_response.rb +21 -0
  55. data/lib/grape/util/inheritable_setting.rb +23 -2
  56. data/lib/grape/util/inheritable_values.rb +1 -1
  57. data/lib/grape/util/stackable_values.rb +5 -2
  58. data/lib/grape/util/strict_hash_configuration.rb +2 -1
  59. data/lib/grape/validations/attributes_iterator.rb +8 -3
  60. data/lib/grape/validations/params_scope.rb +164 -22
  61. data/lib/grape/validations/types/build_coercer.rb +53 -0
  62. data/lib/grape/validations/types/custom_type_coercer.rb +183 -0
  63. data/lib/grape/validations/types/file.rb +28 -0
  64. data/lib/grape/validations/types/json.rb +65 -0
  65. data/lib/grape/validations/types/multiple_type_coercer.rb +76 -0
  66. data/lib/grape/validations/types/variant_collection_coercer.rb +59 -0
  67. data/lib/grape/validations/types/virtus_collection_patch.rb +16 -0
  68. data/lib/grape/validations/types.rb +144 -0
  69. data/lib/grape/validations/validators/all_or_none.rb +1 -1
  70. data/lib/grape/validations/validators/allow_blank.rb +3 -3
  71. data/lib/grape/validations/validators/base.rb +7 -0
  72. data/lib/grape/validations/validators/coerce.rb +32 -34
  73. data/lib/grape/validations/validators/presence.rb +2 -3
  74. data/lib/grape/validations/validators/regexp.rb +2 -4
  75. data/lib/grape/validations/validators/values.rb +3 -3
  76. data/lib/grape/validations.rb +5 -0
  77. data/lib/grape/version.rb +2 -1
  78. data/lib/grape.rb +15 -12
  79. data/pkg/grape-0.13.0.gem +0 -0
  80. data/spec/grape/api/custom_validations_spec.rb +5 -4
  81. data/spec/grape/api/deeply_included_options_spec.rb +7 -7
  82. data/spec/grape/api/nested_helpers_spec.rb +4 -2
  83. data/spec/grape/api/shared_helpers_spec.rb +8 -8
  84. data/spec/grape/api_spec.rb +151 -54
  85. data/spec/grape/dsl/configuration_spec.rb +13 -0
  86. data/spec/grape/dsl/helpers_spec.rb +16 -2
  87. data/spec/grape/dsl/inside_route_spec.rb +40 -4
  88. data/spec/grape/dsl/parameters_spec.rb +0 -6
  89. data/spec/grape/dsl/routing_spec.rb +1 -1
  90. data/spec/grape/dsl/validations_spec.rb +18 -0
  91. data/spec/grape/endpoint_spec.rb +130 -6
  92. data/spec/grape/entity_spec.rb +10 -8
  93. data/spec/grape/exceptions/invalid_accept_header_spec.rb +1 -15
  94. data/spec/grape/exceptions/validation_errors_spec.rb +28 -0
  95. data/spec/grape/integration/rack_spec.rb +3 -2
  96. data/spec/grape/middleware/base_spec.rb +40 -16
  97. data/spec/grape/middleware/error_spec.rb +16 -15
  98. data/spec/grape/middleware/exception_spec.rb +45 -43
  99. data/spec/grape/middleware/formatter_spec.rb +34 -5
  100. data/spec/grape/middleware/versioner/header_spec.rb +79 -47
  101. data/spec/grape/path_spec.rb +10 -10
  102. data/spec/grape/presenters/presenter_spec.rb +2 -2
  103. data/spec/grape/request_spec.rb +100 -0
  104. data/spec/grape/util/inheritable_values_spec.rb +14 -0
  105. data/spec/grape/util/stackable_values_spec.rb +10 -0
  106. data/spec/grape/validations/params_scope_spec.rb +86 -0
  107. data/spec/grape/validations/types_spec.rb +95 -0
  108. data/spec/grape/validations/validators/coerce_spec.rb +364 -10
  109. data/spec/grape/validations/validators/values_spec.rb +27 -15
  110. data/spec/grape/validations_spec.rb +53 -24
  111. data/spec/shared/versioning_examples.rb +2 -2
  112. data/spec/spec_helper.rb +0 -1
  113. data/spec/support/versioned_helpers.rb +2 -2
  114. metadata +55 -14
  115. data/.gitignore +0 -46
  116. data/.rspec +0 -2
  117. data/.rubocop.yml +0 -7
  118. data/.rubocop_todo.yml +0 -84
  119. data/.travis.yml +0 -20
  120. data/.yardopts +0 -2
  121. data/lib/backports/active_support/deep_dup.rb +0 -49
  122. data/lib/backports/active_support/duplicable.rb +0 -88
  123. data/lib/grape/http/request.rb +0 -27
@@ -0,0 +1,22 @@
1
+ module Grape
2
+ module Env
3
+ API_VERSION = 'api.version'.freeze
4
+ API_ENDPOINT = 'api.endpoint'.freeze
5
+ API_REQUEST_INPUT = 'api.request.input'.freeze
6
+ API_REQUEST_BODY = 'api.request.body'.freeze
7
+ API_TYPE = 'api.type'.freeze
8
+ API_SUBTYPE = 'api.subtype'.freeze
9
+ API_VENDOR = 'api.vendor'.freeze
10
+ API_FORMAT = 'api.format'.freeze
11
+
12
+ RACK_INPUT = 'rack.input'.freeze
13
+ RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'.freeze
14
+ RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'.freeze
15
+ RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'.freeze
16
+ RACK_ROUTING_ARGS = 'rack.routing_args'.freeze
17
+
18
+ GRAPE_REQUEST = 'grape.request'.freeze
19
+ GRAPE_REQUEST_HEADERS = 'grape.request.headers'.freeze
20
+ GRAPE_REQUEST_PARAMS = 'grape.request.params'.freeze
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ module Grape
2
+ module Util
3
+ # A simple class used to identify responses which represent files and do not
4
+ # need to be formatted or pre-read by Rack::Response
5
+ class FileResponse
6
+ attr_reader :file
7
+
8
+ # @param file [Object]
9
+ def initialize(file)
10
+ @file = file
11
+ end
12
+
13
+ # Equality provided mostly for tests.
14
+ #
15
+ # @return [Boolean]
16
+ def ==(other)
17
+ file == other.file
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,22 +1,31 @@
1
1
  module Grape
2
2
  module Util
3
+ # A branchable, inheritable settings object which can store both stackable
4
+ # and inheritable values (see InheritableValues and StackableValues).
3
5
  class InheritableSetting
4
6
  attr_accessor :route, :api_class, :namespace, :namespace_inheritable, :namespace_stackable
5
7
  attr_accessor :parent, :point_in_time_copies
6
8
 
9
+ # Retrieve global settings.
7
10
  def self.global
8
11
  @global ||= {}
9
12
  end
10
13
 
11
- def self.reset_global! # only for testing
14
+ # Clear all global settings.
15
+ # @api private
16
+ # @note only for testing
17
+ def self.reset_global!
12
18
  @global = {}
13
19
  end
14
20
 
21
+ # Instantiate a new settings instance, with blank values. The fresh
22
+ # instance can then be set to inherit from an existing instance (see
23
+ # #inherit_from).
15
24
  def initialize
16
25
  self.route = {}
17
26
  self.api_class = {}
18
27
  self.namespace = InheritableValues.new # only inheritable from a parent when
19
- # used with a mount, or should every API::Class be a seperate namespace by default?
28
+ # used with a mount, or should every API::Class be a separate namespace by default?
20
29
  self.namespace_inheritable = InheritableValues.new
21
30
  self.namespace_stackable = StackableValues.new
22
31
 
@@ -25,10 +34,15 @@ module Grape
25
34
  self.parent = nil
26
35
  end
27
36
 
37
+ # Return the class-level global properties.
28
38
  def global
29
39
  self.class.global
30
40
  end
31
41
 
42
+ # Set our inherited values to the given parent's current values. Also,
43
+ # update the inherited values on any settings instances which were forked
44
+ # from us.
45
+ # @param parent [InheritableSetting]
32
46
  def inherit_from(parent)
33
47
  return if parent.nil?
34
48
 
@@ -41,6 +55,10 @@ module Grape
41
55
  point_in_time_copies.map { |cloned_one| cloned_one.inherit_from parent }
42
56
  end
43
57
 
58
+ # Create a point-in-time copy of this settings instance, with clones of
59
+ # all our values. Note that, should this instance's parent be set or
60
+ # changed via #inherit_from, it will copy that inheritence to any copies
61
+ # which were made.
44
62
  def point_in_time_copy
45
63
  self.class.new.tap do |new_setting|
46
64
  point_in_time_copies << new_setting
@@ -56,10 +74,13 @@ module Grape
56
74
  end
57
75
  end
58
76
 
77
+ # Resets the instance store of per-route settings.
78
+ # @api private
59
79
  def route_end
60
80
  @route = {}
61
81
  end
62
82
 
83
+ # Return a serializable hash of our values.
63
84
  def to_hash
64
85
  {
65
86
  global: global.clone,
@@ -36,7 +36,7 @@ module Grape
36
36
  def initialize_copy(other)
37
37
  super
38
38
  self.inherited_values = other.inherited_values
39
- self.new_values = other.new_values.deep_dup
39
+ self.new_values = other.new_values.dup
40
40
  end
41
41
 
42
42
  protected
@@ -13,7 +13,10 @@ module Grape
13
13
 
14
14
  def [](name)
15
15
  return @froozen_values[name] if @froozen_values.key? name
16
- [@inherited_values[name], @new_values[name]].compact.flatten(1)
16
+ value = [@inherited_values[name], @new_values[name]]
17
+ value.compact!
18
+ value.flatten!(1)
19
+ value
17
20
  end
18
21
 
19
22
  def []=(name, value)
@@ -45,7 +48,7 @@ module Grape
45
48
  def initialize_copy(other)
46
49
  super
47
50
  self.inherited_values = other.inherited_values
48
- self.new_values = other.new_values.deep_dup
51
+ self.new_values = other.new_values.dup
49
52
  end
50
53
  end
51
54
  end
@@ -64,7 +64,8 @@ module Grape
64
64
  end
65
65
 
66
66
  define_method 'to_hash' do
67
- merge_hash = setting_name.keys.each_with_object({}) { |k, hash| hash[k] = send("#{k}_context").to_hash }
67
+ merge_hash = {}
68
+ setting_name.each_key { |k| merge_hash[k] = send("#{k}_context").to_hash }
68
69
 
69
70
  @settings.to_hash.merge(
70
71
  merge_hash
@@ -3,15 +3,20 @@ module Grape
3
3
  class AttributesIterator
4
4
  include Enumerable
5
5
 
6
+ attr_reader :scope
7
+
6
8
  def initialize(validator, scope, params)
9
+ @scope = scope
7
10
  @attrs = validator.attrs
8
- @params = scope.params(params)
9
- @params = (@params.is_a?(Array) ? @params : [@params])
11
+ @params = Array.wrap(scope.params(params))
10
12
  end
11
13
 
12
14
  def each
13
15
  @params.each do |resource_params|
14
- @attrs.each do |attr_name|
16
+ @attrs.each_with_index do |attr_name, index|
17
+ if resource_params.is_a?(Hash) && resource_params[attr_name].is_a?(Array)
18
+ scope.index = index
19
+ end
15
20
  yield resource_params, attr_name
16
21
  end
17
22
  end
@@ -1,16 +1,30 @@
1
1
  module Grape
2
2
  module Validations
3
3
  class ParamsScope
4
- attr_accessor :element, :parent
4
+ attr_accessor :element, :parent, :index
5
5
 
6
6
  include Grape::DSL::Parameters
7
7
 
8
+ # Open up a new ParamsScope, allowing parameter definitions per
9
+ # Grape::DSL::Params.
10
+ # @param opts [Hash] options for this scope
11
+ # @option opts :element [Symbol] the element that contains this scope; for
12
+ # this to be relevant, @parent must be set
13
+ # @option opts :parent [ParamsScope] the scope containing this scope
14
+ # @option opts :api [API] the API endpoint to modify
15
+ # @option opts :optional [Boolean] whether or not this scope needs to have
16
+ # any parameters set or not
17
+ # @option opts :type [Class] a type meant to govern this scope (deprecated)
18
+ # @option opts :dependent_on [Symbol] if present, this scope should only
19
+ # validate if this param is present in the parent scope
20
+ # @yield the instance context, open for parameter definitions
8
21
  def initialize(opts, &block)
9
- @element = opts[:element]
10
- @parent = opts[:parent]
11
- @api = opts[:api]
12
- @optional = opts[:optional] || false
13
- @type = opts[:type]
22
+ @element = opts[:element]
23
+ @parent = opts[:parent]
24
+ @api = opts[:api]
25
+ @optional = opts[:optional] || false
26
+ @type = opts[:type]
27
+ @dependent_on = opts[:dependent_on]
14
28
  @declared_params = []
15
29
 
16
30
  instance_eval(&block) if block_given?
@@ -18,27 +32,63 @@ module Grape
18
32
  configure_declared_params
19
33
  end
20
34
 
35
+ # @return [Boolean] whether or not this entire scope needs to be
36
+ # validated
21
37
  def should_validate?(parameters)
22
38
  return false if @optional && params(parameters).respond_to?(:all?) && params(parameters).all?(&:blank?)
39
+ return false if @dependent_on && params(parameters).try(:[], @dependent_on).blank?
23
40
  return true if parent.nil?
24
41
  parent.should_validate?(parameters)
25
42
  end
26
43
 
44
+ # @return [String] the proper attribute name, with nesting considered.
27
45
  def full_name(name)
28
- return "#{@parent.full_name(@element)}[#{name}]" if @parent
29
- name.to_s
46
+ case
47
+ when nested?
48
+ # Find our containing element's name, and append ours.
49
+ "#{@parent.full_name(@element)}#{parent_index}[#{name}]"
50
+ when lateral?
51
+ # Find the name of the element as if it was at the
52
+ # same nesting level as our parent.
53
+ @parent.full_name(name)
54
+ else
55
+ # We must be the root scope, so no prefix needed.
56
+ name.to_s
57
+ end
58
+ end
59
+
60
+ def parent_index
61
+ "[#{@parent.index}]" if @parent.present? && @parent.index.present?
30
62
  end
31
63
 
64
+ # @return [Boolean] whether or not this scope is the root-level scope
32
65
  def root?
33
66
  !@parent
34
67
  end
35
68
 
69
+ # A nested scope is contained in one of its parent's elements.
70
+ # @return [Boolean] whether or not this scope is nested
71
+ def nested?
72
+ @parent && @element
73
+ end
74
+
75
+ # A lateral scope is subordinate to its parent, but its keys are at the
76
+ # same level as its parent and thus is not contained within an element.
77
+ # @return [Boolean] whether or not this scope is lateral
78
+ def lateral?
79
+ @parent && !@element
80
+ end
81
+
82
+ # @return [Boolean] whether or not this scope needs to be present, or can
83
+ # be blank
36
84
  def required?
37
85
  !@optional
38
86
  end
39
87
 
40
88
  protected
41
89
 
90
+ # Adds a parameter declaration to our list of validations.
91
+ # @param attrs [Array] (see Grape::DSL::Parameters#requires)
42
92
  def push_declared_params(attrs)
43
93
  @declared_params.concat attrs
44
94
  end
@@ -79,21 +129,46 @@ module Grape
79
129
  validates(attrs, validations)
80
130
  end
81
131
 
132
+ # Returns a new parameter scope, subordinate to the current one and nested
133
+ # under the parameter corresponding to `attrs.first`.
134
+ # @param attrs [Array] the attributes passed to the `requires` or
135
+ # `optional` invocation that opened this scope.
136
+ # @param optional [Boolean] whether the parameter this are nested under
137
+ # is optional or not (and hence, whether this block's params will be).
138
+ # @yield parameter scope
82
139
  def new_scope(attrs, optional = false, &block)
83
140
  # if required params are grouped and no type or unsupported type is provided, raise an error
84
141
  type = attrs[1] ? attrs[1][:type] : nil
85
142
  if attrs.first && !optional
86
143
  fail Grape::Exceptions::MissingGroupTypeError.new if type.nil?
87
- fail Grape::Exceptions::UnsupportedGroupTypeError.new unless [Array, Hash].include?(type)
144
+ fail Grape::Exceptions::UnsupportedGroupTypeError.new unless [Array, Hash, JSON, Array[JSON]].include?(type)
88
145
  end
89
146
 
90
147
  opts = attrs[1] || { type: Array }
91
- ParamsScope.new(api: @api, element: attrs.first, parent: self, optional: optional, type: opts[:type], &block)
148
+ self.class.new(api: @api, element: attrs.first, parent: self, optional: optional, type: opts[:type], &block)
149
+ end
150
+
151
+ # Returns a new parameter scope, not nested under any current-level param
152
+ # but instead at the same level as the current scope.
153
+ # @param options [Hash] options to control how this new scope behaves
154
+ # @option options :dependent_on [Symbol] if given, specifies that this
155
+ # scope should only validate if this parameter from the above scope is
156
+ # present
157
+ # @yield parameter scope
158
+ def new_lateral_scope(options, &block)
159
+ self.class.new(
160
+ api: @api,
161
+ element: nil,
162
+ parent: self,
163
+ options: @optional,
164
+ type: Hash,
165
+ dependent_on: options[:dependent_on],
166
+ &block)
92
167
  end
93
168
 
94
169
  # Pushes declared params to parent or settings
95
170
  def configure_declared_params
96
- if @parent
171
+ if nested?
97
172
  @parent.push_declared_params [element => @declared_params]
98
173
  else
99
174
  @api.namespace_stackable(:declared_params, @declared_params)
@@ -106,10 +181,7 @@ module Grape
106
181
  def validates(attrs, validations)
107
182
  doc_attrs = { required: validations.keys.include?(:presence) }
108
183
 
109
- # special case (type = coerce)
110
- validations[:coerce] = validations.delete(:type) if validations.key?(:type)
111
-
112
- coerce_type = validations[:coerce]
184
+ coerce_type = infer_coercion(validations)
113
185
 
114
186
  doc_attrs[:type] = coerce_type.to_s if coerce_type
115
187
 
@@ -141,19 +213,87 @@ module Grape
141
213
  validations.delete(:presence)
142
214
  end
143
215
 
144
- # Before we run the rest of the validators, lets handle
216
+ # Before we run the rest of the validators, let's handle
145
217
  # whatever coercion so that we are working with correctly
146
218
  # type casted values
147
- if validations.key? :coerce
148
- validate('coerce', validations[:coerce], attrs, doc_attrs)
149
- validations.delete(:coerce)
150
- end
219
+ coerce_type validations, attrs, doc_attrs
151
220
 
152
221
  validations.each do |type, options|
153
222
  validate(type, options, attrs, doc_attrs)
154
223
  end
155
224
  end
156
225
 
226
+ # Validate and comprehend the +:type+, +:types+, and +:coerce_with+
227
+ # options that have been supplied to the parameter declaration.
228
+ # The +:type+ and +:types+ options will be removed from the
229
+ # validations list, replaced appropriately with +:coerce+ and
230
+ # +:coerce_with+ options that will later be passed to
231
+ # {Validators::CoerceValidator}. The type that is returned may be
232
+ # used for documentation and further validation of parameter
233
+ # options.
234
+ #
235
+ # @param validations [Hash] list of validations supplied to the
236
+ # parameter declaration
237
+ # @return [class-like] type to which the parameter will be coerced
238
+ # @raise [ArgumentError] if the given type options are invalid
239
+ def infer_coercion(validations)
240
+ if validations.key?(:type) && validations.key?(:types)
241
+ fail ArgumentError, ':type may not be supplied with :types'
242
+ end
243
+
244
+ validations[:coerce] = validations[:type] if validations.key?(:type)
245
+ validations[:coerce] = validations.delete(:types) if validations.key?(:types)
246
+
247
+ coerce_type = validations[:coerce]
248
+
249
+ # Special case - when the argument is a single type that is a
250
+ # variant-type collection.
251
+ if Types.multiple?(coerce_type) && validations.key?(:type)
252
+ validations[:coerce] = Types::VariantCollectionCoercer.new(
253
+ coerce_type,
254
+ validations.delete(:coerce_with)
255
+ )
256
+ end
257
+ validations.delete(:type)
258
+
259
+ coerce_type
260
+ end
261
+
262
+ # Enforce correct usage of :coerce_with parameter.
263
+ # We do not allow coercion without a type, nor with
264
+ # +JSON+ as a type since this defines its own coercion
265
+ # method.
266
+ def check_coerce_with(validations)
267
+ return unless validations.key?(:coerce_with)
268
+ # type must be supplied for coerce_with..
269
+ fail ArgumentError, 'must supply type for coerce_with' unless validations.key?(:coerce)
270
+
271
+ # but not special JSON types, which
272
+ # already imply coercion method
273
+ return unless [JSON, Array[JSON]].include? validations[:coerce]
274
+ fail ArgumentError, 'coerce_with disallowed for type: JSON'
275
+ end
276
+
277
+ # Add type coercion validation to this scope,
278
+ # if any has been specified.
279
+ # This validation has special handling since it is
280
+ # composited from more than one +requires+/+optional+
281
+ # parameter, and needs to be run before most other
282
+ # validations.
283
+ def coerce_type(validations, attrs, doc_attrs)
284
+ check_coerce_with(validations)
285
+
286
+ return unless validations.key?(:coerce)
287
+
288
+ coerce_options = {
289
+ type: validations[:coerce],
290
+ method: validations[:coerce_with]
291
+ }
292
+ validate('coerce', coerce_options, attrs, doc_attrs)
293
+ validations.delete(:coerce_with)
294
+ validations.delete(:coerce)
295
+ end
296
+
157
297
  def guess_coerce_type(coerce_type, values)
158
298
  return coerce_type if !values || values.is_a?(Proc)
159
299
  return values.first.class if coerce_type == Array && (values.is_a?(Range) || !values.empty?)
@@ -183,9 +323,11 @@ module Grape
183
323
  return if values.is_a?(Proc)
184
324
  coerce_type = coerce_type.first if coerce_type.is_a?(Array)
185
325
  value_types = values.is_a?(Range) ? [values.begin, values.end] : values
186
- if value_types.any? { |v| !v.is_a?(coerce_type) }
187
- fail Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
326
+ if coerce_type == Virtus::Attribute::Boolean
327
+ value_types = value_types.map { |type| Virtus::Attribute.build(type) }
188
328
  end
329
+ return unless value_types.any? { |v| !v.is_a?(coerce_type) }
330
+ fail Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
189
331
  end
190
332
  end
191
333
  end
@@ -0,0 +1,53 @@
1
+ module Grape
2
+ module Validations
3
+ module Types
4
+ # Work out the +Virtus::Attribute+ object to
5
+ # use for coercing strings to the given +type+.
6
+ # Coercion +method+ will be inferred if none is
7
+ # supplied.
8
+ #
9
+ # If a +Virtus::Attribute+ object already built
10
+ # with +Virtus::Attribute.build+ is supplied as
11
+ # the +type+ it will be returned and +method+
12
+ # will be ignored.
13
+ #
14
+ # See {CustomTypeCoercer} for further details
15
+ # about coercion and type-checking inference.
16
+ #
17
+ # @param type [Class] the type to which input strings
18
+ # should be coerced
19
+ # @param method [Class,#call] the coercion method to use
20
+ # @return [Virtus::Attribute] object to be used
21
+ # for coercion and type validation
22
+ def self.build_coercer(type, method = nil)
23
+ # Accept pre-rolled virtus attributes without interference
24
+ return type if type.is_a? Virtus::Attribute
25
+
26
+ converter_options = {
27
+ nullify_blank: true
28
+ }
29
+ conversion_type = type
30
+
31
+ # Use a special coercer for multiply-typed parameters.
32
+ if Types.multiple?(type)
33
+ converter_options[:coercer] = Types::MultipleTypeCoercer.new(type, method)
34
+ conversion_type = Object
35
+
36
+ # Use a special coercer for custom types and coercion methods.
37
+ elsif method || Types.custom?(type)
38
+ converter_options[:coercer] = Types::CustomTypeCoercer.new(type, method)
39
+
40
+ # Grape swaps in its own Virtus::Attribute implementations
41
+ # for certain special types that merit first-class support
42
+ # (but not if a custom coercion method has been supplied).
43
+ elsif Types.special?(type)
44
+ conversion_type = Types::SPECIAL[type]
45
+ end
46
+
47
+ # Virtus will infer coercion and validation rules
48
+ # for many common ruby types.
49
+ Virtus::Attribute.build(conversion_type, converter_options)
50
+ end
51
+ end
52
+ end
53
+ end