grape 1.3.3 → 1.6.2

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 (165) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -2
  3. data/CONTRIBUTING.md +2 -1
  4. data/README.md +135 -23
  5. data/UPGRADING.md +237 -46
  6. data/grape.gemspec +5 -5
  7. data/lib/grape/api/instance.rb +34 -42
  8. data/lib/grape/api.rb +21 -16
  9. data/lib/grape/cookies.rb +2 -0
  10. data/lib/grape/dsl/callbacks.rb +1 -1
  11. data/lib/grape/dsl/desc.rb +3 -5
  12. data/lib/grape/dsl/headers.rb +5 -2
  13. data/lib/grape/dsl/helpers.rb +8 -5
  14. data/lib/grape/dsl/inside_route.rb +72 -53
  15. data/lib/grape/dsl/middleware.rb +4 -4
  16. data/lib/grape/dsl/parameters.rb +11 -7
  17. data/lib/grape/dsl/request_response.rb +9 -6
  18. data/lib/grape/dsl/routing.rb +8 -9
  19. data/lib/grape/dsl/settings.rb +5 -5
  20. data/lib/grape/dsl/validations.rb +18 -1
  21. data/lib/grape/eager_load.rb +1 -1
  22. data/lib/grape/endpoint.rb +29 -42
  23. data/lib/grape/error_formatter/json.rb +2 -6
  24. data/lib/grape/error_formatter/xml.rb +2 -6
  25. data/lib/grape/exceptions/empty_message_body.rb +11 -0
  26. data/lib/grape/exceptions/validation.rb +2 -3
  27. data/lib/grape/exceptions/validation_errors.rb +1 -1
  28. data/lib/grape/formatter/json.rb +1 -0
  29. data/lib/grape/formatter/serializable_hash.rb +2 -1
  30. data/lib/grape/formatter/xml.rb +1 -0
  31. data/lib/grape/locale/en.yml +1 -1
  32. data/lib/grape/middleware/auth/base.rb +3 -3
  33. data/lib/grape/middleware/auth/dsl.rb +7 -1
  34. data/lib/grape/middleware/base.rb +6 -3
  35. data/lib/grape/middleware/error.rb +11 -13
  36. data/lib/grape/middleware/formatter.rb +7 -7
  37. data/lib/grape/middleware/stack.rb +10 -3
  38. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -5
  39. data/lib/grape/middleware/versioner/header.rb +6 -4
  40. data/lib/grape/middleware/versioner/param.rb +1 -0
  41. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  42. data/lib/grape/middleware/versioner/path.rb +2 -0
  43. data/lib/grape/parser/json.rb +1 -1
  44. data/lib/grape/parser/xml.rb +1 -1
  45. data/lib/grape/path.rb +1 -0
  46. data/lib/grape/request.rb +4 -1
  47. data/lib/grape/router/attribute_translator.rb +3 -3
  48. data/lib/grape/router/pattern.rb +1 -1
  49. data/lib/grape/router/route.rb +2 -2
  50. data/lib/grape/router.rb +31 -30
  51. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  52. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  53. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  54. data/lib/grape/util/base_inheritable.rb +2 -2
  55. data/lib/grape/util/inheritable_setting.rb +1 -3
  56. data/lib/grape/util/lazy_value.rb +4 -2
  57. data/lib/grape/util/strict_hash_configuration.rb +1 -1
  58. data/lib/grape/validations/attributes_iterator.rb +8 -0
  59. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  60. data/lib/grape/validations/params_scope.rb +97 -62
  61. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  62. data/lib/grape/validations/types/custom_type_coercer.rb +16 -3
  63. data/lib/grape/validations/types/dry_type_coercer.rb +1 -1
  64. data/lib/grape/validations/types/invalid_value.rb +24 -0
  65. data/lib/grape/validations/types/json.rb +2 -1
  66. data/lib/grape/validations/types/primitive_coercer.rb +4 -5
  67. data/lib/grape/validations/types.rb +1 -4
  68. data/lib/grape/validations/validator_factory.rb +1 -1
  69. data/lib/grape/validations/validators/all_or_none.rb +8 -5
  70. data/lib/grape/validations/validators/allow_blank.rb +9 -7
  71. data/lib/grape/validations/validators/as.rb +6 -8
  72. data/lib/grape/validations/validators/at_least_one_of.rb +7 -4
  73. data/lib/grape/validations/validators/base.rb +74 -69
  74. data/lib/grape/validations/validators/coerce.rb +63 -76
  75. data/lib/grape/validations/validators/default.rb +36 -34
  76. data/lib/grape/validations/validators/exactly_one_of.rb +9 -6
  77. data/lib/grape/validations/validators/except_values.rb +13 -11
  78. data/lib/grape/validations/validators/multiple_params_base.rb +24 -19
  79. data/lib/grape/validations/validators/mutual_exclusion.rb +8 -5
  80. data/lib/grape/validations/validators/presence.rb +7 -4
  81. data/lib/grape/validations/validators/regexp.rb +8 -5
  82. data/lib/grape/validations/validators/same_as.rb +18 -15
  83. data/lib/grape/validations/validators/values.rb +61 -56
  84. data/lib/grape/validations.rb +6 -0
  85. data/lib/grape/version.rb +1 -1
  86. data/lib/grape.rb +7 -3
  87. data/spec/grape/api/custom_validations_spec.rb +77 -45
  88. data/spec/grape/api/deeply_included_options_spec.rb +3 -3
  89. data/spec/grape/api/defines_boolean_in_params_spec.rb +2 -1
  90. data/spec/grape/api/invalid_format_spec.rb +2 -0
  91. data/spec/grape/api/recognize_path_spec.rb +1 -1
  92. data/spec/grape/api/routes_with_requirements_spec.rb +8 -8
  93. data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +9 -15
  94. data/spec/grape/api_remount_spec.rb +25 -19
  95. data/spec/grape/api_spec.rb +576 -211
  96. data/spec/grape/dsl/callbacks_spec.rb +2 -1
  97. data/spec/grape/dsl/headers_spec.rb +39 -9
  98. data/spec/grape/dsl/helpers_spec.rb +3 -2
  99. data/spec/grape/dsl/inside_route_spec.rb +185 -34
  100. data/spec/grape/dsl/logger_spec.rb +16 -18
  101. data/spec/grape/dsl/middleware_spec.rb +2 -1
  102. data/spec/grape/dsl/parameters_spec.rb +2 -0
  103. data/spec/grape/dsl/request_response_spec.rb +1 -0
  104. data/spec/grape/dsl/routing_spec.rb +10 -7
  105. data/spec/grape/endpoint/declared_spec.rb +848 -0
  106. data/spec/grape/endpoint_spec.rb +77 -589
  107. data/spec/grape/entity_spec.rb +29 -23
  108. data/spec/grape/exceptions/body_parse_errors_spec.rb +3 -0
  109. data/spec/grape/exceptions/invalid_accept_header_spec.rb +61 -22
  110. data/spec/grape/exceptions/validation_errors_spec.rb +13 -10
  111. data/spec/grape/exceptions/validation_spec.rb +5 -3
  112. data/spec/grape/extensions/param_builders/hash_spec.rb +7 -7
  113. data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +8 -8
  114. data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +8 -8
  115. data/spec/grape/integration/rack_sendfile_spec.rb +13 -9
  116. data/spec/grape/loading_spec.rb +8 -8
  117. data/spec/grape/middleware/auth/dsl_spec.rb +15 -6
  118. data/spec/grape/middleware/auth/strategies_spec.rb +61 -21
  119. data/spec/grape/middleware/base_spec.rb +24 -15
  120. data/spec/grape/middleware/error_spec.rb +3 -3
  121. data/spec/grape/middleware/exception_spec.rb +111 -161
  122. data/spec/grape/middleware/formatter_spec.rb +28 -7
  123. data/spec/grape/middleware/globals_spec.rb +7 -4
  124. data/spec/grape/middleware/stack_spec.rb +15 -12
  125. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +2 -1
  126. data/spec/grape/middleware/versioner/header_spec.rb +14 -13
  127. data/spec/grape/middleware/versioner/param_spec.rb +7 -1
  128. data/spec/grape/middleware/versioner/path_spec.rb +5 -1
  129. data/spec/grape/middleware/versioner_spec.rb +1 -1
  130. data/spec/grape/parser_spec.rb +4 -0
  131. data/spec/grape/path_spec.rb +52 -52
  132. data/spec/grape/presenters/presenter_spec.rb +7 -6
  133. data/spec/grape/request_spec.rb +6 -4
  134. data/spec/grape/util/inheritable_setting_spec.rb +7 -7
  135. data/spec/grape/util/inheritable_values_spec.rb +3 -2
  136. data/spec/grape/util/reverse_stackable_values_spec.rb +3 -1
  137. data/spec/grape/util/stackable_values_spec.rb +7 -5
  138. data/spec/grape/validations/instance_behaivour_spec.rb +9 -10
  139. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +14 -3
  140. data/spec/grape/validations/params_scope_spec.rb +72 -10
  141. data/spec/grape/validations/single_attribute_iterator_spec.rb +18 -6
  142. data/spec/grape/validations/types/primitive_coercer_spec.rb +63 -7
  143. data/spec/grape/validations/types_spec.rb +8 -8
  144. data/spec/grape/validations/validators/all_or_none_spec.rb +50 -56
  145. data/spec/grape/validations/validators/allow_blank_spec.rb +136 -140
  146. data/spec/grape/validations/validators/at_least_one_of_spec.rb +50 -56
  147. data/spec/grape/validations/validators/coerce_spec.rb +248 -33
  148. data/spec/grape/validations/validators/default_spec.rb +121 -78
  149. data/spec/grape/validations/validators/exactly_one_of_spec.rb +71 -77
  150. data/spec/grape/validations/validators/except_values_spec.rb +4 -3
  151. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +71 -77
  152. data/spec/grape/validations/validators/presence_spec.rb +16 -1
  153. data/spec/grape/validations/validators/regexp_spec.rb +25 -31
  154. data/spec/grape/validations/validators/same_as_spec.rb +14 -20
  155. data/spec/grape/validations/validators/values_spec.rb +183 -178
  156. data/spec/grape/validations_spec.rb +342 -29
  157. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  158. data/spec/integration/multi_json/json_spec.rb +1 -1
  159. data/spec/integration/multi_xml/xml_spec.rb +1 -1
  160. data/spec/shared/versioning_examples.rb +32 -29
  161. data/spec/spec_helper.rb +12 -12
  162. data/spec/support/basic_auth_encode_helpers.rb +1 -1
  163. data/spec/support/chunks.rb +14 -0
  164. data/spec/support/versioned_helpers.rb +4 -6
  165. metadata +110 -102
