grape 2.0.0 → 2.1.3

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -1
  3. data/README.md +362 -316
  4. data/UPGRADING.md +197 -7
  5. data/grape.gemspec +5 -6
  6. data/lib/grape/api/instance.rb +14 -11
  7. data/lib/grape/api.rb +19 -10
  8. data/lib/grape/content_types.rb +0 -2
  9. data/lib/grape/cookies.rb +2 -1
  10. data/lib/grape/dry_types.rb +0 -2
  11. data/lib/grape/dsl/desc.rb +22 -20
  12. data/lib/grape/dsl/headers.rb +1 -1
  13. data/lib/grape/dsl/inside_route.rb +46 -15
  14. data/lib/grape/dsl/parameters.rb +5 -4
  15. data/lib/grape/dsl/routing.rb +20 -4
  16. data/lib/grape/dsl/validations.rb +13 -0
  17. data/lib/grape/endpoint.rb +14 -17
  18. data/lib/grape/{util/env.rb → env.rb} +0 -5
  19. data/lib/grape/error_formatter/txt.rb +11 -10
  20. data/lib/grape/exceptions/base.rb +3 -3
  21. data/lib/grape/exceptions/validation.rb +0 -2
  22. data/lib/grape/exceptions/validation_array_errors.rb +1 -0
  23. data/lib/grape/exceptions/validation_errors.rb +2 -4
  24. data/lib/grape/extensions/hash.rb +5 -1
  25. data/lib/grape/http/headers.rb +18 -34
  26. data/lib/grape/{util/json.rb → json.rb} +1 -3
  27. data/lib/grape/locale/en.yml +3 -0
  28. data/lib/grape/middleware/auth/base.rb +0 -2
  29. data/lib/grape/middleware/auth/dsl.rb +0 -2
  30. data/lib/grape/middleware/base.rb +1 -3
  31. data/lib/grape/middleware/error.rb +55 -50
  32. data/lib/grape/middleware/formatter.rb +16 -13
  33. data/lib/grape/middleware/globals.rb +1 -3
  34. data/lib/grape/middleware/stack.rb +4 -5
  35. data/lib/grape/middleware/versioner/accept_version_header.rb +0 -2
  36. data/lib/grape/middleware/versioner/header.rb +17 -163
  37. data/lib/grape/middleware/versioner/param.rb +2 -4
  38. data/lib/grape/middleware/versioner/path.rb +1 -3
  39. data/lib/grape/namespace.rb +3 -4
  40. data/lib/grape/path.rb +24 -29
  41. data/lib/grape/request.rb +4 -12
  42. data/lib/grape/router/base_route.rb +39 -0
  43. data/lib/grape/router/greedy_route.rb +20 -0
  44. data/lib/grape/router/pattern.rb +39 -30
  45. data/lib/grape/router/route.rb +22 -59
  46. data/lib/grape/router.rb +32 -37
  47. data/lib/grape/util/accept_header_handler.rb +105 -0
  48. data/lib/grape/util/base_inheritable.rb +4 -4
  49. data/lib/grape/util/cache.rb +0 -3
  50. data/lib/grape/util/endpoint_configuration.rb +1 -1
  51. data/lib/grape/util/header.rb +13 -0
  52. data/lib/grape/util/inheritable_values.rb +0 -2
  53. data/lib/grape/util/lazy/block.rb +29 -0
  54. data/lib/grape/util/lazy/object.rb +45 -0
  55. data/lib/grape/util/lazy/value.rb +38 -0
  56. data/lib/grape/util/lazy/value_array.rb +21 -0
  57. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  58. data/lib/grape/util/lazy/value_hash.rb +21 -0
  59. data/lib/grape/util/media_type.rb +70 -0
  60. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  61. data/lib/grape/util/stackable_values.rb +1 -6
  62. data/lib/grape/util/strict_hash_configuration.rb +3 -3
  63. data/lib/grape/validations/attributes_doc.rb +38 -36
  64. data/lib/grape/validations/attributes_iterator.rb +1 -0
  65. data/lib/grape/validations/contract_scope.rb +71 -0
  66. data/lib/grape/validations/params_scope.rb +22 -19
  67. data/lib/grape/validations/types/array_coercer.rb +0 -2
  68. data/lib/grape/validations/types/build_coercer.rb +69 -71
  69. data/lib/grape/validations/types/dry_type_coercer.rb +1 -11
  70. data/lib/grape/validations/types/json.rb +0 -2
  71. data/lib/grape/validations/types/primitive_coercer.rb +0 -2
  72. data/lib/grape/validations/types/set_coercer.rb +0 -3
  73. data/lib/grape/validations/types.rb +0 -3
  74. data/lib/grape/validations/validators/base.rb +1 -0
  75. data/lib/grape/validations/validators/default_validator.rb +5 -1
  76. data/lib/grape/validations/validators/exactly_one_of_validator.rb +1 -1
  77. data/lib/grape/validations/validators/length_validator.rb +42 -0
  78. data/lib/grape/validations/validators/values_validator.rb +6 -1
  79. data/lib/grape/validations.rb +3 -7
  80. data/lib/grape/version.rb +1 -1
  81. data/lib/grape/{util/xml.rb → xml.rb} +1 -1
  82. data/lib/grape.rb +30 -274
  83. metadata +30 -37
  84. data/lib/grape/eager_load.rb +0 -20
  85. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  86. data/lib/grape/router/attribute_translator.rb +0 -63
  87. data/lib/grape/util/lazy_block.rb +0 -27
  88. data/lib/grape/util/lazy_object.rb +0 -43
  89. data/lib/grape/util/lazy_value.rb +0 -91
