grape 1.6.1 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (151) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/CONTRIBUTING.md +1 -1
  4. data/README.md +120 -19
  5. data/UPGRADING.md +19 -4
  6. data/lib/grape/api/instance.rb +1 -1
  7. data/lib/grape/dsl/api.rb +0 -2
  8. data/lib/grape/dsl/callbacks.rb +0 -2
  9. data/lib/grape/dsl/configuration.rb +0 -2
  10. data/lib/grape/dsl/desc.rb +0 -15
  11. data/lib/grape/dsl/helpers.rb +0 -2
  12. data/lib/grape/dsl/inside_route.rb +33 -29
  13. data/lib/grape/dsl/middleware.rb +0 -2
  14. data/lib/grape/dsl/parameters.rb +5 -7
  15. data/lib/grape/dsl/request_response.rb +0 -2
  16. data/lib/grape/dsl/routing.rb +4 -2
  17. data/lib/grape/dsl/settings.rb +0 -2
  18. data/lib/grape/dsl/validations.rb +0 -15
  19. data/lib/grape/error_formatter/json.rb +7 -1
  20. data/lib/grape/exceptions/base.rb +2 -2
  21. data/lib/grape/exceptions/missing_group_type.rb +8 -1
  22. data/lib/grape/exceptions/too_many_multipart_files.rb +11 -0
  23. data/lib/grape/exceptions/unsupported_group_type.rb +8 -1
  24. data/lib/grape/exceptions/validation.rb +0 -4
  25. data/lib/grape/locale/en.yml +9 -8
  26. data/lib/grape/middleware/auth/dsl.rb +0 -1
  27. data/lib/grape/middleware/error.rb +2 -2
  28. data/lib/grape/request.rb +2 -0
  29. data/lib/grape/validations/attributes_doc.rb +58 -0
  30. data/lib/grape/validations/params_scope.rb +66 -40
  31. data/lib/grape/validations/types/array_coercer.rb +2 -2
  32. data/lib/grape/validations/types/build_coercer.rb +94 -0
  33. data/lib/grape/validations/types/dry_type_coercer.rb +13 -8
  34. data/lib/grape/validations/types/json.rb +2 -0
  35. data/lib/grape/validations/types/primitive_coercer.rb +20 -10
  36. data/lib/grape/validations/types/set_coercer.rb +3 -2
  37. data/lib/grape/validations/types.rb +20 -26
  38. data/lib/grape/validations/validators/base.rb +7 -0
  39. data/lib/grape/validations.rb +16 -6
  40. data/lib/grape/version.rb +1 -1
  41. data/lib/grape.rb +20 -15
  42. data/spec/grape/api/custom_validations_spec.rb +41 -2
  43. data/spec/grape/api/deeply_included_options_spec.rb +0 -2
  44. data/spec/grape/api/defines_boolean_in_params_spec.rb +0 -2
  45. data/spec/grape/api/documentation_spec.rb +59 -0
  46. data/spec/grape/api/inherited_helpers_spec.rb +0 -2
  47. data/spec/grape/api/instance_spec.rb +0 -1
  48. data/spec/grape/api/invalid_format_spec.rb +0 -2
  49. data/spec/grape/api/namespace_parameters_in_route_spec.rb +0 -2
  50. data/spec/grape/api/nested_helpers_spec.rb +0 -2
  51. data/spec/grape/api/optional_parameters_in_route_spec.rb +0 -2
  52. data/spec/grape/api/parameters_modification_spec.rb +0 -2
  53. data/spec/grape/api/patch_method_helpers_spec.rb +0 -2
  54. data/spec/grape/api/recognize_path_spec.rb +0 -2
  55. data/spec/grape/api/required_parameters_in_route_spec.rb +0 -2
  56. data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +0 -2
  57. data/spec/grape/api/routes_with_requirements_spec.rb +0 -2
  58. data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +0 -2
  59. data/spec/grape/api/shared_helpers_spec.rb +0 -2
  60. data/spec/grape/api_remount_spec.rb +0 -1
  61. data/spec/grape/api_spec.rb +18 -5
  62. data/spec/grape/config_spec.rb +0 -2
  63. data/spec/grape/dsl/callbacks_spec.rb +0 -2
  64. data/spec/grape/dsl/configuration_spec.rb +0 -2
  65. data/spec/grape/dsl/desc_spec.rb +0 -2
  66. data/spec/grape/dsl/headers_spec.rb +2 -4
  67. data/spec/grape/dsl/helpers_spec.rb +0 -2
  68. data/spec/grape/dsl/inside_route_spec.rb +10 -12
  69. data/spec/grape/dsl/logger_spec.rb +0 -2
  70. data/spec/grape/dsl/middleware_spec.rb +0 -2
  71. data/spec/grape/dsl/parameters_spec.rb +0 -2
  72. data/spec/grape/dsl/request_response_spec.rb +6 -8
  73. data/spec/grape/dsl/routing_spec.rb +1 -3
  74. data/spec/grape/dsl/settings_spec.rb +0 -2
  75. data/spec/grape/dsl/validations_spec.rb +0 -17
  76. data/spec/grape/endpoint/declared_spec.rb +2 -4
  77. data/spec/grape/endpoint_spec.rb +22 -3
  78. data/spec/grape/entity_spec.rb +0 -1
  79. data/spec/grape/exceptions/base_spec.rb +16 -2
  80. data/spec/grape/exceptions/body_parse_errors_spec.rb +0 -2
  81. data/spec/grape/exceptions/invalid_accept_header_spec.rb +0 -2
  82. data/spec/grape/exceptions/invalid_formatter_spec.rb +0 -2
  83. data/spec/grape/exceptions/invalid_response_spec.rb +0 -2
  84. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +1 -3
  85. data/spec/grape/exceptions/missing_group_type_spec.rb +21 -0
  86. data/spec/grape/exceptions/missing_mime_type_spec.rb +0 -2
  87. data/spec/grape/exceptions/missing_option_spec.rb +1 -3
  88. data/spec/grape/exceptions/unknown_options_spec.rb +0 -2
  89. data/spec/grape/exceptions/unknown_validator_spec.rb +0 -2
  90. data/spec/grape/exceptions/unsupported_group_type_spec.rb +23 -0
  91. data/spec/grape/exceptions/validation_errors_spec.rb +0 -1
  92. data/spec/grape/exceptions/validation_spec.rb +1 -3
  93. data/spec/grape/extensions/param_builders/hash_spec.rb +0 -2
  94. data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +0 -2
  95. data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +0 -2
  96. data/spec/grape/integration/global_namespace_function_spec.rb +0 -2
  97. data/spec/grape/integration/rack_sendfile_spec.rb +0 -2
  98. data/spec/grape/integration/rack_spec.rb +0 -2
  99. data/spec/grape/loading_spec.rb +0 -2
  100. data/spec/grape/middleware/auth/base_spec.rb +0 -1
  101. data/spec/grape/middleware/auth/dsl_spec.rb +0 -2
  102. data/spec/grape/middleware/auth/strategies_spec.rb +0 -2
  103. data/spec/grape/middleware/base_spec.rb +0 -2
  104. data/spec/grape/middleware/error_spec.rb +6 -1
  105. data/spec/grape/middleware/exception_spec.rb +0 -2
  106. data/spec/grape/middleware/formatter_spec.rb +0 -2
  107. data/spec/grape/middleware/globals_spec.rb +0 -2
  108. data/spec/grape/middleware/stack_spec.rb +0 -2
  109. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +0 -2
  110. data/spec/grape/middleware/versioner/header_spec.rb +18 -4
  111. data/spec/grape/middleware/versioner/param_spec.rb +0 -2
  112. data/spec/grape/middleware/versioner/path_spec.rb +0 -2
  113. data/spec/grape/middleware/versioner_spec.rb +0 -2
  114. data/spec/grape/named_api_spec.rb +0 -2
  115. data/spec/grape/parser_spec.rb +0 -2
  116. data/spec/grape/path_spec.rb +0 -2
  117. data/spec/grape/presenters/presenter_spec.rb +0 -2
  118. data/spec/grape/request_spec.rb +0 -2
  119. data/spec/grape/util/inheritable_setting_spec.rb +0 -1
  120. data/spec/grape/util/inheritable_values_spec.rb +0 -1
  121. data/spec/grape/util/reverse_stackable_values_spec.rb +0 -1
  122. data/spec/grape/util/stackable_values_spec.rb +0 -1
  123. data/spec/grape/util/strict_hash_configuration_spec.rb +0 -1
  124. data/spec/grape/validations/attributes_doc_spec.rb +153 -0
  125. data/spec/grape/validations/attributes_iterator_spec.rb +0 -2
  126. data/spec/grape/validations/instance_behaivour_spec.rb +0 -2
  127. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +0 -2
  128. data/spec/grape/validations/params_scope_spec.rb +315 -86
  129. data/spec/grape/validations/single_attribute_iterator_spec.rb +0 -2
  130. data/spec/grape/validations/types/array_coercer_spec.rb +0 -2
  131. data/spec/grape/validations/types/primitive_coercer_spec.rb +20 -5
  132. data/spec/grape/validations/types/set_coercer_spec.rb +0 -2
  133. data/spec/grape/validations/types_spec.rb +28 -2
  134. data/spec/grape/validations/validators/all_or_none_spec.rb +0 -2
  135. data/spec/grape/validations/validators/allow_blank_spec.rb +0 -2
  136. data/spec/grape/validations/validators/at_least_one_of_spec.rb +0 -2
  137. data/spec/grape/validations/validators/coerce_spec.rb +0 -2
  138. data/spec/grape/validations/validators/default_spec.rb +0 -2
  139. data/spec/grape/validations/validators/exactly_one_of_spec.rb +0 -2
  140. data/spec/grape/validations/validators/except_values_spec.rb +0 -2
  141. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +0 -2
  142. data/spec/grape/validations/validators/presence_spec.rb +0 -2
  143. data/spec/grape/validations/validators/regexp_spec.rb +0 -2
  144. data/spec/grape/validations/validators/same_as_spec.rb +0 -2
  145. data/spec/grape/validations/validators/values_spec.rb +0 -2
  146. data/spec/grape/validations_spec.rb +50 -22
  147. data/spec/integration/multi_json/json_spec.rb +0 -2
  148. data/spec/integration/multi_xml/xml_spec.rb +0 -2
  149. data/spec/spec_helper.rb +9 -4
  150. metadata +17 -8
  151. data/spec/support/eager_load.rb +0 -19