data/lib/grape/request.rb CHANGED
@@ -8,13 +8,15 @@ module Grape
8
8
 
9
9
  alias rack_params params
10
10
 
11
- def initialize(env, options = {})
11
+ def initialize(env, **options)
12
12
  extend options[:build_params_with] || Grape.config.param_builder
13
13
  super(env)
14
14
  end
15
15
 
16
16
  def params
17
17
  @params ||= build_params
18
+ rescue EOFError
19
+ raise Grape::Exceptions::EmptyMessageBody.new(content_type)
18
20
  end
19
21
 
20
22
  def headers
@@ -35,6 +37,7 @@ module Grape
35
37
  Grape::Util::LazyObject.new do
36
38
  env.each_pair.with_object({}) do |(k, v), headers|
37
39
  next unless k.to_s.start_with? HTTP_PREFIX
40
+
38
41
  transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k)
39
42
  headers[transformed_header] = v
40
43
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  class Router
5
5
  # this could be an OpenStruct, but doesn't work in Ruby 2.3.0, see https://bugs.ruby-lang.org/issues/12251
6
6
  class AttributeTranslator
7
- attr_reader :attributes, :request_method, :requirements
7
+ attr_reader :attributes
8
8
 
9
9
  ROUTE_ATTRIBUTES = %i[
10
10
  prefix
@@ -23,7 +23,7 @@ module Grape
23
23
 
24
24
  ROUTER_ATTRIBUTES = %i[pattern index].freeze
25
25
 
26
- def initialize(attributes = {})
26
+ def initialize(**attributes)
27
27
  @attributes = attributes
28
28
  end
29
29
 
@@ -37,7 +37,7 @@ module Grape
37
37
  attributes
38
38
  end
39
39
 
40
- def method_missing(method_name, *args) # rubocop:disable Style/MethodMissing
40
+ def method_missing(method_name, *args)
41
41
  if setter?(method_name[-1])
42
42
  attributes[method_name[0..-1]] = *args
43
43
  else
@@ -41,7 +41,7 @@ module Grape
41
41
  end
42
42
 
43
43
  pattern = -pattern.split('/').tap do |parts|
44
- parts[parts.length - 1] = '?' + parts.last
44
+ parts[parts.length - 1] = "?#{parts.last}"
45
45
  end.join('/') if pattern.end_with?('*path')
46
46
 
47
47
  PatternCache[[pattern, suffix]]
@@ -84,8 +84,8 @@ module Grape
84
84
  path, line = *location.scan(SOURCE_LOCATION_REGEXP).first
85
85
  path = File.realpath(path) if Pathname.new(path).relative?
86
86
  expected ||= name
87
- warn <<-WARNING
88
- #{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.
87
+ warn <<~WARNING
88
+ #{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.
89
89
  WARNING
90
90
  end
91
91
  end
data/lib/grape/router.rb CHANGED
@@ -7,20 +7,12 @@ module Grape
7
7
  class Router
8
8
  attr_reader :map, :compiled
9
9
 
10
- class NormalizePathCache < Grape::Util::Cache
11
- def initialize
12
- @cache = Hash.new do |h, path|
13
- normalized_path = +"/#{path}"
14
- normalized_path.squeeze!('/')
15
- normalized_path.sub!(%r{/+\Z}, '')
16
- normalized_path = '/' if normalized_path.empty?
17
- h[path] = -normalized_path
18
- end
19
- end
20
- end
21
-
22
10
  def self.normalize_path(path)
23
- NormalizePathCache[path]
11
+ path = +"/#{path}"
12
+ path.squeeze!('/')
13
+ path.sub!(%r{/+\Z}, '')
14
+ path = '/' if path == ''
15
+ path
24
16
  end
25
17
 
26
18
  def self.supported_methods
@@ -36,6 +28,7 @@ module Grape
36
28
 
37
29
  def compile!
38
30
  return if compiled
31
+
39
32
  @union = Regexp.union(@neutral_regexes)
40
33
  @neutral_regexes = nil
41
34
  self.class.supported_methods.each do |method|
@@ -55,7 +48,7 @@ module Grape
55
48
 
56
49
  def associate_routes(pattern, **options)
57
50
  @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}")
