grape 0.18.0 → 0.19.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.

Potentially problematic release.


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

Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -69
  3. data/Gemfile +3 -3
  4. data/Gemfile.lock +37 -41
  5. data/README.md +49 -7
  6. data/UPGRADING.md +56 -1
  7. data/gemfiles/rack_edge.gemfile +34 -0
  8. data/gemfiles/rails_5.gemfile +1 -1
  9. data/gemfiles/rails_edge.gemfile +34 -0
  10. data/lib/grape/api.rb +1 -1
  11. data/lib/grape/dsl/helpers.rb +1 -0
  12. data/lib/grape/dsl/inside_route.rb +2 -0
  13. data/lib/grape/dsl/parameters.rb +24 -12
  14. data/lib/grape/dsl/request_response.rb +2 -3
  15. data/lib/grape/dsl/routing.rb +4 -0
  16. data/lib/grape/endpoint.rb +15 -16
  17. data/lib/grape/error_formatter/base.rb +1 -0
  18. data/lib/grape/formatter/serializable_hash.rb +2 -2
  19. data/lib/grape/http/headers.rb +2 -0
  20. data/lib/grape/middleware/base.rb +3 -4
  21. data/lib/grape/middleware/error.rb +1 -1
  22. data/lib/grape/middleware/versioner/path.rb +1 -1
  23. data/lib/grape/router.rb +37 -21
  24. data/lib/grape/router/attribute_translator.rb +1 -1
  25. data/lib/grape/router/pattern.rb +9 -1
  26. data/lib/grape/router/route.rb +4 -0
  27. data/lib/grape/validations/params_scope.rb +24 -6
  28. data/lib/grape/validations/validators/base.rb +1 -2
  29. data/lib/grape/validations/validators/regexp.rb +2 -1
  30. data/lib/grape/version.rb +1 -1
  31. data/pkg/grape-0.17.0.gem +0 -0
  32. data/spec/grape/api/patch_method_helpers_spec.rb +1 -2
  33. data/spec/grape/api_spec.rb +87 -21
  34. data/spec/grape/dsl/desc_spec.rb +2 -4
  35. data/spec/grape/dsl/inside_route_spec.rb +29 -22
  36. data/spec/grape/dsl/parameters_spec.rb +15 -1
  37. data/spec/grape/endpoint_spec.rb +53 -19
  38. data/spec/grape/middleware/formatter_spec.rb +39 -30
  39. data/spec/grape/middleware/versioner/param_spec.rb +15 -10
  40. data/spec/grape/middleware/versioner/path_spec.rb +4 -3
  41. data/spec/grape/util/inheritable_setting_spec.rb +2 -1
  42. data/spec/grape/util/strict_hash_configuration_spec.rb +1 -2
  43. data/spec/grape/validations/params_scope_spec.rb +182 -0
  44. data/spec/grape/validations/validators/default_spec.rb +1 -3
  45. data/spec/grape/validations/validators/presence_spec.rb +29 -1
  46. data/spec/grape/validations/validators/regexp_spec.rb +88 -0
  47. metadata +5 -3
  48. data/pkg/grape-0.18.0.gem +0 -0
@@ -15,6 +15,7 @@ module Grape
15
15
  # env['api.endpoint'].route does not work when the error occurs within a middleware
16
16
  # the Endpoint does not have a valid env at this moment
17
17
  http_codes = env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info].http_codes || []
18
+
18
19
  found_code = http_codes.find do |http_code|
19
20
  (http_code[0].to_i == env[Grape::Env::API_ENDPOINT].status) && http_code[2].respond_to?(:represent)
20
21
  end if env[Grape::Env::API_ENDPOINT].request
@@ -12,13 +12,13 @@ module Grape
12
12
  private
13
13
 
14
14
  def serializable?(object)
15
- object.respond_to?(:serializable_hash) || object.is_a?(Array) && !object.map { |o| o.respond_to? :serializable_hash }.include?(false) || object.is_a?(Hash)
15
+ object.respond_to?(:serializable_hash) || object.is_a?(Array) && object.all? { |o| o.respond_to? :serializable_hash } || object.is_a?(Hash)
16
16
  end
17
17
 
18
18
  def serialize(object)