@@ -2,10 +2,17 @@
2
2
 
3
3
  module Grape
4
4
  module Exceptions
5
- class MissingGroupTypeError < Base
5
+ class MissingGroupType < Base
6
6
  def initialize
7
7
  super(message: compose_message(:missing_group_type))
8
8
  end
9
9
  end
10
10
  end
11
11
  end
12
+
13
+ Grape::Exceptions::MissingGroupTypeError = Class.new(Grape::Exceptions::MissingGroupType) do
14
+ def initialize(*)
15
+ super
16
+ warn '[DEPRECATION] `Grape::Exceptions::MissingGroupTypeError` is deprecated. Use `Grape::Exceptions::MissingGroupType` instead.'
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Exceptions
5
+ class TooManyMultipartFiles < Base
6
+ def initialize(limit)
7
+ super(message: compose_message(:too_many_multipart_files, limit: limit), status: 413)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -2,10 +2,17 @@
2
2
 
3
3
  module Grape
4
4
  module Exceptions
5
- class UnsupportedGroupTypeError < Base
5
+ class UnsupportedGroupType < Base
6
6
  def initialize
7
7
  super(message: compose_message(:unsupported_group_type))
8
8
  end
9
9
  end
10
10
  end
11
11
  end
12
+
13
+ Grape::Exceptions::UnsupportedGroupTypeError = Class.new(Grape::Exceptions::UnsupportedGroupType) do
14
+ def initialize(*)
15
+ super
16
+ warn '[DEPRECATION] `Grape::Exceptions::UnsupportedGroupTypeError` is deprecated. Use `Grape::Exceptions::UnsupportedGroupType` instead.'
17
+ end
18
+ end
@@ -21,10 +21,6 @@ module Grape
21
21
  def as_json(*_args)