58
- @neutral_map << Grape::Router::AttributeTranslator.new(options.merge(pattern: pattern, index: @neutral_map.length))
51
+ @neutral_map << Grape::Router::AttributeTranslator.new(**options, pattern: pattern, index: @neutral_map.length)
59
52
  end
60
53
 
61
54
  def call(env)
@@ -68,6 +61,7 @@ module Grape
68
61
  def recognize_path(input)
69
62
  any = with_optimization { greedy_match?(input) }
70
63
  return if any == default_response
64
+
71
65
  any.endpoint
72
66
  end
73
67
 
@@ -88,6 +82,7 @@ module Grape
88
82
  map[method].each do |route|
89
83
  next if exact_route == route
90
84
  next unless route.match?(input)
85
+
91
86
  response = process_route(route, env)
92
87
  break unless cascade?(response)
93
88
  end
@@ -99,37 +94,35 @@ module Grape
99
94
  response = yield(input, method)
100
95
 
101
96
  return response if response && !(cascade = cascade?(response))
102
- neighbor = greedy_match?(input)
103
97
 
104
- # If neighbor exists and request method is OPTIONS,
98
+ last_neighbor_route = greedy_match?(input)
99
+
100
+ # If last_neighbor_route exists and request method is OPTIONS,
105
101
  # return response by using #call_with_allow_headers.
