grape 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -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 +13 -10
  7. data/lib/grape/api.rb +17 -8
  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 +38 -13
  14. data/lib/grape/dsl/parameters.rb +4 -3
  15. data/lib/grape/dsl/routing.rb +20 -4
  16. data/lib/grape/dsl/validations.rb +13 -0
  17. data/lib/grape/endpoint.rb +12 -15
  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 +1 -3
  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 +0 -2
  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 +2 -3
  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 +30 -36
  47. data/lib/grape/util/accept/header.rb +19 -0
  48. data/lib/grape/util/accept_header_handler.rb +105 -0
  49. data/lib/grape/util/base_inheritable.rb +4 -4
  50. data/lib/grape/util/cache.rb +0 -3
  51. data/lib/grape/util/endpoint_configuration.rb +1 -1
  52. data/lib/grape/util/header.rb +13 -0
  53. data/lib/grape/util/inheritable_values.rb +0 -2
  54. data/lib/grape/util/lazy/block.rb +29 -0
  55. data/lib/grape/util/lazy/object.rb +45 -0
  56. data/lib/grape/util/lazy/value.rb +38 -0
  57. data/lib/grape/util/lazy/value_array.rb +21 -0
  58. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  59. data/lib/grape/util/lazy/value_hash.rb +21 -0
  60. data/lib/grape/util/media_type.rb +70 -0
  61. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  62. data/lib/grape/util/stackable_values.rb +1 -6
  63. data/lib/grape/util/strict_hash_configuration.rb +3 -3
  64. data/lib/grape/validations/attributes_doc.rb +38 -36
  65. data/lib/grape/validations/contract_scope.rb +71 -0
  66. data/lib/grape/validations/params_scope.rb +10 -9
  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/length_validator.rb +42 -0
  77. data/lib/grape/validations/validators/values_validator.rb +6 -1
  78. data/lib/grape/validations.rb +3 -7
  79. data/lib/grape/version.rb +1 -1
  80. data/lib/grape/{util/xml.rb → xml.rb} +1 -1
  81. data/lib/grape.rb +30 -274
  82. metadata +31 -37
  83. data/lib/grape/eager_load.rb +0 -20
  84. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  85. data/lib/grape/router/attribute_translator.rb +0 -63
  86. data/lib/grape/util/lazy_block.rb +0 -27
  87. data/lib/grape/util/lazy_object.rb +0 -43
  88. 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
@@ -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)
@@ -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
@@ -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
  {}
@@ -162,9 +166,27 @@ module Grape
162
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
168
  def error!(message, status = nil, additional_headers = nil)
165
- self.status(status || namespace_inheritable(:default_error_status))
169
+ status = self.status(status || namespace_inheritable(:default_error_status))
166
170
  headers = additional_headers.present? ? header.merge(additional_headers) : header
167
- throw :error, message: message, status: self.status, headers: headers
171
+ throw :error, message: message, status: status, headers: headers
172
+ end
173
+
174
+ # Creates a Rack response based on the provided message, status, and headers.
175
+ # The content type in the headers is set to the default content type unless provided.
176
+ # The message is HTML-escaped if the content type is 'text/html'.
177
+ #
178
+ # @param message [String] The content of the response.
179
+ # @param status [Integer] The HTTP status code.
180
+ # @params headers [Hash] (optional) Headers for the response
181
+ # (default: {Rack::CONTENT_TYPE => content_type}).
182
+ #
183
+ # Returns:
184
+ # A Rack::Response object containing the specified message, status, and headers.
185
+ #
186
+ def rack_response(message, status = 200, headers = { Rack::CONTENT_TYPE => content_type })
187
+ Grape.deprecator.warn('The rack_response method has been deprecated, use error! instead.')
188
+ message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == 'text/html'
189
+ Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), headers)
168
190
  end
169
191
 
170
192
  # Redirect to a new url.
@@ -178,7 +200,7 @@ module Grape
178
200
  if permanent
179
201
  status 301
180
202
  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
203
+ elsif http_version == 'HTTP/1.1' && !request.get?
182
204
  status 303
183
205
  body_message ||= "An alternate resource is located at #{url}."