22
22
  to_s
23
23
  end
24
-
25
- def to_s
26
- message
27
- end
28
24
  end
29
25
  end
30
26
  end
@@ -11,8 +11,8 @@ en:
11
11
  except_values: 'has a value not allowed'
12
12
  same_as: 'is not the same as %{parameter}'
13
13
  missing_vendor_option:
14
- problem: 'missing :vendor option.'
15
- summary: 'when version using header, you must specify :vendor option. '
14
+ problem: 'missing :vendor option'
15
+ summary: 'when version using header, you must specify :vendor option'
16
16
  resolution: "eg: version 'v1', using: :header, vendor: 'twitter'"
17
17
  missing_mime_type:
18
18
  problem: 'missing mime type for %{new_format}'
@@ -21,12 +21,12 @@ en:
21
21
  or add your own with content_type :%{new_format}, 'application/%{new_format}'
22
22
  "
23
23
  invalid_with_option_for_represent:
24
- problem: 'You must specify an entity class in the :with option.'
24
+ problem: 'you must specify an entity class in the :with option'
25
25
  resolution: 'eg: represent User, :with => Entity::User'
26
- missing_option: 'You must specify :%{option} options.'
26
+ missing_option: 'you must specify :%{option} options'
27
27
  invalid_formatter: 'cannot convert %{klass} to %{to_format}'