106
- return call_with_allow_headers(
107
- env,
108
- neighbor.allow_header,
109
- neighbor.endpoint
110
- ) if neighbor && method == Grape::Http::Headers::OPTIONS && !cascade
102
+ return call_with_allow_headers(env, last_neighbor_route) if last_neighbor_route && method == Grape::Http::Headers::OPTIONS && !cascade
111
103
 
112
104
  route = match?(input, '*')
113
- return neighbor.endpoint.call(env) if neighbor && cascade && route
105
+
106
+ return last_neighbor_route.endpoint.call(env) if last_neighbor_route && cascade && route
114
107
 
115
108
  if route
116
109
  response = process_route(route, env)
117
110
  return response if response && !(cascade = cascade?(response))
118
111
  end
119
112
 
120
- !cascade && neighbor ? call_with_allow_headers(env, neighbor.allow_header, neighbor.endpoint) : nil
113
+ return call_with_allow_headers(env, last_neighbor_route) if !cascade && last_neighbor_route
114
+
115
+ nil
121
116
  end
122
117
 
123
118
  def process_route(route, env)
124
- input, = *extract_input_and_method(env)
125
- routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
126
- env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
119
+ prepare_env_from_route(env, route)
127
120
  route.exec(env)
128
121
  end
129
122
 
