grape 1.6.1 → 1.7.0

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 (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