28
28
  invalid_versioner_option:
29
- problem: 'Unknown :using for versioner: %{strategy}'
29
+ problem: 'unknown :using for versioner: %{strategy}'
30
30
  resolution: 'available strategy for :using is :path, :header, :accept_version_header, :param'
31
31
  unknown_validator: 'unknown validator: %{validator_type}'
32
32
  unknown_options: 'unknown options: %{options}'
@@ -44,11 +44,12 @@ en:
44
44
  "when specifying %{body_format} as content-type, you must pass valid
45
45
  %{body_format} in the request's 'body'
46
46
  "
47
- empty_message_body: 'Empty message body supplied with %{body_format} content-type'
47
+ empty_message_body: 'empty message body supplied with %{body_format} content-type'
48
+ too_many_multipart_files: "the number of uploaded files exceeded the system's configured limit (%{limit})"
48
49
  invalid_accept_header:
49
- problem: 'Invalid accept header'
50
+ problem: 'invalid accept header'
50
51
  resolution: '%{message}'
51
52
  invalid_version_header:
52
- problem: 'Invalid version header'
53
+ problem: 'invalid version header'
53
54
  resolution: '%{message}'
54
55
  invalid_response: 'Invalid response'
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rack/auth/basic'
4
- require 'active_support/concern'
5
4
 
6
5
  module Grape
7
6
  module Middleware
@@ -72,7 +72,7 @@ module Grape
72
72
 
73
73
  def rack_response(message, status = options[:default_status], headers = { Grape::Http::Headers::CONTENT_TYPE => content_type })
74
74
  message = ERB::Util.html_escape(message) if headers[Grape::Http::Headers::CONTENT_TYPE] == TEXT_HTML
75
- Rack::Response.new([message], status, headers)
75
+ Rack::Response.new([message], Rack::Utils.status_code(status), headers)
76
76
  end
77
77
 
78
78
  def format_message(message, backtrace, original_exception = nil)
@@ -121,7 +121,7 @@ module Grape
121
121
 
122
122
  def run_rescue_handler(handler, error)
123
123
  if handler.instance_of?(Symbol)
124
- raise NoMethodError, "undefined method `#{handler}'" unless respond_to?(handler)
124
+ raise NoMethodError, "undefined method '#{handler}'" unless respond_to?(handler)
125
125
 
126
126
  handler = public_method(handler)
127
127
  end
data/lib/grape/request.rb CHANGED
@@ -17,6 +17,8 @@ module Grape
17
17
  @params ||= build_params
18
18
  rescue EOFError
19
19
  raise Grape::Exceptions::EmptyMessageBody.new(content_type)
20
+ rescue Rack::Multipart::MultipartPartLimitError
21
+ raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit)
20
22
  end
21
23
 
22
24
  def headers
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ class ParamsScope
6
+ # Documents parameters of an endpoint. If documentation isn't needed (for instance, it is an
7
+ # internal API), the class only cleans up attributes to avoid junk in RAM.
8
+ class AttributesDoc
9
+ attr_accessor :type, :values
10
+
11
+ # @param api [Grape::API::Instance]
12
+ # @param scope [Validations::ParamsScope]
13
+ def initialize(api, scope)
14
+ @api = api
15
+ @scope = scope
16
+ @type = type
17
+ end
18
+
19
+ def extract_details(validations)
20
+ details[:required] = validations.key?(:presence)
21
+
22
+ desc = validations.delete(:desc) || validations.delete(:description)
23
+
24
+ details[:desc] = desc if desc
25
+
26
+ documentation = validations.delete(:documentation)
27
+
28
+ details[:documentation] = documentation if documentation
29
+
30
+ details[:default] = validations[:default] if validations.key?(:default)
31
+ end
32
+
33
+ def document(attrs)
34
+ return if @api.namespace_inheritable(:do_not_document)
35
+
36
+ details[:type] = type.to_s if type
37
+ details[:values] = values if values
38
+
39
+ documented_attrs = attrs.each_with_object({}) do |name, memo|
40
+ memo[@scope.full_name(name)] = details
41
+ end
42
+
43
+ @api.namespace_stackable(:params, documented_attrs)
44
+ end
45
+
46
+ def required
47
+ details[:required]
48
+ end
49
+
50
+ protected
51
+
52
+ def details
53
+ @details ||= {}
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'attributes_doc'
4
+
3
5
  module Grape