130
123
  def make_routing_args(default_args, route, input)
131
124
  args = default_args || { route_info: route }
132
- args.merge(route.params(input))
125
+ args.merge(route.params(input) || {})
133
126
  end
134
127
 
135
128
  def extract_input_and_method(env)
@@ -150,19 +143,27 @@ module Grape
150
143
  def match?(input, method)
151
144
  current_regexp = @optimized_map[method]
152
145
  return unless current_regexp.match(input)
146
+
153
147
  last_match = Regexp.last_match
154
148
  @map[method].detect { |route| last_match["_#{route.index}"] }
155
149
  end
156
150
 
157
151
  def greedy_match?(input)
158
152
  return unless @union.match(input)
153
+
159
154
  last_match = Regexp.last_match
160
155
  @neutral_map.detect { |route| last_match["_#{route.index}"] }
161
156
  end
162
157
 
163
- def call_with_allow_headers(env, methods, endpoint)
164
- env[Grape::Env::GRAPE_ALLOWED_METHODS] = methods.join(', ').freeze
165
- endpoint.call(env)
158
+ def call_with_allow_headers(env, route)
159
+ prepare_env_from_route(env, route)
160
+ env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header.join(', ').freeze
161
+ route.endpoint.call(env)
162
+ end
163
+
164
+ def prepare_env_from_route(env, route)
165
+ input, = *extract_input_and_method(env)
166
+ env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(env[Grape::Env::GRAPE_ROUTING_ARGS], route, input)
166
167
  end
167
168
 
168
169
  def cascade?(response)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grape
4
- module ServeFile
4
+ module ServeStream
5
5
  CHUNK_SIZE = 16_384
6
6
 
7
7
  # Class helps send file through API
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grape
4
- module ServeFile
4
+ module ServeStream
5
5
  # Response should respond to to_path method
6
6
  # for using Rack::SendFile middleware
7
7
  class SendfileResponse < Rack::Response
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grape
4
- module ServeFile
5
- # A simple class used to identify responses which represent files and do not
4
+ module ServeStream
5
+ # A simple class used to identify responses which represent streams (or files) and do not
6
6
  # need to be formatted or pre-read by Rack::Response
7
- class FileResponse
8
- attr_reader :file
7
+ class StreamResponse
8
+ attr_reader :stream
9
9
 
10
- # @param file [Object]
11
- def initialize(file)
12
- @file = file
10
+ # @param stream [Object]
11
+ def initialize(stream)
12
+ @stream = stream
13
13
  end
14
14
 
15
15
  # Equality provided mostly for tests.
16
16
  #
17
17
  # @return [Boolean]
18
18
  def ==(other)
19
- file == other.file
19
+ stream == other.stream
20
20
  end
21
21
  end
22
22
  end
@@ -9,8 +9,8 @@ module Grape
9
9
 
10
10
  # @param inherited_values [Object] An object implementing an interface
11
11
  # of the Hash class.
12
- def initialize(inherited_values = {})
13
- @inherited_values = inherited_values
12
+ def initialize(inherited_values = nil)
13
+ @inherited_values = inherited_values || {}
14
14
  @new_values = {}
15
15
  end
16
16
 
@@ -5,9 +5,7 @@ module Grape
5
5
  # A branchable, inheritable settings object which can store both stackable
6
6
  # and inheritable values (see InheritableValues and StackableValues).
7
7
  class InheritableSetting
8
- attr_accessor :route, :api_class, :namespace
9
- attr_accessor :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable
10
- attr_accessor :parent, :point_in_time_copies
8
+ attr_accessor :route, :api_class, :namespace, :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable, :parent, :point_in_time_copies
11
9
 
12
10
  # Retrieve global settings.
13
11
  def self.global
@@ -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
@@ -48,9 +49,10 @@ module Grape
48
49
  end
49
50
 