19
19
  if object.respond_to? :serializable_hash
20
20
  object.serializable_hash
21
- elsif object.is_a?(Array) && !object.map { |o| o.respond_to? :serializable_hash }.include?(false)
21
+ elsif object.is_a?(Array) && object.all? { |o| o.respond_to? :serializable_hash }
22
22
  object.map(&:serializable_hash)
23
23
  elsif object.is_a?(Hash)
24
24
  h = {}
@@ -16,6 +16,8 @@ module Grape
16
16
  HEAD = 'HEAD'.freeze
17
17
  OPTIONS = 'OPTIONS'.freeze
18
18
 
19
+ SUPPORTED_METHODS = [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS].freeze
20
+
19
21
  HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'.freeze
20
22
  X_CASCADE = 'X-Cascade'.freeze
21
23
  HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'.freeze
@@ -13,6 +13,7 @@ module Grape
13
13
  def initialize(app, **options)
14
14
  @app = app
15
15
  @options = default_options.merge(**options)
16
+ @app_response = nil
16
17
  end
17
18
 
18
19
  def default_options
@@ -44,14 +45,12 @@ module Grape
44
45
 
45
46
  # @abstract
46
47
  # Called before the application is called in the middleware lifecycle.
47
- def before
48
- end
48
+ def before; end
49
49
 
50
50
  # @abstract
51
51
  # Called after the application is called in the middleware lifecycle.
52
52
  # @return [Response, nil] a Rack SPEC response or nil to call the application afterwards.
53
- def after
54
- end
53
+ def after; end
55
54
 
56
55
  def response
57
56
  return @app_response if @app_response.is_a?(Rack::Response)
@@ -70,7 +70,7 @@ module Grape
70
70
  end
71
71
 
72
72
  def exec_handler(e, &handler)
73
- if handler.lambda? && handler.arity == 0
73
+ if handler.lambda? && handler.arity.zero?
74
74
  instance_exec(&handler)
75
75
  else
76
76
  instance_exec(e, &handler)
@@ -26,7 +26,7 @@ module Grape
26
26
  def before
27
27
  path = env[Grape::Http::Headers::PATH_INFO].dup
28
28
 
29
- if prefix && path.index(prefix) == 0
29
+ if prefix && path.index(prefix).zero?
30
30
  path.sub!(prefix, '')
31
31
  path = Grape::Router.normalize_path(path)
32
32
  end
data/lib/grape/router.rb CHANGED
@@ -19,6 +19,10 @@ module Grape
19
19
  path
20
20
  end
21
21
 
22
+ def self.supported_methods
23
+ @supported_methods ||= Grape::Http::Headers::SUPPORTED_METHODS + ['*']
24
+ end
25
+
22
26
  def initialize
23
27
  @neutral_map = []
24
28
  @map = Hash.new { |hash, key| hash[key] = [] }
@@ -28,7 +32,8 @@ module Grape
28
32
  def compile!
29
33
  return if compiled
30
34
  @union = Regexp.union(@neutral_map.map(&:regexp))
31
- map.each do |method, routes|
35
+ self.class.supported_methods.each do |method|
36
+ routes = map[method]
32
37
  @optimized_map[method] = routes.map.with_index do |route, index|
33
38
  route.index = index