4
6
  module Validations
5
7
  class ParamsScope
@@ -8,6 +10,35 @@ module Grape
8
10
 
9
11
  include Grape::DSL::Parameters
10
12
 
13
+ class Attr
14
+ attr_accessor :key, :scope
15
+
16
+ # Open up a new ParamsScope::Attr
17
+ # @param key [Hash, Symbol] key of attr
18
+ # @param scope [Grape::Validations::ParamsScope] scope of attr
19
+ def initialize(key, scope)
20
+ @key = key
21
+ @scope = scope
22
+ end
23
+
24
+ # @return Array[Symbol, Hash[Symbol => Array]] declared_params with symbol instead of Attr
25
+ def self.attrs_keys(declared_params)
26
+ declared_params.map do |declared_param_attr|
27
+ attr_key(declared_param_attr)
28
+ end
29
+ end
30
+
31
+ def self.attr_key(declared_param_attr)
32
+ return attr_key(declared_param_attr.key) if declared_param_attr.is_a?(self)
33
+
34
+ if declared_param_attr.is_a?(Hash)
35
+ declared_param_attr.transform_values { |value| attrs_keys(value) }
36
+ else
37
+ declared_param_attr
38
+ end
39
+ end
40
+ end
41
+
11
42
  # Open up a new ParamsScope, allowing parameter definitions per
12
43
  # Grape::DSL::Params.
13
44
  # @param opts [Hash] options for this scope
@@ -31,7 +62,7 @@ module Grape
31
62
  @api = opts[:api]
32
63
  @optional = opts[:optional] || false
33
64
  @type = opts[:type]
34
- @group = opts[:group] || {}
65
+ @group = opts[:group]
35
66
  @dependent_on = opts[:dependent_on]
36
67
  @declared_params = []
37
68
  @index = nil
@@ -64,6 +95,18 @@ module Grape
64
95
 
65
96
  return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array)
66
97
 
98
+ meets_hash_dependency?(params)
99
+ end
100
+
101
+ def attr_meets_dependency?(params)
102
+ return true unless @dependent_on
103
+
104
+ return false if @parent.present? && !@parent.attr_meets_dependency?(params)
105
+
106
+ meets_hash_dependency?(params)
107
+ end
108
+
109
+ def meets_hash_dependency?(params)
67
110
  # params might be anything what looks like a hash, so it must implement a `key?` method
68
111
  return false unless params.respond_to?(:key?)
69
112
 
@@ -128,13 +171,14 @@ module Grape
128
171
  # Adds a parameter declaration to our list of validations.
129
172
  # @param attrs [Array] (see Grape::DSL::Parameters#requires)
130
173
  def push_declared_params(attrs, **opts)
174
+ opts = opts.merge(declared_params_scope: self) unless opts.key?(:declared_params_scope)
131
175
  if lateral?
132
176
  @parent.push_declared_params(attrs, **opts)
133
177
  else
134
178
  push_renamed_param(full_path + [attrs.first], opts[:as]) \
135
179
  if opts && opts[:as]
136
180
 
137
- @declared_params.concat attrs
181
+ @declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, opts[:declared_params_scope]) })
138
182
  end
139
183
  end
140
184
 
@@ -206,8 +250,8 @@ module Grape
206
250
  # if required params are grouped and no type or unsupported type is provided, raise an error
207
251
  type = attrs[1] ? attrs[1][:type] : nil
208
252
  if attrs.first && !optional
209
- raise Grape::Exceptions::MissingGroupTypeError.new if type.nil?
210
- raise Grape::Exceptions::UnsupportedGroupTypeError.new unless Grape::Validations::Types.group?(type)
253
+ raise Grape::Exceptions::MissingGroupType if type.nil?
254
+ raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
211
255
  end
212
256
 