50
51
  def []=(key, value)
51
- @value_hash[key] = if value.is_a?(Hash)
52
+ @value_hash[key] = case value
53
+ when Hash
52
54
  LazyValueHash.new(value)
53
- elsif value.is_a?(Array)
55
+ when Array
54
56
  LazyValueArray.new(value)
55
57
  else
56
58
  LazyValue.new(value)
@@ -65,7 +65,7 @@ module Grape
65
65
  end
66
66
  end
67
67
 
68
- define_method 'to_hash' do
68
+ define_method :to_hash do
69
69
  merge_hash = {}
70
70
  setting_name.each_key { |k| merge_hash[k] = send("#{k}_context").to_hash }
71
71
 
@@ -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
@@ -13,6 +13,8 @@ module Grape
13
13
  # @param opts [Hash] options for this scope
14
14
  # @option opts :element [Symbol] the element that contains this scope; for
15
15
  # this to be relevant, @parent must be set
16
+ # @option opts :element_renamed [Symbol, nil] whenever this scope should
17
+ # be renamed and to what, given +nil+ no renaming is done
16
18
  # @option opts :parent [ParamsScope] the scope containing this scope
17
19
  # @option opts :api [API] the API endpoint to modify
18
20
  # @option opts :optional [Boolean] whether or not this scope needs to have
@@ -23,23 +25,24 @@ module Grape
23
25
  # validate if this param is present in the parent scope
24
26
  # @yield the instance context, open for parameter definitions
25
27
  def initialize(opts, &block)
26
- @element = opts[:element]
27
- @parent = opts[:parent]
28
- @api = opts[:api]
29
- @optional = opts[:optional] || false
30
- @type = opts[:type]
31
- @group = opts[:group] || {}
32
- @dependent_on = opts[:dependent_on]
28
+ @element = opts[:element]
29
+ @element_renamed = opts[:element_renamed]
30
+ @parent = opts[:parent]
31
+ @api = opts[:api]
32
+ @optional = opts[:optional] || false
33
+ @type = opts[:type]
34
+ @group = opts[:group] || {}
35
+ @dependent_on = opts[:dependent_on]
33
36
  @declared_params = []
34
37
  @index = nil
35
38
 
36
- instance_eval(&block) if block_given?
39
+ instance_eval(&block) if block
37
40
 
38
41
  configure_declared_params
39
42
  end
40
43
 
41
44
  def configuration
42
- @api.configuration.evaluate
45
+ @api.configuration.respond_to?(:evaluate) ? @api.configuration.evaluate : @api.configuration
43
46
  end
44
47
 
45
48
  # @return [Boolean] whether or not this entire scope needs to be
@@ -50,18 +53,19 @@ module Grape
50
53
  return false if @optional && (scoped_params.blank? || all_element_blank?(scoped_params))
51
54
  return false unless meets_dependency?(scoped_params, parameters)
52
55
  return true if parent.nil?
56
+
53
57
  parent.should_validate?(parameters)
54
58
  end
55
59
 
56
60
  def meets_dependency?(params, request_params)
57
- if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
58
- return false
59
- end
60
-
61
61
  return true unless @dependent_on
62
+
63
+ return false if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
64
+
62
65
  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
66
+
67
+ # params might be anything what looks like a hash, so it must implement a `key?` method
68
+ return false unless params.respond_to?(:key?)
65
69
 
66
70
  @dependent_on.each do |dependency|
67
71
  if dependency.is_a?(Hash)
@@ -127,18 +131,35 @@ module Grape
127
131
  if lateral?
128
132
  @parent.push_declared_params(attrs, **opts)
129
133
  else
130
- if opts && opts[:as]
131
- @api.route_setting(:renamed_params, @api.route_setting(:renamed_params) || [])
132
- @api.route_setting(:renamed_params) << { attrs.first => opts[:as] }
133
- attrs = [opts[:as]]
134
- end
134
+ push_renamed_param(full_path + [attrs.first], opts[:as]) \
135
+ if opts && opts[:as]
135
136
 
136
137
  @declared_params.concat attrs
137
138
  end
138
139
  end
139
140
 
141
+ # Get the full path of the parameter scope in the hierarchy.
142
+ #
143
+ # @return [Array<Symbol>] the nesting/path of the current parameter scope
144
+ def full_path
145
+ nested? ? @parent.full_path + [@element] : []
146
+ end
147
+
140
148
  private