34
39
  route.regexp = /(?<_#{index}>#{route.pattern.to_regexp})/
@@ -64,38 +69,54 @@ module Grape
64
69
 
65
70
  def identity(env)
66
71
  route = nil
67
- response = transaction(env) do |input, method, routing_args|
72
+ response = transaction(env) do |input, method|
68
73
  route = match?(input, method)
69
- if route
70
- env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
71
- route.exec(env)
72
- end
74
+ process_route(route, env) if route
73
75
  end
74
76
  [response, route]
75
77
  end
76
78
 
77
79
  def rotation(env, exact_route = nil)
78
80
  response = nil
79
- input, method, routing_args = *extract_required_args(env)
80
- routes_for(method).each do |route|
81
+ input, method = *extract_input_and_method(env)
82
+ map[method].each do |route|
81
83
  next if exact_route == route
82
84
  next unless route.match?(input)
83
- env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
84
- response = route.exec(env)
85
+ response = process_route(route, env)
85
86
  break unless cascade?(response)
86
87
  end
87
88
  response
88
89
  end
89
90
 
90
91
  def transaction(env)
91
- input, method, routing_args = *extract_required_args(env)
92
- response = yield(input, method, routing_args)
92
+ input, method = *extract_input_and_method(env)
93
+ response = yield(input, method)
93
94
 
94
95
  return response if response && !(cascade = cascade?(response))
95
96
  neighbor = greedy_match?(input)
96
- return unless neighbor
97
97
 
98
- (!cascade && neighbor) ? call_with_allow_headers(env, neighbor.allow_header, neighbor.endpoint) : nil
98
+ # If neighbor exists and request method is OPTIONS,
99
+ # return response by using #call_with_allow_headers.
100
+ return call_with_allow_headers(
101
+ env,
102
+ neighbor.allow_header,
103
+ neighbor.endpoint
104
+ ) if neighbor && method == 'OPTIONS' && !cascade
105
+
106
+ route = match?(input, '*')
107
+ if route
108
+ response = process_route(route, env)
109
+ return response if response && !(cascade = cascade?(response))
110
+ end
111
+
112
+ !cascade && neighbor ? call_with_allow_headers(env, neighbor.allow_header, neighbor.endpoint) : nil
113
+ end
114
+
115
+ def process_route(route, env)
116
+ input, = *extract_input_and_method(env)
117
+ routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
118
+ env[Grape::Env::GRAPE_ROUTING_ARGS] = make_routing_args(routing_args, route, input)
119
+ route.exec(env)
99
120
  end
100
121
 
101
122
  def make_routing_args(default_args, route, input)
@@ -103,11 +124,10 @@ module Grape
103
124
  args.merge(route.params(input))
104
125
  end
105
126
 
106
- def extract_required_args(env)
127
+ def extract_input_and_method(env)
107
128
  input = string_for(env[Grape::Http::Headers::PATH_INFO])
108
129
  method = env[Grape::Http::Headers::REQUEST_METHOD]
109
- routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
110
- [input, method, routing_args]
130
+ [input, method]
111
131
  end
112
132
 
113
133
  def with_optimization
@@ -141,10 +161,6 @@ module Grape
141
161
  response && response[1][Grape::Http::Headers::X_CASCADE] == 'pass'
142
162
  end
143
163
 
144
- def routes_for(method)
145
- map[method] + map['ANY']
146
- end
147
-
148
164
  def string_for(input)
149
165
  self.class.normalize_path(input)
150
166
  end
@@ -13,7 +13,7 @@ module Grape
13
13
  def method_missing(m, *args)
14
14
  if m[-1] == '='
15
15
  @attributes[m[0..-1]] = *args
16
- else
16
+ elsif m[-1] != '='
17
17
  @attributes[m]
18
18
  end
19
19
  end
@@ -35,7 +35,15 @@ module Grape
35
35
  end
36
36
 
37
37
  def build_path(pattern, anchor: false, suffix: nil, **_options)
38
- pattern << '*path' unless anchor || pattern.end_with?('*path')
38
+ unless anchor || pattern.end_with?('*path')
39
+ pattern << '/' unless pattern.end_with?('/')
40
+ pattern << '*path'
41
+ end
42
+
43
+ pattern = pattern.split('/').tap do |parts|
44
+ parts[parts.length - 1] = '?' + parts.last
45
+ end.join('/') if pattern.end_with?('*path')
46
+
39
47
  pattern + suffix.to_s
40
48
  end
41
49
 
@@ -28,6 +28,10 @@ module Grape
28
28
  end
29
29
  end
30
30
 
31
+ def respond_to_missing?(method_id, _)
32
+ ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s)
33
+ end
34
+
31
35
  [
32
36
  :prefix,
33
37
  :version,
@@ -16,6 +16,7 @@ module Grape
16
16
  # @option opts :optional [Boolean] whether or not this scope needs to have
17
17
  # any parameters set or not
18
18
  # @option opts :type [Class] a type meant to govern this scope (deprecated)
19
+ # @option opts :type [Hash] group options for this scope
19
20
  # @option opts :dependent_on [Symbol] if present, this scope should only
20
21
  # validate if this param is present in the parent scope
21
22
  # @yield the instance context, open for parameter definitions
@@ -25,8 +26,10 @@ module Grape
25
26
  @api = opts[:api]
26
27
  @optional = opts[:optional] || false
27
28
  @type = opts[:type]
29
+ @group = opts[:group] || {}
28
30
  @dependent_on = opts[:dependent_on]
29
31
  @declared_params = []
32
+ @index = nil
30
33
 
31
34
  instance_eval(&block) if block_given?
32
35
 
@@ -55,11 +58,10 @@ module Grape
55
58
 
56
59
  # @return [String] the proper attribute name, with nesting considered.
57
60
  def full_name(name)
58
- case
59
- when nested?
61
+ if nested?
60
62
  # Find our containing element's name, and append ours.
61
63
  "#{@parent.full_name(@element)}#{array_index}[#{name}]"
62
- when lateral?
64
+ elsif lateral?
63
65
  # Find the name of the element as if it was at the
64
66
  # same nesting level as our parent.
65
67
  @parent.full_name(name)
@@ -168,7 +170,8 @@ module Grape
168
170
  parent: self,
169
171
  optional: optional,
170
172
  type: opts[:type],
171
- &block)
173
+ &block
174
+ )
172
175
  end