213
257
  self.class.new(
@@ -269,17 +313,14 @@ module Grape
269
313
  end
270
314
 
271
315
  def validates(attrs, validations)
272
- doc_attrs = { required: validations.key?(:presence) }
316
+ doc = AttributesDoc.new @api, self
317
+ doc.extract_details validations
273
318
 
274
319
  coerce_type = infer_coercion(validations)
275
320
 
276
- doc_attrs[:type] = coerce_type.to_s if coerce_type
277
-
278
- desc = validations.delete(:desc) || validations.delete(:description)
279
- doc_attrs[:desc] = desc if desc
321
+ doc.type = coerce_type
280
322
 
281
323
  default = validations[:default]
282
- doc_attrs[:default] = default if validations.key?(:default)
283
324
 
284
325
  if (values_hash = validations[:values]).is_a? Hash
285
326
  values = values_hash[:value]
@@ -288,7 +329,8 @@ module Grape
288
329
  else
289
330
  values = validations[:values]
290
331
  end
291
- doc_attrs[:values] = values if values
332
+
333
+ doc.values = values
292
334
 
293
335
  except_values = options_key?(:except_values, :value, validations) ? validations[:except_values][:value] : validations[:except_values]
294
336
 
@@ -304,28 +346,22 @@ module Grape
304
346
  # type should be compatible with values array, if both exist
305
347
  validate_value_coercion(coerce_type, values, except_values, excepts)
306
348
 
307
- doc_attrs[:documentation] = validations.delete(:documentation) if validations.key?(:documentation)
308
-
309
- document_attribute(attrs, doc_attrs)
349
+ doc.document attrs
310
350
 
311
351
  opts = derive_validator_options(validations)
312
352
 
313
- order_specific_validations = Set[:as]
314
-
315
353
  # Validate for presence before any other validators
316
- validates_presence(validations, attrs, doc_attrs, opts) do |validation_type|
317
- order_specific_validations << validation_type
318
- end
354
+ validates_presence(validations, attrs, doc, opts)
319
355
 
320
356
  # Before we run the rest of the validators, let's handle
321
357
  # whatever coercion so that we are working with correctly
322
358
  # type casted values
323
- coerce_type validations, attrs, doc_attrs, opts
359
+ coerce_type validations, attrs, doc, opts
324
360
 
325
361
  validations.each do |type, options|
326
- next if order_specific_validations.include?(type)
362
+ next if type == :as
327
363
 
328
- validate(type, options, attrs, doc_attrs, opts)
364
+ validate(type, options, attrs, doc, opts)
329
365
  end
330
366
  end
331
367
 
@@ -389,7 +425,7 @@ module Grape
389
425
  # composited from more than one +requires+/+optional+
390
426
  # parameter, and needs to be run before most other
391
427
  # validations.
392
- def coerce_type(validations, attrs, doc_attrs, opts)
428
+ def coerce_type(validations, attrs, doc, opts)
393
429
  check_coerce_with(validations)
394
430
 
395
431
  return unless validations.key?(:coerce)
@@ -399,7 +435,7 @@ module Grape
399
435
  method: validations[:coerce_with],
400
436
  message: validations[:coerce_message]
401
437
  }
402
- validate('coerce', coerce_options, attrs, doc_attrs, opts)
438
+ validate('coerce', coerce_options, attrs, doc, opts)
403
439
  validations.delete(:coerce_with)
404
440
  validations.delete(:coerce)
405
441
  validations.delete(:coerce_message)
@@ -430,18 +466,14 @@ module Grape
430
466
  unless Array(default).none? { |def_val| excepts.include?(def_val) }
431
467
  end
432
468
 
433
- def validate(type, options, attrs, doc_attrs, opts)
434
- validator_class = Validations.validators[type.to_s]
435
-
436
- raise Grape::Exceptions::UnknownValidator.new(type) unless validator_class
437
-
469
+ def validate(type, options, attrs, doc, opts)
438
470
  validator_options = {
439
471
  attributes: attrs,
440
472
  options: options,
441
- required: doc_attrs[:required],
473
+ required: doc.required,
442
474
  params_scope: self,
443
475
  opts: opts,
444
- validator_class: validator_class
476
+ validator_class: Validations.require_validator(type)
445
477
  }
446
478
  @api.namespace_stackable(:validations, validator_options)
447
479
  end
@@ -485,17 +517,11 @@ module Grape
485
517
  }
486
518
  end
487
519
 
488
- def validates_presence(validations, attrs, doc_attrs, opts)
520
+ def validates_presence(validations, attrs, doc, opts)
489
521
  return unless validations.key?(:presence) && validations[:presence]
490
522
 