141
149
 
150
+ # Add a new parameter which should be renamed when using the +#declared+
151
+ # method.
152
+ #
153
+ # @param path [Array<String, Symbol>] the full path of the parameter
154
+ # (including the parameter name as last array element)
155
+ # @param new_name [String, Symbol] the new name of the parameter (the
156
+ # renamed name, with the +as: ...+ semantic)
157
+ def push_renamed_param(path, new_name)
158
+ base = @api.route_setting(:renamed_params) || {}
159
+ base[Array(path).map(&:to_s)] = new_name.to_s
160
+ @api.route_setting(:renamed_params, base)
161
+ end
162
+
142
163
  def require_required_and_optional_fields(context, opts)
143
164
  if context == :all
144
165
  optional_fields = Array(opts[:except])
@@ -150,6 +171,7 @@ module Grape
150
171
  required_fields.each do |field|
151
172
  field_opts = opts[:using][field]
152
173
  raise ArgumentError, "required field not exist: #{field}" unless field_opts
174
+
153
175
  requires(field, field_opts)
154
176
  end
155
177
  optional_fields.each do |field|
@@ -189,11 +211,12 @@ module Grape
189
211
  end
190
212
 
191
213
  self.class.new(
192
- api: @api,
193
- element: attrs[1][:as] || attrs.first,
194
- parent: self,
214
+ api: @api,
215
+ element: attrs.first,
216
+ element_renamed: attrs[1][:as],
217
+ parent: self,
195
218
  optional: optional,
196
- type: type || Array,
219
+ type: type || Array,
197
220
  &block
198
221
  )
199
222
  end
@@ -207,11 +230,11 @@ module Grape
207
230
  # @yield parameter scope
208
231
  def new_lateral_scope(options, &block)
209
232
  self.class.new(
210
- api: @api,
211
- element: nil,
212
- parent: self,
213
- options: @optional,
214
- type: type == Array ? Array : Hash,
233
+ api: @api,
234
+ element: nil,
235
+ parent: self,
236
+ options: @optional,
237
+ type: type == Array ? Array : Hash,
215
238
  dependent_on: options[:dependent_on],
216
239
  &block
217
240
  )
@@ -224,23 +247,25 @@ module Grape
224
247
  # @yield parameter scope
225
248
  def new_group_scope(attrs, &block)
226
249
  self.class.new(
227
- api: @api,
228
- parent: self,
229
- group: attrs.first,
250
+ api: @api,
251
+ parent: self,
252
+ group: attrs.first,
230
253
  &block
231
254
  )
232
255
  end
233
256
 
234
257
  # Pushes declared params to parent or settings
235
258
  def configure_declared_params
259
+ push_renamed_param(full_path, @element_renamed) if @element_renamed
260
+
236
261
  if nested?
237
262
  @parent.push_declared_params [element => @declared_params]
238
263
  else
239
264
  @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
265
  end
266
+
267
+ # params were stored in settings, it can be cleaned from the params scope
268
+ @declared_params = nil
244
269
  end
245
270
 
246
271
  def validates(attrs, validations)
@@ -281,16 +306,15 @@ module Grape
281
306
 
282
307
  doc_attrs[:documentation] = validations.delete(:documentation) if validations.key?(:documentation)
283
308
 
284
- full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } }
285
- @api.document_attribute(full_attrs, doc_attrs)
309
+ document_attribute(attrs, doc_attrs)
286
310
 
287
311
  opts = derive_validator_options(validations)
288
312
 
313
+ order_specific_validations = Set[:as]
314
+
289
315
  # Validate for presence before any other validators
290
- if validations.key?(:presence) && validations[:presence]
291
- validate('presence', validations[:presence], attrs, doc_attrs, opts)
292
- validations.delete(:presence)
293
- validations.delete(:message) if validations.key?(:message)
316
+ validates_presence(validations, attrs, doc_attrs, opts) do |validation_type|
317
+ order_specific_validations << validation_type
294
318
  end
295
319
 
296
320
  # Before we run the rest of the validators, let's handle
@@ -299,6 +323,8 @@ module Grape
299
323
  coerce_type validations, attrs, doc_attrs, opts
300
324
 
301
325
  validations.each do |type, options|