173
176
 
174
177
  # Returns a new parameter scope, not nested under any current-level param
@@ -186,7 +189,22 @@ module Grape
186
189
  options: @optional,
187
190
  type: Hash,
188
191
  dependent_on: options[:dependent_on],
189
- &block)
192
+ &block
193
+ )
194
+ end
195
+
196
+ # Returns a new parameter scope, subordinate to the current one and nested
197
+ # under the parameter corresponding to `attrs.first`.
198
+ # @param attrs [Array] the attributes passed to the `requires` or
199
+ # `optional` invocation that opened this scope.
200
+ # @yield parameter scope
201
+ def new_group_scope(attrs, &block)
202
+ self.class.new(
203
+ api: @api,
204
+ parent: self,
205
+ group: attrs.first,
206
+ &block
207
+ )
190
208
  end
191
209
 
192
210
  # Pushes declared params to parent or settings
@@ -369,7 +387,7 @@ module Grape
369
387
  def extract_message_option(attrs)
370
388
  return nil unless attrs.is_a?(Array)
371
389
  opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
372
- (opts.key?(:message) && !opts[:message].nil?) ? opts.delete(:message) : nil
390
+ opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
373
391
  end
374
392
 
375
393
  def options_key?(type, key, validations)
@@ -39,13 +39,12 @@ module Grape
39
39
  def validate!(params)
40
40
  attributes = AttributesIterator.new(self, @scope, params)
41
41
  array_errors = []
42
- attributes.each do |resource_params, attr_name, inside_array|
42
+ attributes.each do |resource_params, attr_name|
43
43
  next unless @required || (resource_params.respond_to?(:key?) && resource_params.key?(attr_name))
44
44
 
45
45
  begin
46
46
  validate_param!(attr_name, resource_params)
47
47
  rescue Grape::Exceptions::Validation => e
48
- raise e unless inside_array
49
48
  # we collect errors inside array because
50
49
  # there may be more than one error per field
51
50
  array_errors << e
@@ -2,7 +2,8 @@ module Grape
2
2
  module Validations
3
3
  class RegexpValidator < Base
4
4
  def validate_param!(attr_name, params)
5
- return unless params.key?(attr_name) && !params[attr_name].nil? && !(params[attr_name].to_s =~ (options_key?(:value) ? @option[:value] : @option))
5
+ return unless params.respond_to?(:key?) && params.key?(attr_name)
6
+ return if Array.wrap(params[attr_name]).all? { |param| param.nil? || (param.to_s =~ (options_key?(:value) ? @option[:value] : @option)) }
6
7
  raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:regexp)
7
8
  end
8
9
  end
data/lib/grape/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Grape
2
2
  # The current version of Grape.
3
- VERSION = '0.18.0'.freeze
3
+ VERSION = '0.19.0'.freeze
4
4
  end
Binary file
@@ -12,8 +12,7 @@ describe Grape::API::Helpers do
12
12
  end
13
13
 
14
14
  module AuthMethods