184
206
  else
@@ -204,10 +226,9 @@ module Grape
204
226
  when nil
205
227
  return @status if instance_variable_defined?(:@status) && @status
206
228
 
207
- case request.request_method.to_s.upcase
208
- when Grape::Http::Headers::POST
229
+ if request.post?
209
230
  201
210
- when Grape::Http::Headers::DELETE
231
+ elsif request.delete?
211
232
  if instance_variable_defined?(:@body) && @body.present?
212
233
  200
213
234
  else
@@ -436,6 +457,10 @@ module Grape
436
457
  embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
437
458
  entity_class.represent(object, **embeds.merge(options))
438
459
  end
460
+
461
+ def http_version
462
+ env['HTTP_VERSION'] || env[Rack::SERVER_PROTOCOL]
463
+ end
439
464
  end
440
465
  end
441
466
  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.
@@ -30,7 +30,7 @@ module Grape
30
30
  if args.any?
31
31
  options = args.extract_options!
32
32
  options = options.reverse_merge(using: :path)
33
- requested_versions = args.flatten
33
+ requested_versions = args.flatten.map(&:to_s)
34
34
 
35
35
  raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor)
36
36
 
@@ -54,7 +54,7 @@ module Grape
54
54
 
55
55
  # Define a root URL prefix for your entire API.
56
56
  def prefix(prefix = nil)
57
- namespace_inheritable(:root_prefix, prefix)
57
+ namespace_inheritable(:root_prefix, prefix&.to_s)
58
58
  end
59
59
 
60
60
  # Create a scope without affecting the URL.
@@ -85,8 +85,8 @@ module Grape
85
85
  mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
86
86
  mounts.each_pair do |app, path|
87
87
  if app.respond_to?(:mount_instance)
88
- opts_with = opts.any? ? opts.shift[:with] : {}
89
- mount({ app.mount_instance(configuration: opts_with) => path })
88
+ opts_with = opts.any? ? opts.first[:with] : {}
89
+ mount({ app.mount_instance(configuration: opts_with) => path }, *opts)
90
90
  next
91
91
  end
92
92
  in_setting = inheritable_setting
@@ -103,6 +103,15 @@ module Grape
103
103
  change!
104
104
  end
105
105
 
106
+ # When trying to mount multiple times the same endpoint, remove the previous ones
107
+ # from the list of endpoints if refresh_already_mounted parameter is true
108
+ refresh_already_mounted = opts.any? ? opts.first[:refresh_already_mounted] : false
109
+ if refresh_already_mounted && !endpoints.empty?
110
+ endpoints.delete_if do |endpoint|
111
+ endpoint.options[:app].to_s == app.to_s
112
+ end
113
+ end
114
+
106
115
  endpoints << Grape::Endpoint.new(
107
116
  in_setting,
108
117
  method: :any,
@@ -225,6 +234,13 @@ module Grape
225
234
  def versions
226
235
  @versions ||= []
227
236
  end
237
+
238
+ private
239
+
240
+ def refresh_mounted_api(mounts, *opts)
241
+ opts << { refresh_already_mounted: true }
242
+ mount(mounts, *opts)
243
+ end
228
244
  end
229
245
  end
230
246
  end
@@ -38,6 +38,19 @@ module Grape
38
38
  def params(&block)
39
39
  Grape::Validations::ParamsScope.new(api: self, type: Hash, &block)
40
40
  end
41
+
42
+ # Declare the contract to be used for the endpoint's parameters.
43
+ # @param contract [Class<Dry::Validation::Contract> | Dry::Schema::Processor]
44
+ # The contract or schema to be used for validation. Optional.
45
+ # @yield a block yielding a new instance of Dry::Schema::Params
46
+ # subclass, allowing to define the schema inline. When the
47
+ # +contract+ parameter is a schema, it will be used as a parent. Optional.
48
+ def contract(contract = nil, &block)
49
+ raise ArgumentError, 'Either contract or block must be provided' unless contract || block
50
+ raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema)
51
+
52
+ Grape::Validations::ContractScope.new(self, contract, &block)
53
+ end
41
54
  end
42
55
  end
43
56
  end