326
+ next if order_specific_validations.include?(type)
327
+
302
328
  validate(type, options, attrs, doc_attrs, opts)
303
329
  end
304
330
  end
@@ -317,9 +343,7 @@ module Grape
317
343
  # @return [class-like] type to which the parameter will be coerced
318
344
  # @raise [ArgumentError] if the given type options are invalid
319
345
  def infer_coercion(validations)
320
- if validations.key?(:type) && validations.key?(:types)
321
- raise ArgumentError, ':type may not be supplied with :types'
322
- end
346
+ raise ArgumentError, ':type may not be supplied with :types' if validations.key?(:type) && validations.key?(:types)
323
347
 
324
348
  validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type)
325
349
  validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type)
@@ -355,6 +379,7 @@ module Grape
355
379
  # but not special JSON types, which
356
380
  # already imply coercion method
357
381
  return unless [JSON, Array[JSON]].include? validations[:coerce]
382
+
358
383
  raise ArgumentError, 'coerce_with disallowed for type: JSON'
359
384
  end
360
385
 
@@ -382,6 +407,7 @@ module Grape
382
407
 
383
408
  def guess_coerce_type(coerce_type, *values_list)
384
409
  return coerce_type unless coerce_type == Array
410
+
385
411
  values_list.each do |values|
386
412
  next if !values || values.is_a?(Proc)
387
413
  return values.first.class if values.is_a?(Range) || !values.empty?
@@ -392,14 +418,11 @@ module Grape
392
418
  def check_incompatible_option_values(default, values, except_values, excepts)
393
419
  return unless default && !default.is_a?(Proc)
394
420
 
395
- if values && !values.is_a?(Proc)
396
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) \
397
- unless Array(default).all? { |def_val| values.include?(def_val) }
398
- end
421
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && !Array(default).all? { |def_val| values.include?(def_val) }
399
422
 
400
- if except_values && !except_values.is_a?(Proc)
423
+ if except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
401
424
  raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) \
402
- unless Array(default).none? { |def_val| except_values.include?(def_val) }
425
+
403
426
  end
404
427
 
405
428
  return unless excepts && !excepts.is_a?(Proc)
@@ -413,11 +436,11 @@ module Grape
413
436
  raise Grape::Exceptions::UnknownValidator.new(type) unless validator_class
414
437
 
415
438
  validator_options = {
416
- attributes: attrs,
417
- options: options,
418
- required: doc_attrs[:required],
419
- params_scope: self,
420
- opts: opts,
439
+ attributes: attrs,
440
+ options: options,
441
+ required: doc_attrs[:required],
442
+ params_scope: self,
443
+ opts: opts,
421
444
  validator_class: validator_class
422
445
  }
423
446
  @api.namespace_stackable(:validations, validator_options)
@@ -425,21 +448,20 @@ module Grape
425
448
 
426
449
  def validate_value_coercion(coerce_type, *values_list)
427
450
  return unless coerce_type
451
+
428
452
  coerce_type = coerce_type.first if coerce_type.is_a?(Array)
429
453
  values_list.each do |values|
430
454
  next if !values || values.is_a?(Proc)
455
+
431
456
  value_types = values.is_a?(Range) ? [values.begin, values.end] : values
432
- if coerce_type == Grape::API::Boolean
433
- value_types = value_types.map { |type| Grape::API::Boolean.build(type) }
434
- end
435
- unless value_types.all? { |v| v.is_a? coerce_type }
436
- raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
437
- end
457
+ value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean
458
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type)
438
459
  end
439
460
  end
440
461
 
441
462
  def extract_message_option(attrs)
442
463
  return nil unless attrs.is_a?(Array)
464
+
443
465
  opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
444
466
  opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
445
467
  end
@@ -459,9 +481,22 @@ module Grape
459
481
 
460
482
  {
461
483
  allow_blank: allow_blank.is_a?(Hash) ? allow_blank[:value] : allow_blank,
462
- fail_fast: validations.delete(:fail_fast) || false
484
+ fail_fast: validations.delete(:fail_fast) || false
463
485
  }
464
486
  end
487
+
488
+ def validates_presence(validations, attrs, doc_attrs, opts)
489
+ return unless validations.key?(:presence) && validations[:presence]
490
+
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)
499
+ end
465
500
  end
466
501
  end
467
502
  end