15
- def authenticate!
16
- end
15
+ def authenticate!; end
17
16
  end
18
17
 
19
18
  class PatchPrivate < Grape::API
@@ -508,6 +508,37 @@ describe Grape::API do
508
508
  end
509
509
  end
510
510
 
511
+ it 'allows for catch-all in a namespace' do
512
+ subject.namespace :nested do
513
+ get do
514
+ 'root'
515
+ end
516
+
517
+ get 'something' do
518
+ 'something'
519
+ end
520
+
521
+ route :any, '*path' do
522
+ 'catch-all'
523
+ end
524
+ end
525
+
526
+ get 'nested'
527
+ expect(last_response.body).to eql 'root'
528
+
529
+ get 'nested/something'
530
+ expect(last_response.body).to eql 'something'
531
+
532
+ get 'nested/missing'
533
+ expect(last_response.body).to eql 'catch-all'
534
+
535
+ post 'nested'
536
+ expect(last_response.body).to eql 'catch-all'
537
+
538
+ post 'nested/something'
539
+ expect(last_response.body).to eql 'catch-all'
540
+ end
541
+
511
542
  verbs = %w(post get head delete put options patch)
512
543
  verbs.each do |verb|
513
544
  it "allows and properly constrain a #{verb.upcase} method" do
@@ -516,9 +547,15 @@ describe Grape::API do
516
547
  end
517
548
  send(verb, '/example')
518
549
  expect(last_response.body).to eql verb == 'head' ? '' : verb
519
- # Call it with a method other than the properly constrained one.
520
- send(used_verb = verbs[(verbs.index(verb) + 2) % verbs.size], '/example')
521
- expect(last_response.status).to eql used_verb == 'options' ? 204 : 405
550
+ # Call it with all methods other than the properly constrained one.
551
+ (verbs - [verb]).each do |other_verb|
552
+ send(other_verb, '/example')
553
+ expected_rc = if other_verb == 'options' then 204
554
+ elsif other_verb == 'head' && verb == 'get' then 200
555
+ else 405
556
+ end
557
+ expect(last_response.status).to eql expected_rc
558
+ end
522
559
  end
523
560
  end
524
561
 
@@ -549,6 +586,7 @@ describe Grape::API do
549
586
  before_validation { raise 'before_validation filter should not run' }
550
587
  after_validation { raise 'after_validation filter should not run' }
551
588
  after { raise 'after filter should not run' }
589
+ params { requires :only_for_get }
552
590
  get
553
591
  end
554
592
 
@@ -573,6 +611,26 @@ describe Grape::API do
573
611
  expect(last_response.headers['X-Custom-Header']).to eql 'foo'
574
612
  end
575
613
 
614
+ it 'runs all filters and body with a custom OPTIONS method' do
615
+ subject.namespace :example do
616
+ before { header 'X-Custom-Header-1', 'foo' }
617
+ before_validation { header 'X-Custom-Header-2', 'foo' }
618
+ after_validation { header 'X-Custom-Header-3', 'foo' }
619
+ after { header 'X-Custom-Header-4', 'foo' }
620
+ options { 'yup' }
621
+ get
622
+ end
623
+
624
+ options '/example'
625
+ expect(last_response.status).to eql 200
626
+ expect(last_response.body).to eql 'yup'
627
+ expect(last_response.headers['Allow']).to be_nil
628
+ expect(last_response.headers['X-Custom-Header-1']).to eql 'foo'
629
+ expect(last_response.headers['X-Custom-Header-2']).to eql 'foo'
630
+ expect(last_response.headers['X-Custom-Header-3']).to eql 'foo'
631
+ expect(last_response.headers['X-Custom-Header-4']).to eql 'foo'
632
+ end
633
+
576
634
  context 'when format is xml' do
577
635
  it 'returns a 405 for an unsupported method' do
578
636
  subject.format :xml
@@ -594,8 +652,8 @@ XML
594
652
  context 'when accessing env' do
595
653
  it 'returns a 405 for an unsupported method' do
596
654
  subject.before do
597
- _custom_header_1 = headers['X-Custom-Header']
598
- _custom_header_2 = env['HTTP_X_CUSTOM_HEADER']
655
+ _customheader1 = headers['X-Custom-Header']
656
+ _customheader2 = env['HTTP_X_CUSTOM_HEADER']
599
657
  end