data/UPGRADING.md CHANGED
@@ -1,6 +1,199 @@
1
1
  Upgrading Grape
2
2
  ===============
3
3
 
4
+ ### Upgrading to >= 2.1.0
5
+
6
+ #### Optional Builder
7
+
8
+ The `builder` gem dependency has been made optional as it's only used when generating XML. If your code does, add `builder` to your `Gemfile`.
9
+
10
+ See [#2445](https://github.com/ruby-grape/grape/pull/2445) for more information.
11
+
12
+ #### Deep Merging of Parameter Attributes
13
+
14
+ Grape now uses `deep_merge` to combine parameter attributes within the `with` method. Previously, attributes defined at the parameter level would override those defined at the group level.
15
+ With deep merge, attributes are now combined, allowing for more detailed and nuanced API specifications.
16
+
17
+ For example:
18
+
19
+ ```ruby
20
+ with(documentation: { in: 'body' }) do
21
+ optional :vault, documentation: { default: 33 }
22
+ end
23
+ ```
24
+
25
+ Before it was equivalent to:
26
+
27
+ ```ruby
28
+ optional :vault, documentation: { default: 33 }
29
+ ```
30
+
31
+ After it is an equivalent of:
32
+
33
+ ```ruby
34
+ optional :vault, documentation: { in: 'body', default: 33 }
35
+ ```
36
+
37
+ See [#2432](https://github.com/ruby-grape/grape/pull/2432) for more information.
38
+
39
+ #### Zeitwerk
40
+
41
+ Grape's autoloader has been updated and it's now based on [Zeitwerk](https://github.com/fxn/zeitwerk).
42
+ If you MP (Monkey Patch) some files and you're not following the [file structure](https://github.com/fxn/zeitwerk?tab=readme-ov-file#file-structure), you might end up with a Zeitwerk error.
43
+
44
+ See [#2363](https://github.com/ruby-grape/grape/pull/2363) for more information.
45
+
46
+ #### Changes in rescue_from
47
+
48
+ The `rack_response` method has been deprecated and the `error_response` method has been removed. Use `error!` instead.
49
+
50
+ See [#2414](https://github.com/ruby-grape/grape/pull/2414) for more information.
51
+
52
+ #### Change in parameters precedence
53
+
54
+ When using together with `Grape::Extensions::Hash::ParamBuilder`, `route_param` takes higher precedence over a regular parameter defined with same name, which now matches the default param builder behavior.
55
+
56
+ This was a regression introduced by [#2326](https://github.com/ruby-grape/grape/pull/2326) in Grape v1.8.0.
57
+
58
+ ```ruby
59
+ grape.configure do |config|
60
+ config.param_builder = Grape::Extensions::Hash::ParamBuilder
61
+ end
62
+
63
+ params do
64
+ requires :foo, type: String
65
+ end
66
+ route_param :foo do
67
+ get do
68
+ { value: params[:foo] }
69
+ end
70
+ end
71
+ ```
72
+
73
+ Request:
74
+
75
+ ```bash
76
+ curl -X POST -H "Content-Type: application/json" localhost:9292/bar -d '{"foo": "baz"}'
77
+ ```
78
+
79
+ Response prior to v1.8.0:
80
+
81
+ ```json
82
+ {
83
+ "value": "bar"
84
+ }
85
+ ```
86
+
87
+ v1.8.0..v2.0.0:
88
+
89
+ ```json
90
+ {
91
+ "value": "baz"
92
+ }
93
+ ```
94
+
95
+ v2.1.0+:
96
+
97
+ ```json
98
+ {
99
+ "value": "bar"
100
+ }
101
+ ```
102
+
103
+ See [#2378](https://github.com/ruby-grape/grape/pull/2378) for details.
104
+
105
+ #### Grape::Router::Route.route_xxx methods have been removed
106
+
107
+ - `route_method` is accessible through `request_method`
108
+ - `route_path` is accessible through `path`
109
+ - Any other `route_xyz` are accessible through `options[xyz]`
110
+
111
+ #### Instance variables scope
112
+
113
+ Due to the changes done in [#2377](https://github.com/ruby-grape/grape/pull/2377), the instance variables defined inside each of the endpoints (or inside a `before` validator) are now accessible inside the `rescue_from`. The behavior of the instance variables was undefined until `2.1.0`.
114
+
115
+ If you were using the same variable name defined inside an endpoint or `before` validator inside a `rescue_from` handler, you need to take in mind that you can start getting different values or you can be overriding values.
116
+
117
+ Before:
118
+ ```ruby
119
+ class TwitterAPI < Grape::API
120
+ before do
121
+ @var = 1
122
+ end
123
+
124
+ get '/' do
125
+ puts @var # => 1
126
+ raise
127
+ end
128
+
129
+ rescue_from :all do
130
+ puts @var # => nil
131
+ end
132
+ end
133
+ ```
134
+
135
+ After:
136
+ ```ruby
137
+ class TwitterAPI < Grape::API
138
+ before do
139
+ @var = 1
140
+ end
141
+
142
+ get '/' do
143
+ puts @var # => 1
144
+ raise
145
+ end
146
+
147
+ rescue_from :all do
148
+ puts @var # => 1
149
+ end
150
+ end
151
+ ```
152
+
153
+ #### Recognizing Path
154
+
155
+ Grape now considers the types of the configured `route_params` in order to determine the endpoint that matches with the performed request.
156
+
157
+ So taking into account this `Grape::API` class
158
+
159
+ ```ruby
160
+ class Books < Grape::API
161
+ resource :books do
162
+ route_param :id, type: Integer do
163
+ # GET /books/:id
164
+ get do
165
+ #...
166
+ end
167
+ end
168
+
169
+ resource :share do
170
+ # POST /books/share
171
+ post do
172
+ # ....
173
+ end
174
+ end
175
+ end
176
+ end
177
+ ```
178
+
179
+ Before:
180
+ ```ruby
181
+ API.recognize_path '/books/1' # => /books/:id
182
+ API.recognize_path '/books/share' # => /books/:id
183
+ API.recognize_path '/books/other' # => /books/:id
184
+ ```
185
+
186
+ After:
187
+ ```ruby
188
+ API.recognize_path '/books/1' # => /books/:id
189
+ API.recognize_path '/books/share' # => /books/share
190
+ API.recognize_path '/books/other' # => nil
191
+ ```
192
+
193
+ This implies that before this changes, when you performed `/books/other` and it matched with the `/books/:id` endpoint, you get a `400 Bad Request` response because the type of the provided `:id` param was not an `Integer`. However, after upgrading to version `2.1.0` you will get a `404 Not Found` response, because there is not a defined endpoint that matches with `/books/other`.
194
+
195
+ See [#2379](https://github.com/ruby-grape/grape/pull/2379) for more information.
196
+
4
197
  ### Upgrading to >= 2.0.0
5
198
 
6
199
  #### Headers
@@ -19,7 +212,7 @@ If you are using Rack 3 in your application then the headers will be set to:
19
212
  { "content-type" => "application/json", "secret-password" => "foo"}
20
213
  ```
21
214
 
22
- This means if you are checking for header values in your application, you would need to change your code to use downcased keys.
215
+ This means if you are checking for header values in your application, you would need to change your code to use downcased keys.
23
216
 
24
217
  ```ruby
25
218
  get do
@@ -474,8 +667,7 @@ end
474
667
 
475
668
  ##### `name` (and other caveats) of the mounted API
476
669
 
477
- After the patch, the mounted API is no longer a Named class inheriting from `Grape::API`, it is an anonymous class
478
- which inherit from `Grape::API::Instance`.
670
+ After the patch, the mounted API is no longer a Named class inheriting from `Grape::API`, it is an anonymous class which inherit from `Grape::API::Instance`.
479
671
 
480
672
  What this means in practice, is:
481
673
 
@@ -855,8 +1047,7 @@ See [#1114](https://github.com/ruby-grape/grape/pull/1114) for more information.
855
1047
 
856
1048
  #### Bypasses formatters when status code indicates no content
857
1049
 
858
- To be consistent with rack and it's handling of standard responses associated with no content, both default and custom formatters will now
859
- be bypassed when processing responses for status codes defined [by rack](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567)
1050
+ To be consistent with rack and it's handling of standard responses associated with no content, both default and custom formatters will now be bypassed when processing responses for status codes defined [by rack](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567)
860
1051
 
861
1052
  See [#1190](https://github.com/ruby-grape/grape/pull/1190) for more information.
862
1053
 
@@ -1297,8 +1488,7 @@ As replacement can be used
1297
1488
  * `Grape::Middleware::Auth::Digest` => [`Rack::Auth::Digest::MD5`](https://github.com/rack/rack/blob/master/lib/rack/auth/digest/md5.rb)
1298
1489
  * `Grape::Middleware::Auth::OAuth2` => [warden-oauth2](https://github.com/opperator/warden-oauth2) or [rack-oauth2](https://github.com/nov/rack-oauth2)
1299
1490
 
1300
- If this is not possible you can extract the middleware files from [grape v0.7.0](https://github.com/ruby-grape/grape/tree/v0.7.0/lib/grape/middleware/auth)
1301
- and host these files within your application
1491
+ If this is not possible you can extract the middleware files from [grape v0.7.0](https://github.com/ruby-grape/grape/tree/v0.7.0/lib/grape/middleware/auth) and host these files within your application
1302
1492
 
1303
1493
  See [#703](https://github.com/ruby-grape/Grape/pull/703) for more information.
1304
1494
 
data/grape.gemspec CHANGED
@@ -20,14 +20,13 @@ Gem::Specification.new do |s|
20
20
  'source_code_uri' => "https://github.com/ruby-grape/grape/tree/v#{s.version}"
21
21
  }
22
22
 
23
- s.add_runtime_dependency 'activesupport', '>= 5'
24
- s.add_runtime_dependency 'builder'
23
+ s.add_runtime_dependency 'activesupport', '>= 6'
25
24
  s.add_runtime_dependency 'dry-types', '>= 1.1'
26
- s.add_runtime_dependency 'mustermann-grape', '~> 1.0.0'
27
- s.add_runtime_dependency 'rack', '>= 1.3.0'
28
- s.add_runtime_dependency 'rack-accept'
25
+ s.add_runtime_dependency 'mustermann-grape', '~> 1.1.0'
26
+ s.add_runtime_dependency 'rack', '>= 2'
27
+ s.add_runtime_dependency 'zeitwerk'
29
28
 
30
29
  s.files = Dir['lib/**/*', 'CHANGELOG.md', 'CONTRIBUTING.md', 'README.md', 'grape.png', 'UPGRADING.md', 'LICENSE', 'grape.gemspec']
31
30
  s.require_paths = ['lib']
32
- s.required_ruby_version = '>= 2.6.0'
31
+ s.required_ruby_version = '>= 2.7.0'
33
32
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/router'
4
-
5
3
  module Grape
6
4
  class API
7
5
  # The API Instance class, is the engine behind Grape::API. Each class that inherits
@@ -48,7 +46,7 @@ module Grape
48
46
  # Parses the API's definition and compiles it into an instance of
49
47
  # Grape::API.
50
48
  def compile
51
- @instance ||= new
49
+ @instance ||= new # rubocop:disable Naming/MemoizedInstanceVariableName
52
50
  end
53
51
 
54
52
  # Wipe the compiled API so we can recompile after changes were made.
@@ -112,7 +110,7 @@ module Grape
112
110
  end
113
111
 
114
112
  def evaluate_as_instance_with_configuration(block, lazy: false)
115
- lazy_block = Grape::Util::LazyBlock.new do |configuration|
113
+ lazy_block = Grape::Util::Lazy::Block.new do |configuration|
116
114
  value_for_configuration = configuration
117
115
  self.configuration = value_for_configuration.evaluate if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy?
118
116
  response = instance_eval(&block)
@@ -127,6 +125,7 @@ module Grape
127
125
  end
128
126
 
129
127
  def inherited(subclass)
128
+ super
130
129
  subclass.reset!
131
130
  subclass.logger = logger.clone
132
131
  end
@@ -162,9 +161,13 @@ module Grape
162
161
 
163
162
  # Handle a request. See Rack documentation for what `env` is.
164
163
  def call(env)
165
- result = @router.call(env)
166
- result[1].delete(Grape::Http::Headers::X_CASCADE) unless cascade?
167
- result
164
+ status, headers, response = @router.call(env)
165
+ unless cascade?
166
+ headers = Grape::Util::Header.new.merge(headers)
167
+ headers.delete(Grape::Http::Headers::X_CASCADE)
168
+ end
169
+
170
+ [status, headers, response]
168
171
  end
169
172
 
170
173
  # Some requests may return a HTTP 404 error if grape cannot find a matching
@@ -203,11 +206,11 @@ module Grape
203
206
 
204
207
  allowed_methods = config[:methods].dup
205
208
 
206
- allowed_methods |= [Grape::Http::Headers::HEAD] if !self.class.namespace_inheritable(:do_not_route_head) && allowed_methods.include?(Grape::Http::Headers::GET)
209
+ allowed_methods |= [Rack::HEAD] if !self.class.namespace_inheritable(:do_not_route_head) && allowed_methods.include?(Rack::GET)
207
210
 
208
- allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods)
211
+ allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Rack::OPTIONS] | allowed_methods)
209
212
 
210
- config[:endpoint].options[:options_route_enabled] = true unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS)
213
+ config[:endpoint].options[:options_route_enabled] = true unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Rack::OPTIONS)
211
214
 
212
215
  attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header)
213
216
  generate_not_allowed_method(config[:pattern], **attributes)
@@ -218,7 +221,7 @@ module Grape
218
221
 
219
222
  def collect_route_config_per_pattern
220
223
  all_routes = self.class.endpoints.map(&:routes).flatten
221
- routes_by_regexp = all_routes.group_by { |route| route.pattern.to_regexp }
224
+ routes_by_regexp = all_routes.group_by(&:pattern_regexp)
222
225
 
223
226
  # Build the configuration based on the first endpoint and the collection of methods supported.
224
227
  routes_by_regexp.values.map do |routes|
data/lib/grape/api.rb CHANGED
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/router'
4
- require 'grape/api/instance'
5
-
6
3
  module Grape
7
4
  # The API class is the primary entry point for creating Grape APIs. Users
8
5
  # should subclass this class in order to build an API.
@@ -26,8 +23,8 @@ module Grape
26
23
  attr_accessor :base_instance, :instances
27
24
 
28
25
  # Rather than initializing an object of type Grape::API, create an object of type Instance
29
- def new(*args, &block)
30
- base_instance.new(*args, &block)
26
+ def new(...)
27
+ base_instance.new(...)
31
28
  end
32
29
 
33
30
  # When inherited, will create a list of all instances (times the API was mounted)
@@ -35,7 +32,7 @@ module Grape
35
32
  def inherited(api)
36
33
  super
37
34
 
38
- api.initial_setup(Grape::API == self ? Grape::API::Instance : @base_instance)
35
+ api.initial_setup(self == Grape::API ? Grape::API::Instance : @base_instance)
39
36
  api.override_all_methods!
40
37
  end
41
38
 
@@ -77,8 +74,8 @@ module Grape
77
74
  # the headers, and the body. See [the rack specification]
78
75
  # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
79
76
  # NOTE: This will only be called on an API directly mounted on RACK
80
- def call(*args, &block)
81
- instance_for_rack.call(*args, &block)
77
+ def call(...)
78
+ instance_for_rack.call(...)
82
79
  end
83
80
 
84
81
  # Alleviates problems with autoloading by tring to search for the constant
@@ -111,7 +108,7 @@ module Grape
111
108
  end
112
109
 
113
110
  def respond_to?(method, include_private = false)
114
- super(method, include_private) || base_instance.respond_to?(method, include_private)
111
+ super || base_instance.respond_to?(method, include_private)
115
112
  end
116
113
 
117
114
  def respond_to_missing?(method, include_private = false)
@@ -128,7 +125,6 @@ module Grape
128
125
  end
129
126
 
130
127
  def compile!
131
- require 'grape/eager_load'
132
128
  instance_for_rack.compile! # See API::Instance.compile!
133
129
  end
134
130
 
@@ -150,6 +146,19 @@ module Grape
150
146
  @instances.each do |instance|
151
147
  last_response = replay_step_on(instance, setup_step)
152
148
  end
149
+
150
+ # Updating all previously mounted classes in the case that new methods have been executed.
151
+ if method != :mount && @setup.any?
152
+ previous_mount_steps = @setup.select { |step| step[:method] == :mount }
153
+ previous_mount_steps.each do |mount_step|
154
+ refresh_mount_step = mount_step.merge(method: :refresh_mounted_api)
155
+ @setup += [refresh_mount_step]
156
+ @instances.each do |instance|
157
+ replay_step_on(instance, refresh_mount_step)
158
+ end
159
+ end
160
+ end
161
+
153
162
  last_response
154
163
  end
155
164
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/util/registrable'
4
-
5
3
  module Grape
6
4
  module ContentTypes
7
5
  extend Util::Registrable
data/lib/grape/cookies.rb CHANGED
@@ -33,9 +33,10 @@ module Grape
33
33
  @cookies.each(&block)
34
34
  end
35
35
 
36
+ # see https://github.com/rack/rack/blob/main/lib/rack/utils.rb#L338-L340
36
37
  # rubocop:disable Layout/SpaceBeforeBrackets
37
38
  def delete(name, **opts)
38
- options = opts.merge(value: 'deleted', expires: Time.at(0))
39
+ options = opts.merge(max_age: '0', value: '', expires: Time.at(0))
39
40
  self.[]=(name, options)
40
41
  end
41
42
  # rubocop:enable Layout/SpaceBeforeBrackets
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry-types'
4
-
5
3
  module Grape
6
4
  module DryTypes
7
5
  # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is
@@ -5,6 +5,27 @@ module Grape
5
5
  module Desc
6
6
  include Grape::DSL::Settings
7
7
 
8
+ ROUTE_ATTRIBUTES = %i[
9
+ body_name
10
+ consumes
11
+ default
12
+ deprecated
13
+ description
14
+ detail
15
+ entity
16
+ headers
17
+ hidden
18
+ http_codes
19
+ is_array
20
+ named
21
+ nickname
22
+ params
23
+ produces
24
+ security
25
+ summary
26
+ tags
27
+ ].freeze
28
+
8
29
  # Add a description to the next namespace or function.
9
30
  # @param description [String] descriptive string for this endpoint
10
31
  # or namespace
@@ -81,26 +102,7 @@ module Grape
81
102
  # Returns an object which configures itself via an instance-context DSL.
82
103
  def desc_container(endpoint_configuration)
83
104
  Module.new do
84
- include Grape::Util::StrictHashConfiguration.module(
85
- :summary,
86
- :description,
87
- :detail,
88
- :params,
89
- :entity,
90
- :http_codes,
91
- :named,
92
- :body_name,
93
- :headers,
94
- :hidden,
95
- :deprecated,
96
- :is_array,
97
- :nickname,
98
- :produces,
99
- :consumes,
100
- :security,
101
- :tags,
102
- :default
103
- )
105
+ include Grape::Util::StrictHashConfiguration.module(*ROUTE_ATTRIBUTES)
104
106
  config_context.define_singleton_method(:configuration) do
105
107
  endpoint_configuration
106
108
  end
@@ -12,7 +12,7 @@ module Grape
12
12
  if key
13
13
  val ? header[key.to_s] = val : header.delete(key.to_s)
14
14
  else
15
- @header ||= {}
15
+ @header ||= Grape::Util::Header.new
16
16
  end
17
17
  end
18
18
  alias headers header
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/dsl/headers'
4
-
5
3
  module Grape
6
4
  module DSL
7
5
  module InsideRoute
@@ -31,11 +29,17 @@ module Grape
31
29
  options = options.reverse_merge(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
32
30
  declared_params ||= optioned_declared_params(**options)
33
31
 
34
- if passed_params.is_a?(Array)
35
- declared_array(passed_params, options, declared_params, params_nested_path)
36
- else
37
- declared_hash(passed_params, options, declared_params, params_nested_path)
32
+ res = if passed_params.is_a?(Array)
33
+ declared_array(passed_params, options, declared_params, params_nested_path)
34
+ else
35
+ declared_hash(passed_params, options, declared_params, params_nested_path)
36
+ end
37
+
38
+ if (key_maps = namespace_stackable(:contract_key_map))
39
+ key_maps.each { |key_map| key_map.write(passed_params, res) }
38
40
  end
41
+
42
+ res
39
43
  end
40
44
 
41
45
  private
@@ -99,7 +103,7 @@ module Grape
99
103
 
100
104
  route_options_params = options[:route_options][:params] || {}
101
105
  type = route_options_params.dig(key, :type)
102
- has_children = route_options_params.keys.any? { |k| k != key && k.start_with?(key) }
106
+ has_children = route_options_params.keys.any? { |k| k != key && k.start_with?("#{key}[") }
103
107
 
104
108
  if type == 'Hash' && !has_children
105
109
  {}
@@ -159,12 +163,32 @@ module Grape
159
163
  # end user with the specified message.
160
164
  #
161
165
  # @param message [String] The message to display.
162
- # @param status [Integer] the HTTP Status Code. Defaults to default_error_status, 500 if not set.
166
+ # @param status [Integer] The HTTP Status Code. Defaults to default_error_status, 500 if not set.
163
167
  # @param additional_headers [Hash] Addtional headers for the response.
164
- def error!(message, status = nil, additional_headers = nil)
165
- self.status(status || namespace_inheritable(:default_error_status))
168
+ # @param backtrace [Array<String>] The backtrace of the exception that caused the error.
169
+ # @param original_exception [Exception] The original exception that caused the error.
170
+ def error!(message, status = nil, additional_headers = nil, backtrace = nil, original_exception = nil)
171
+ status = self.status(status || namespace_inheritable(:default_error_status))
166
172
  headers = additional_headers.present? ? header.merge(additional_headers) : header
167
- throw :error, message: message, status: self.status, headers: headers
173
+ throw :error, message: message, status: status, headers: headers, backtrace: backtrace, original_exception: original_exception
174
+ end
175
+
176
+ # Creates a Rack response based on the provided message, status, and headers.
177
+ # The content type in the headers is set to the default content type unless provided.
178
+ # The message is HTML-escaped if the content type is 'text/html'.
179
+ #
180
+ # @param message [String] The content of the response.
181
+ # @param status [Integer] The HTTP status code.
182
+ # @params headers [Hash] (optional) Headers for the response
183
+ # (default: {Rack::CONTENT_TYPE => content_type}).
184
+ #
185
+ # Returns:
186
+ # A Rack::Response object containing the specified message, status, and headers.
187
+ #
188
+ def rack_response(message, status = 200, headers = { Rack::CONTENT_TYPE => content_type })
189
+ Grape.deprecator.warn('The rack_response method has been deprecated, use error! instead.')
190
+ message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == 'text/html'
191
+ Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), headers)
168
192
  end
169
193
 
170
194
  # Redirect to a new url.
@@ -178,7 +202,7 @@ module Grape
178
202
  if permanent
179
203
  status 301
180
204
  body_message ||= "This resource has been moved permanently to #{url}."
181
- elsif env[Grape::Http::Headers::HTTP_VERSION] == 'HTTP/1.1' && request.request_method.to_s.upcase != Grape::Http::Headers::GET
205
+ elsif http_version == 'HTTP/1.1' && !request.get?
182
206
  status 303
183
207
  body_message ||= "An alternate resource is located at #{url}."
184
208
  else
@@ -204,10 +228,9 @@ module Grape
204
228
  when nil
205
229
  return @status if instance_variable_defined?(:@status) && @status
206
230
 
207
- case request.request_method.to_s.upcase
208
- when Grape::Http::Headers::POST
231
+ if request.post?
209
232
  201
210
- when Grape::Http::Headers::DELETE
233
+ elsif request.delete?
211
234
  if instance_variable_defined?(:@body) && @body.present?
212
235
  200
213
236
  else
@@ -436,6 +459,14 @@ module Grape
436
459
  embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
437
460
  entity_class.represent(object, **embeds.merge(options))
438
461
  end
462
+
463
+ def http_version
464
+ env['HTTP_VERSION'] || env[Rack::SERVER_PROTOCOL]
465
+ end
466
+
467
+ def context
468
+ self
469
+ end
439
470
  end
440
471
  end
441
472
  end
@@ -130,7 +130,7 @@ module Grape
130
130
 
131
131
  opts = attrs.extract_options!.clone
132
132
  opts[:presence] = { value: true, message: opts[:message] }
133
- opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group
133
+ opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group
134
134
 
135
135
  if opts[:using]
136
136
  require_required_and_optional_fields(attrs.first, opts)
@@ -149,7 +149,7 @@ module Grape
149
149
 
150
150
  opts = attrs.extract_options!.clone
151
151
  type = opts[:type]
152
- opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group
152
+ opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group
153
153
 
154
154
  # check type for optional parameter group
155
155
  if attrs && block
@@ -170,7 +170,8 @@ module Grape
170
170
  # @param (see #requires)
171
171
  # @option (see #requires)
172
172
  def with(*attrs, &block)
173
- new_group_scope(attrs.clone, &block)
173
+ new_group_attrs = [@group, attrs.clone.first].compact.reduce(&:deep_merge)
174
+ new_group_scope([new_group_attrs], &block)
174
175
  end
175
176
 
176
177
  # Disallow the given parameters to be present in the same request.
@@ -230,7 +231,7 @@ module Grape
230
231
 
231
232
  alias group requires
232
233
 
233
- class EmptyOptionalValue; end
234
+ class EmptyOptionalValue; end # rubocop:disable Lint/EmptyClass
234
235
 
235
236
  def map_params(params, element, is_array = false)
236
237
  if params.is_a?(Array)