grape 1.3.3 → 1.6.2

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