600
658
  subject.get 'example' do
601
659
  'example'
@@ -630,7 +688,11 @@ XML
630
688
 
631
689
  describe 'adds an OPTIONS route that' do
632
690
  before do
633
- subject.before { header 'X-Custom-Header', 'foo' }
691
+ subject.before { header 'X-Custom-Header', 'foo' }
692
+ subject.before_validation { header 'X-Custom-Header-2', 'bar' }
693
+ subject.after_validation { header 'X-Custom-Header-3', 'baz' }
694
+ subject.after { header 'X-Custom-Header-4', 'bing' }
695
+ subject.params { requires :only_for_get }
634
696
  subject.get 'example' do
635
697
  'example'
636
698
  end
@@ -652,10 +714,22 @@ XML
652
714
  expect(last_response.headers['Allow']).to eql 'OPTIONS, GET, HEAD'
653
715
  end
654
716
 
655
- it 'has a X-Custom-Header' do
717
+ it 'calls before hook' do
656
718
  expect(last_response.headers['X-Custom-Header']).to eql 'foo'
657
719
  end
658
720
 
721
+ it 'does not call before_validation hook' do
722
+ expect(last_response.headers.key?('X-Custom-Header-2')).to be false
723
+ end
724
+
725
+ it 'does not call after_validation hook' do
726
+ expect(last_response.headers.key?('X-Custom-Header-3')).to be false
727
+ end
728
+
729
+ it 'calls after hook' do
730
+ expect(last_response.headers['X-Custom-Header-4']).to eq 'bing'
731
+ end
732
+
659
733
  it 'has no Content-Type' do
660
734
  expect(last_response.content_type).to be_nil
661
735
  end
@@ -712,9 +786,6 @@ XML
712
786
  subject.post :example do
713
787
  'example'
714
788
  end
715
- subject.route :any, '*path' do
716
- error! :not_found, 404
717
- end
718
789
  get '/example'
719
790
  end
720
791
 
@@ -1131,7 +1202,7 @@ XML
1131
1202
  def initialize(app, *args)
1132
1203
  @args = args
1133
1204
  @app = app
1134
- @block = true if block_given?
1205
+ @block = block_given? ? true : nil
1135
1206
  end
1136
1207
 
1137
1208
  def call(env)
@@ -2555,13 +2626,11 @@ XML
2555
2626
  params: {
2556
2627
  'param1' => { required: true },
2557
2628
  'param2' => { required: false }
2558
- }
2559
- },
2629
+ } },
2560
2630
  { description: 'global description',
2561
2631
  params: {
2562
2632
  'param2' => { required: false }
2563
- }
2564
- }
2633
+ } }
2565
2634
  ]
2566
2635
  end
2567
2636
  it 'merges the parameters of the namespace with the parameters of the method' do
@@ -2585,8 +2654,7 @@ XML
2585
2654
  params: {
2586
2655
  'ns_param' => { required: true, desc: 'namespace parameter' },
2587
2656
  'method_param' => { required: false, desc: 'method parameter' }
2588
- }
2589
- }
2657
+ } }
2590
2658
  ]
2591
2659
  end
2592
2660
  it 'merges the parameters of nested namespaces' do
@@ -2618,8 +2686,7 @@ XML
2618
2686
  'ns1_param' => { required: true, desc: 'ns1 param' },
2619
2687
  'ns2_param' => { required: true, desc: 'ns2 param' },
2620
2688
  'method_param' => { required: false, desc: 'method param' }
2621
- }
2622
- }
2689
+ } }
2623
2690
  ]
2624
2691
  end
2625
2692
  it 'groups nested params and prevents overwriting of params with same name in different groups' do
@@ -2662,8 +2729,7 @@ XML
2662
2729
  'root_param' => { required: true, desc: 'root param' },
2663
2730
  'nested' => { required: true, type: 'Array' },
2664
2731
  'nested[nested_param]' => { required: true, desc: 'nested param' }
2665
- }
2666
- }
2732
+ } }
2667
2733
  ]
2668
2734
  end
2669
2735
  it 'allows to set the type attribute on :group element' do