491
- validate(:presence, validations[:presence], attrs, doc_attrs, opts)
492
- yield :presence
493
- yield :message if validations.key?(:message)
494
- end
495
-
496
- def document_attribute(attrs, doc_attrs)
497
- full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } }
498
- @api.document_attribute(full_attrs, doc_attrs)
523
+ validate(:presence, validations.delete(:presence), attrs, doc, opts)
524
+ validations.delete(:message) if validations.key?(:message)
499
525
  end
500
526
  end
501
527
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'dry_type_coercer'
4
+
3
5
  module Grape
4
6
  module Validations
5
7
  module Types
@@ -12,8 +14,6 @@ module Grape
12
14
  # behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
13
15
  # maintains Virtus behavior in coercing.
14
16
  class ArrayCoercer < DryTypeCoercer
15
- register_collection Array
16
-
17
17
  def initialize(type, strict = false)
18
18
  super
19
19
 
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'array_coercer'
4
+ require_relative 'set_coercer'
5
+ require_relative 'primitive_coercer'
6
+
7
+ module Grape
8
+ module Validations
9
+ module Types
10
+ # Chooses the best coercer for the given type. For example, if the type
11
+ # is Integer, it will return a coercer which will be able to coerce a value
12
+ # to the integer.
13
+ #
14
+ # There are a few very special coercers which might be returned.
15
+ #
16
+ # +Grape::Types::MultipleTypeCoercer+ is a coercer which is returned when
17
+ # the given type implies values in an array with different types.
18
+ # For example, +[Integer, String]+ allows integer and string values in
19
+ # an array.
20
+ #
21
+ # +Grape::Types::CustomTypeCoercer+ is a coercer which is returned when
22
+ # a method is specified by a user with +coerce_with+ option or the user
23
+ # specifies a custom type which implements requirments of
24
+ # +Grape::Types::CustomTypeCoercer+.
25
+ #
26
+ # +Grape::Types::CustomTypeCollectionCoercer+ is a very similar to the
27
+ # previous one, but it expects an array or set of values having a custom
28
+ # type implemented by the user.
29
+ #
30
+ # There is also a group of custom types implemented by Grape, check
31
+ # +Grape::Validations::Types::SPECIAL+ to get the full list.
32
+ #
33
+ # @param type [Class] the type to which input strings
34
+ # should be coerced
35
+ # @param method [Class,#call] the coercion method to use
36
+ # @return [Object] object to be used
37
+ # for coercion and type validation
38
+ def self.build_coercer(type, method: nil, strict: false)
39
+ cache_instance(type, method, strict) do
40
+ create_coercer_instance(type, method, strict)
41
+ end
42
+ end
43
+
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
+
48
+ # Use a special coercer for multiply-typed parameters.
49
+ if Types.multiple?(type)
50
+ MultipleTypeCoercer.new(type, method)
51
+
52
+ # Use a special coercer for custom types and coercion methods.
53
+ elsif method || Types.custom?(type)
54
+ CustomTypeCoercer.new(type, method)
55
+
56
+ # Special coercer for collections of types that implement a parse method.
57
+ # CustomTypeCoercer (above) already handles such types when an explicit coercion
58
+ # method is supplied.
59
+ elsif Types.collection_of_custom?(type)
60
+ Types::CustomTypeCollectionCoercer.new(
61
+ Types.map_special(type.first), type.is_a?(Set)
62
+ )
63
+ else
64
+ DryTypeCoercer.coercer_instance_for(type, strict)
65
+ end
66
+ end
67
+
68
+ def self.cache_instance(type, method, strict, &_block)
69
+ key = cache_key(type, method, strict)
70
+
71
+ return @__cache[key] if @__cache.key?(key)
72
+
73
+ instance = yield
74
+
75
+ @__cache_write_lock.synchronize do
76
+ @__cache[key] = instance
77
+ end
78
+
79
+ instance
80
+ end
81
+
82
+ def self.cache_key(type, method, strict)
83
+ [type, method, strict].each_with_object(+'_') do |val, memo|
84
+ next if val.nil?
85
+
86
+ memo << '_' << val.to_s
87
+ end
88
+ end
89
+
90
+ instance_variable_set(:@__cache, {})
91
+ instance_variable_set(:@__cache_write_lock, Mutex.new)
92
+ end
93
+ end
94
+ end
@@ -1,5 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'dry-types'
4
+
5
+ module DryTypes
6
+ # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is
7
+ # a container in this case. Check documentation for more information
8
+ # https://dry-rb.org/gems/dry-types/1.2/getting-started/
9
+ include Dry.Types()
10
+ end
11
+
3
12
  module Grape
4
13
  module Validations
5
14
  module Types
@@ -9,19 +18,15 @@ module Grape
9
18
  # https://dry-rb.org/gems/dry-types/1.2/built-in-types/
10
19
  class DryTypeCoercer
11
20
  class << self
12
- # Registers a collection coercer which could be found by a type,
13
- # see +collection_coercer_for+ method below. This method is meant for inheritors.
14
- def register_collection(type)
15
- DryTypeCoercer.collection_coercers[type] = self
16
- end
17
-
18
21
  # Returns a collection coercer which corresponds to a given type.
19
22
  # Example:
20
23
  #
21
24
  # collection_coercer_for(Array)
22
25
  # #=> Grape::Validations::Types::ArrayCoercer
23
26
  def collection_coercer_for(type)
24
- collection_coercers[type]
27
+ collection_coercers.fetch(type) do
28
+ DryTypeCoercer.collection_coercers[type] = Grape::Validations::Types.const_get("#{type.name.camelize}Coercer")
29
+ end
25
30
  end
26
31
 
27
32
  # Returns an instance of a coercer for a given type
@@ -43,7 +48,7 @@ module Grape
43
48
  def initialize(type, strict = false)
44
49
  @type = type
45
50
  @strict = strict
46
- @scope = strict ? Grape::DryTypes::Strict : Grape::DryTypes::Params
51
+ @scope = strict ? DryTypes::Strict : DryTypes::Params
47
52
  end
48
53
 
49
54
  # Coerces the given value to a type which was specified during
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Grape
4
6
  module Validations
5
7
  module Types
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'dry_type_coercer'
4
+
3
5
  module Grape
4
6
  module Validations
5
7
  module Types
@@ -8,16 +10,22 @@ module Grape
8
10
  # that it has the proper type.
9
11
  class PrimitiveCoercer < DryTypeCoercer
10
12
  MAPPING = {
11
- Grape::API::Boolean => Grape::DryTypes::Params::Bool,
12
- BigDecimal => Grape::DryTypes::Params::Decimal,
13
+ Grape::API::Boolean => DryTypes::Params::Bool,
14
+ BigDecimal => DryTypes::Params::Decimal,
15
+ Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal,
16
+ TrueClass => DryTypes::Params::Bool.constrained(eql: true),
17
+ FalseClass => DryTypes::Params::Bool.constrained(eql: false),
13
18
 
14
19
  # unfortunately, a +Params+ scope doesn't contain String
15
- String => Grape::DryTypes::Coercible::String
20
+ String => DryTypes::Coercible::String
16
21
  }.freeze
17
22
 
18
23
  STRICT_MAPPING = {
19
- Grape::API::Boolean => Grape::DryTypes::Strict::Bool,
20
- BigDecimal => Grape::DryTypes::Strict::Decimal
24
+ Grape::API::Boolean => DryTypes::Strict::Bool,
25
+ BigDecimal => DryTypes::Strict::Decimal,
26
+ Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal,
27
+ TrueClass => DryTypes::Strict::Bool.constrained(eql: true),
28
+ FalseClass => DryTypes::Strict::Bool.constrained(eql: false)
21
29
  }.freeze
22
30
 
23
31
  def initialize(type, strict = false)
@@ -25,11 +33,13 @@ module Grape
25
33
 
26
34
  @type = type
27
35
 
28
- @coercer = if strict
29
- STRICT_MAPPING.fetch(type) { scope.const_get(type.name) }
30
- else
31
- MAPPING.fetch(type) { scope.const_get(type.name) }
32
- end
36
+ @coercer = (strict ? STRICT_MAPPING : MAPPING).fetch(type) do
37
+ scope.const_get(type.name, false)
38
+ rescue NameError
39
+ raise ArgumentError, "type #{type} should support coercion via `[]`" unless type.respond_to?(:[])
40
+
41
+ type
42
+ end
33
43
  end
34
44
 
35
45
  def call(val)
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+ require_relative 'array_coercer'
5
+
3
6
  module Grape
4
7
  module Validations
5
8
  module Types
6
9
  # Takes the given array and converts it to a set. Every element of the set
7
10
  # is also coerced.
8
11
  class SetCoercer < ArrayCoercer
9
- register_collection Set
10
-
11
12
  def initialize(type, strict = false)
12
13
  super
13
14