grape 1.3.3 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -2
  3. data/CONTRIBUTING.md +2 -1
  4. data/README.md +135 -23
  5. data/UPGRADING.md +237 -46
  6. data/grape.gemspec +5 -5
  7. data/lib/grape/api/instance.rb +34 -42
  8. data/lib/grape/api.rb +21 -16
  9. data/lib/grape/cookies.rb +2 -0
  10. data/lib/grape/dsl/callbacks.rb +1 -1
  11. data/lib/grape/dsl/desc.rb +3 -5
  12. data/lib/grape/dsl/headers.rb +5 -2
  13. data/lib/grape/dsl/helpers.rb +8 -5
  14. data/lib/grape/dsl/inside_route.rb +72 -53
  15. data/lib/grape/dsl/middleware.rb +4 -4
  16. data/lib/grape/dsl/parameters.rb +11 -7
  17. data/lib/grape/dsl/request_response.rb +9 -6
  18. data/lib/grape/dsl/routing.rb +8 -9
  19. data/lib/grape/dsl/settings.rb +5 -5
  20. data/lib/grape/dsl/validations.rb +18 -1
  21. data/lib/grape/eager_load.rb +1 -1
  22. data/lib/grape/endpoint.rb +29 -42
  23. data/lib/grape/error_formatter/json.rb +2 -6
  24. data/lib/grape/error_formatter/xml.rb +2 -6
  25. data/lib/grape/exceptions/empty_message_body.rb +11 -0
  26. data/lib/grape/exceptions/validation.rb +2 -3
  27. data/lib/grape/exceptions/validation_errors.rb +1 -1
  28. data/lib/grape/formatter/json.rb +1 -0
  29. data/lib/grape/formatter/serializable_hash.rb +2 -1
  30. data/lib/grape/formatter/xml.rb +1 -0
  31. data/lib/grape/locale/en.yml +1 -1
  32. data/lib/grape/middleware/auth/base.rb +3 -3
  33. data/lib/grape/middleware/auth/dsl.rb +7 -1
  34. data/lib/grape/middleware/base.rb +6 -3
  35. data/lib/grape/middleware/error.rb +11 -13
  36. data/lib/grape/middleware/formatter.rb +7 -7
  37. data/lib/grape/middleware/stack.rb +10 -3
  38. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -5
  39. data/lib/grape/middleware/versioner/header.rb +6 -4
  40. data/lib/grape/middleware/versioner/param.rb +1 -0
  41. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  42. data/lib/grape/middleware/versioner/path.rb +2 -0
  43. data/lib/grape/parser/json.rb +1 -1
  44. data/lib/grape/parser/xml.rb +1 -1
  45. data/lib/grape/path.rb +1 -0
  46. data/lib/grape/request.rb +4 -1
  47. data/lib/grape/router/attribute_translator.rb +3 -3
  48. data/lib/grape/router/pattern.rb +1 -1
  49. data/lib/grape/router/route.rb +2 -2
  50. data/lib/grape/router.rb +31 -30
  51. data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
  52. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
  53. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
  54. data/lib/grape/util/base_inheritable.rb +2 -2
  55. data/lib/grape/util/inheritable_setting.rb +1 -3
  56. data/lib/grape/util/lazy_value.rb +4 -2
  57. data/lib/grape/util/strict_hash_configuration.rb +1 -1
  58. data/lib/grape/validations/attributes_iterator.rb +8 -0
  59. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  60. data/lib/grape/validations/params_scope.rb +97 -62
  61. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  62. data/lib/grape/validations/types/custom_type_coercer.rb +16 -3
  63. data/lib/grape/validations/types/dry_type_coercer.rb +1 -1
  64. data/lib/grape/validations/types/invalid_value.rb +24 -0
  65. data/lib/grape/validations/types/json.rb +2 -1
  66. data/lib/grape/validations/types/primitive_coercer.rb +4 -5
  67. data/lib/grape/validations/types.rb +1 -4
  68. data/lib/grape/validations/validator_factory.rb +1 -1
  69. data/lib/grape/validations/validators/all_or_none.rb +8 -5
  70. data/lib/grape/validations/validators/allow_blank.rb +9 -7
  71. data/lib/grape/validations/validators/as.rb +6 -8
  72. data/lib/grape/validations/validators/at_least_one_of.rb +7 -4
  73. data/lib/grape/validations/validators/base.rb +74 -69
  74. data/lib/grape/validations/validators/coerce.rb +63 -76
  75. data/lib/grape/validations/validators/default.rb +36 -34
  76. data/lib/grape/validations/validators/exactly_one_of.rb +9 -6
  77. data/lib/grape/validations/validators/except_values.rb +13 -11
  78. data/lib/grape/validations/validators/multiple_params_base.rb +24 -19
  79. data/lib/grape/validations/validators/mutual_exclusion.rb +8 -5
  80. data/lib/grape/validations/validators/presence.rb +7 -4
  81. data/lib/grape/validations/validators/regexp.rb +8 -5
  82. data/lib/grape/validations/validators/same_as.rb +18 -15
  83. data/lib/grape/validations/validators/values.rb +61 -56
  84. data/lib/grape/validations.rb +6 -0
  85. data/lib/grape/version.rb +1 -1
  86. data/lib/grape.rb +7 -3
  87. data/spec/grape/api/custom_validations_spec.rb +77 -45
  88. data/spec/grape/api/deeply_included_options_spec.rb +3 -3
  89. data/spec/grape/api/defines_boolean_in_params_spec.rb +2 -1
  90. data/spec/grape/api/invalid_format_spec.rb +2 -0
  91. data/spec/grape/api/recognize_path_spec.rb +1 -1
  92. data/spec/grape/api/routes_with_requirements_spec.rb +8 -8
  93. data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +9 -15
  94. data/spec/grape/api_remount_spec.rb +25 -19
  95. data/spec/grape/api_spec.rb +576 -211
  96. data/spec/grape/dsl/callbacks_spec.rb +2 -1
  97. data/spec/grape/dsl/headers_spec.rb +39 -9
  98. data/spec/grape/dsl/helpers_spec.rb +3 -2
  99. data/spec/grape/dsl/inside_route_spec.rb +185 -34
  100. data/spec/grape/dsl/logger_spec.rb +16 -18
  101. data/spec/grape/dsl/middleware_spec.rb +2 -1
  102. data/spec/grape/dsl/parameters_spec.rb +2 -0
  103. data/spec/grape/dsl/request_response_spec.rb +1 -0
  104. data/spec/grape/dsl/routing_spec.rb +10 -7
  105. data/spec/grape/endpoint/declared_spec.rb +848 -0
  106. data/spec/grape/endpoint_spec.rb +77 -589
  107. data/spec/grape/entity_spec.rb +29 -23
  108. data/spec/grape/exceptions/body_parse_errors_spec.rb +3 -0
  109. data/spec/grape/exceptions/invalid_accept_header_spec.rb +61 -22
  110. data/spec/grape/exceptions/validation_errors_spec.rb +13 -10
  111. data/spec/grape/exceptions/validation_spec.rb +5 -3
  112. data/spec/grape/extensions/param_builders/hash_spec.rb +7 -7
  113. data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +8 -8
  114. data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +8 -8
  115. data/spec/grape/integration/rack_sendfile_spec.rb +13 -9
  116. data/spec/grape/loading_spec.rb +8 -8
  117. data/spec/grape/middleware/auth/dsl_spec.rb +15 -6
  118. data/spec/grape/middleware/auth/strategies_spec.rb +61 -21
  119. data/spec/grape/middleware/base_spec.rb +24 -15
  120. data/spec/grape/middleware/error_spec.rb +3 -3
  121. data/spec/grape/middleware/exception_spec.rb +111 -161
  122. data/spec/grape/middleware/formatter_spec.rb +28 -7
  123. data/spec/grape/middleware/globals_spec.rb +7 -4
  124. data/spec/grape/middleware/stack_spec.rb +15 -12
  125. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +2 -1
  126. data/spec/grape/middleware/versioner/header_spec.rb +14 -13
  127. data/spec/grape/middleware/versioner/param_spec.rb +7 -1
  128. data/spec/grape/middleware/versioner/path_spec.rb +5 -1
  129. data/spec/grape/middleware/versioner_spec.rb +1 -1
  130. data/spec/grape/parser_spec.rb +4 -0
  131. data/spec/grape/path_spec.rb +52 -52
  132. data/spec/grape/presenters/presenter_spec.rb +7 -6
  133. data/spec/grape/request_spec.rb +6 -4
  134. data/spec/grape/util/inheritable_setting_spec.rb +7 -7
  135. data/spec/grape/util/inheritable_values_spec.rb +3 -2
  136. data/spec/grape/util/reverse_stackable_values_spec.rb +3 -1
  137. data/spec/grape/util/stackable_values_spec.rb +7 -5
  138. data/spec/grape/validations/instance_behaivour_spec.rb +9 -10
  139. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +14 -3
  140. data/spec/grape/validations/params_scope_spec.rb +72 -10
  141. data/spec/grape/validations/single_attribute_iterator_spec.rb +18 -6
  142. data/spec/grape/validations/types/primitive_coercer_spec.rb +63 -7
  143. data/spec/grape/validations/types_spec.rb +8 -8
  144. data/spec/grape/validations/validators/all_or_none_spec.rb +50 -56
  145. data/spec/grape/validations/validators/allow_blank_spec.rb +136 -140
  146. data/spec/grape/validations/validators/at_least_one_of_spec.rb +50 -56
  147. data/spec/grape/validations/validators/coerce_spec.rb +248 -33
  148. data/spec/grape/validations/validators/default_spec.rb +121 -78
  149. data/spec/grape/validations/validators/exactly_one_of_spec.rb +71 -77
  150. data/spec/grape/validations/validators/except_values_spec.rb +4 -3
  151. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +71 -77
  152. data/spec/grape/validations/validators/presence_spec.rb +16 -1
  153. data/spec/grape/validations/validators/regexp_spec.rb +25 -31
  154. data/spec/grape/validations/validators/same_as_spec.rb +14 -20
  155. data/spec/grape/validations/validators/values_spec.rb +183 -178
  156. data/spec/grape/validations_spec.rb +342 -29
  157. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  158. data/spec/integration/multi_json/json_spec.rb +1 -1
  159. data/spec/integration/multi_xml/xml_spec.rb +1 -1
  160. data/spec/shared/versioning_examples.rb +32 -29
  161. data/spec/spec_helper.rb +12 -12
  162. data/spec/support/basic_auth_encode_helpers.rb +1 -1
  163. data/spec/support/chunks.rb +14 -0
  164. data/spec/support/versioned_helpers.rb +4 -6
  165. metadata +110 -102
data/UPGRADING.md CHANGED
@@ -1,16 +1,209 @@
1
1
  Upgrading Grape
2
2
  ===============
3
3
 
4
+ ### Upgrading to >= 1.6.0
5
+
6
+ #### Parameter renaming with :as
7
+
8
+ Prior to 1.6.0 the [parameter renaming](https://github.com/ruby-grape/grape#renaming) with `:as` was directly touching the request payload ([`#params`](https://github.com/ruby-grape/grape#parameters)) while duplicating the old and the new key to be both available in the hash. This allowed clients to bypass any validation in case they knew the internal name of the parameter. Unfortunately, in combination with [grape-swagger](https://github.com/ruby-grape/grape-swagger) the internal name (name set with `:as`) of the parameters were documented.
9
+
10
+ This behavior was fixed. Parameter renaming is now done when using the [`#declared(params)`](https://github.com/ruby-grape/grape#declared) parameters helper. This stops confusing validation/coercion behavior.
11
+
12
+ Here comes an illustration of the old and new behaviour as code:
13
+
14
+ ```ruby
15
+ # (1) Rename a to b, while client sends +a+
16
+ optional :a, type: Integer, as: :b
17
+ params = { a: 1 }
18
+ declared(params, include_missing: false)
19
+ # expected => { b: 1 }
20
+ # actual => { b: 1 }
21
+
22
+ # (2) Rename a to b, while client sends +b+
23
+ optional :a, type: Integer, as: :b, values: [1, 2, 3]
24
+ params = { b: '5' }
25
+ declared(params, include_missing: false)
26
+ # expected => { } (>= 1.6.0)
27
+ # actual => { b: '5' } (uncasted, unvalidated, <= 1.5.3)
28
+ ```
29
+
30
+ Another implication of this change is the dependent parameter resolution. Prior to 1.6.0 the following code produced a `Grape::Exceptions::UnknownParameter` because `:a` was replaced by `:b`:
31
+
32
+ ```ruby
33
+ params do
34
+ optional :a, as: :b
35
+ given :a do # (<= 1.5.3 you had to reference +:b+ here to make it work)
36
+ requires :c
37
+ end
38
+ end
39
+ ```
40
+
41
+ This code now works without any errors, as the renaming is just an internal behaviour of the `#declared(params)` parameter helper.
42
+
43
+ See [#2189](https://github.com/ruby-grape/grape/pull/2189) for more information.
44
+
45
+ ### Upgrading to >= 1.5.3
46
+
47
+ #### Nil value and coercion
48
+
49
+ Prior to 1.2.5 version passing a `nil` value for a parameter with a custom coercer would invoke the coercer, and not passing a parameter would not invoke it.
50
+ This behavior was not tested or documented. Version 1.3.0 quietly changed this behavior, in that `nil` values skipped the coercion. Version 1.5.3 fixes and documents this as follows:
51
+
52
+ ```ruby
53
+ class Api < Grape::API
54
+ params do
55
+ optional :value, type: Integer, coerce_with: ->(val) { val || 0 }
56
+ end
57
+
58
+ get 'example' do
59
+ params[:my_param]
60
+ end
61
+ get '/example', params: { value: nil }
62
+ # 1.5.2 = nil
63
+ # 1.5.3 = 0
64
+ get '/example', params: {}
65
+ # 1.5.2 = nil
66
+ # 1.5.3 = nil
67
+ end
68
+ ```
69
+ See [#2164](https://github.com/ruby-grape/grape/pull/2164) for more information.
70
+
71
+ ### Upgrading to >= 1.5.1
72
+
73
+ #### Dependent params
74
+
75
+ If you use [dependent params](https://github.com/ruby-grape/grape#dependent-parameters) with
76
+ `Grape::Extensions::Hash::ParamBuilder`, make sure a parameter to be dependent on is set as a Symbol.
77
+ If a String is given, a parameter that other parameters depend on won't be found even if it is present.
78
+
79
+ _Correct_:
80
+ ```ruby
81
+ given :matrix do
82
+ # dependent params
83
+ end
84
+ ```
85
+
86
+ _Wrong_:
87
+ ```ruby
88
+ given 'matrix' do
89
+ # dependent params
90
+ end
91
+ ```
92
+
93
+ ### Upgrading to >= 1.5.0
94
+
95
+ Prior to 1.3.3, the `declared` helper would always return the complete params structure if `include_missing=true` was set. In 1.3.3 a regression was introduced such that a missing Hash with or without nested parameters would always resolve to `{}`.
96
+
97
+ In 1.5.0 this behavior is reverted, so the whole params structure will always be available via `declared`, regardless of whether any params are passed.
98
+
99
+ The following rules now apply to the `declared` helper when params are missing and `include_missing=true`:
100
+
101
+ * Hash params with children will resolve to a Hash with keys for each declared child.
102
+ * Hash params with no children will resolve to `{}`.
103
+ * Set params will resolve to `Set.new`.
104
+ * Array params will resolve to `[]`.
105
+ * All other params will resolve to `nil`.
106
+
107
+ #### Example
108
+
109
+ ```ruby
110
+ class Api < Grape::API
111
+ params do
112
+ optional :outer, type: Hash do
113
+ optional :inner, type: Hash do
114
+ optional :value, type: String
115
+ end
116
+ end
117
+ end
118
+ get 'example' do
119
+ declared(params, include_missing: true)
120
+ end
121
+ end
122
+ ```
123
+
124
+ ```
125
+ get '/example'
126
+ # 1.3.3 = {}
127
+ # 1.5.0 = {outer: {inner: {value:null}}}
128
+ ```
129
+
130
+ For more information see [#2103](https://github.com/ruby-grape/grape/pull/2103).
131
+
4
132
  ### Upgrading to >= 1.4.0
5
133
 
134
+ #### Reworking stream and file and un-deprecating stream like-objects
135
+
136
+ Previously in 0.16 stream-like objects were deprecated. This release restores their functionality for use-cases other than file streaming.
137
+
138
+ This release deprecated `file` in favor of `sendfile` to better document its purpose.
139
+
140
+ To deliver a file via the Sendfile support in your web server and have the Rack::Sendfile middleware enabled. See [`Rack::Sendfile`](https://www.rubydoc.info/gems/rack/Rack/Sendfile).
141
+ ```ruby
142
+ class API < Grape::API
143
+ get '/' do
144
+ sendfile '/path/to/file'
145
+ end
146
+ end
147
+ ```
148
+
149
+ Use `stream` to stream file content in chunks.
150
+
151
+ ```ruby
152
+ class API < Grape::API
153
+ get '/' do
154
+ stream '/path/to/file'
155
+ end
156
+ end
157
+ ```
158
+
159
+ Or use `stream` to stream other kinds of content. In the following example a streamer class
160
+ streams paginated data from a database.
161
+
162
+ ```ruby
163
+ class MyObject
164
+ attr_accessor :result
165
+
166
+ def initialize(query)
167
+ @result = query
168
+ end
169
+
170
+ def each
171
+ yield '['
172
+ # Do paginated DB fetches and return each page formatted
173
+ first = false
174
+ result.find_in_batches do |records|
175
+ yield process_records(records, first)
176
+ first = false
177
+ end
178
+ yield ']'
179
+ end
180
+
181
+ def process_records(records, first)
182
+ buffer = +''
183
+ buffer << ',' unless first
184
+ buffer << records.map(&:to_json).join(',')
185
+ buffer
186
+ end
187
+ end
188
+
189
+ class API < Grape::API
190
+ get '/' do
191
+ stream MyObject.new(Sprocket.all)
192
+ end
193
+ end
194
+ ```
195
+
196
+ ### Upgrading to >= 1.3.3
197
+
6
198
  #### Nil values for structures
7
199
 
8
- Nil values always been a special case when dealing with types especially with the following structures:
9
- - Array
10
- - Hash
11
- - Set
12
-
13
- The behaviour for these structures has change through out the latest releases. For instance:
200
+ Nil values have always been a special case when dealing with types, especially with the following structures:
201
+
202
+ - Array
203
+ - Hash
204
+ - Set
205
+
206
+ The behavior for these structures has changed throughout the latest releases. For example:
14
207
 
15
208
  ```ruby
16
209
  class Api < Grape::API
@@ -26,9 +219,10 @@ class Api < Grape::API
26
219
  # 1.3.2 = nil
27
220
  end
28
221
  ```
222
+
29
223
  For now on, `nil` values stay `nil` values for all types, including arrays, sets and hashes.
30
224
 
31
- If you want to have the same behavior as 1.3.1, apply a `default` validator
225
+ If you want to have the same behavior as 1.3.1, apply a `default` validator:
32
226
 
33
227
  ```ruby
34
228
  class Api < Grape::API
@@ -62,16 +256,15 @@ end
62
256
 
63
257
  ### Upgrading to >= 1.3.0
64
258
 
259
+ You will need to upgrade to this version if you depend on `rack >= 2.1.0`.
260
+
65
261
  #### Ruby
66
262
 
67
263
  After adding dry-types, Ruby 2.4 or newer is required.
68
264
 
69
265
  #### Coercion
70
266
 
71
- [Virtus](https://github.com/solnic/virtus) has been replaced by
72
- [dry-types](https://dry-rb.org/gems/dry-types/1.2/) for parameter
73
- coercion. If your project depends on Virtus outside of Grape, explicitly
74
- add it to your `Gemfile`.
267
+ [Virtus](https://github.com/solnic/virtus) has been replaced by [dry-types](https://dry-rb.org/gems/dry-types/1.2/) for parameter coercion. If your project depends on Virtus outside of Grape, explicitly add it to your `Gemfile`.
75
268
 
76
269
  Here's an example of how to migrate a custom type from Virtus to dry-types:
77
270
 
@@ -98,10 +291,7 @@ To use dry-types, we need to:
98
291
  1. Rename `coerce` to `self.parse`
99
292
  1. Rename `value_coerced?` to `self.parsed?`
100
293
 
101
- The custom type must have a class-level `parse` method to the model. A
102
- class-level `parsed?` is needed if the parsed type differs from the
103
- defined type. In the example below, since `SecureUri` is not the same
104
- as `URI::HTTPS`, `self.parsed?` is needed:
294
+ The custom type must have a class-level `parse` method to the model. A class-level `parsed?` is needed if the parsed type differs from the defined type. In the example below, since `SecureUri` is not the same as `URI::HTTPS`, `self.parsed?` is needed:
105
295
 
106
296
  ```ruby
107
297
  # New dry-types parser
@@ -120,21 +310,31 @@ params do
120
310
  end
121
311
  ```
122
312
 
313
+ #### Coercing to `FalseClass` or `TrueClass` no longer works
314
+
315
+ Previous Grape versions allowed this, though it wasn't documented:
316
+
317
+ ```ruby
318
+ requires :true_value, type: TrueClass
319
+ requires :bool_value, types: [FalseClass, TrueClass]
320
+ ```
321
+
322
+ This is no longer supported, if you do this, your values will never be valid. Instead you should do this:
323
+
324
+ ```ruby
325
+ requires :true_value, type: Boolean # in your endpoint you should validate if this is actually `true`
326
+ requires :bool_value, type: Boolean
327
+ ```
328
+
123
329
  #### Ensure that Array types have explicit coercions
124
330
 
125
- Unlike Virtus, dry-types does not perform any implict coercions. If you
126
- have any uses of `Array[String]`, `Array[Integer]`, etc. be sure they
127
- use a `coerce_with` block. For example:
331
+ Unlike Virtus, dry-types does not perform any implict coercions. If you have any uses of `Array[String]`, `Array[Integer]`, etc. be sure they use a `coerce_with` block. For example:
128
332
 
129
333
  ```ruby
130
334
  requires :values, type: Array[String]
131
335
  ```
132
336
 
133
- It's quite common to pass a comma-separated list, such as `tag1,tag2` as
134
- `values`. Previously Virtus would implicitly coerce this to
135
- `Array(values)` so that `["tag1,tag2"]` would pass the type checks, but
136
- with `dry-types` the values are no longer coerced for you. To fix this,
137
- you might do:
337
+ It's quite common to pass a comma-separated list, such as `tag1,tag2` as `values`. Previously Virtus would implicitly coerce this to `Array(values)` so that `["tag1,tag2"]` would pass the type checks, but with `dry-types` the values are no longer coerced for you. To fix this, you might do:
138
338
 
139
339
  ```ruby
140
340
  requires :values, type: Array[String], coerce_with: ->(val) { val.split(',').map(&:strip) }
@@ -201,12 +401,9 @@ In order to make obtaining the name of a mounted class simpler, we've delegated
201
401
 
202
402
  ##### Patching the class
203
403
 
204
- In an effort to make APIs re-mountable, The class `Grape::API` no longer refers to an API instance,
205
- rather, what used to be `Grape::API` is now `Grape::API::Instance` and `Grape::API` was replaced
206
- with a class that can contain several instances of `Grape::API`.
404
+ In an effort to make APIs re-mountable, The class `Grape::API` no longer refers to an API instance, rather, what used to be `Grape::API` is now `Grape::API::Instance` and `Grape::API` was replaced with a class that can contain several instances of `Grape::API`.
207
405
 
208
- This changes were done in such a way that no code-changes should be required.
209
- However, if experiencing problems, or relying on private methods and internal behaviour too deeply, it is possible to restore the prior behaviour by replacing the references from `Grape::API` to `Grape::API::Instance`.
406
+ This changes were done in such a way that no code-changes should be required. However, if experiencing problems, or relying on private methods and internal behaviour too deeply, it is possible to restore the prior behaviour by replacing the references from `Grape::API` to `Grape::API::Instance`.
210
407
 
211
408
  Note, this is particularly relevant if you are opening the class `Grape::API` for modification.
212
409
 
@@ -229,15 +426,20 @@ end
229
426
 
230
427
  After the patch, the mounted API is no longer a Named class inheriting from `Grape::API`, it is an anonymous class
231
428
  which inherit from `Grape::API::Instance`.
429
+
232
430
  What this means in practice, is:
431
+
233
432
  - Generally: you can access the named class from the instance calling the getter `base`.
234
- - In particular: If you need the `name`, you can use `base`.`name`
433
+ - In particular: If you need the `name`, you can use `base`.`name`.
235
434
 
236
435
  **Deprecated**
436
+
237
437
  ```ruby
238
438
  payload[:endpoint].options[:for].name
239
439
  ```
440
+
240
441
  **New**
442
+
241
443
  ```ruby
242
444
  payload[:endpoint].options[:for].base.name
243
445
  ```
@@ -328,8 +530,7 @@ See [#1610](https://github.com/ruby-grape/grape/pull/1610) for more information.
328
530
 
329
531
  #### The `except`, `except_message`, and `proc` options of the `values` validator are deprecated.
330
532
 
331
- The new `except_values` validator should be used in place of the `except` and `except_message` options of
332
- the `values` validator.
533
+ The new `except_values` validator should be used in place of the `except` and `except_message` options of the `values` validator.
333
534
 
334
535
  Arity one Procs may now be used directly as the `values` option to explicitly test param values.
335
536
 
@@ -405,9 +606,7 @@ get '/example' #=> before: 405, after: 404
405
606
 
406
607
  #### Removed param processing from built-in OPTIONS handler
407
608
 
408
- When a request is made to the built-in `OPTIONS` handler, only the `before` and `after`
409
- callbacks associated with the resource will be run. The `before_validation` and
410
- `after_validation` callbacks and parameter validations will be skipped.
609
+ When a request is made to the built-in `OPTIONS` handler, only the `before` and `after` callbacks associated with the resource will be run. The `before_validation` and `after_validation` callbacks and parameter validations will be skipped.
411
610
 
412
611
  See [#1505](https://github.com/ruby-grape/grape/pull/1505) for more information.
413
612
 
@@ -428,8 +627,7 @@ See [#1510](https://github.com/ruby-grape/grape/pull/1510) for more information.
428
627
 
429
628
  #### The default status code for DELETE is now 204 instead of 200.
430
629
 
431
- Breaking change: Sets the default response status code for a delete request to 204.
432
- A status of 204 makes the response more distinguishable and therefore easier to handle on the client side, particularly because a DELETE request typically returns an empty body as the resource was deleted or voided.
630
+ Breaking change: Sets the default response status code for a delete request to 204. A status of 204 makes the response more distinguishable and therefore easier to handle on the client side, particularly because a DELETE request typically returns an empty body as the resource was deleted or voided.
433
631
 
434
632
  To achieve the old behavior, one has to set it explicitly:
435
633
  ```ruby
@@ -607,18 +805,14 @@ See [#1114](https://github.com/ruby-grape/grape/pull/1114) for more information.
607
805
 
608
806
  #### Bypasses formatters when status code indicates no content
609
807
 
610
- To be consistent with rack and it's handling of standard responses
611
- associated with no content, both default and custom formatters will now
808
+ To be consistent with rack and it's handling of standard responses associated with no content, both default and custom formatters will now
612
809
  be bypassed when processing responses for status codes defined [by rack](https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L567)
613
810
 
614
811
  See [#1190](https://github.com/ruby-grape/grape/pull/1190) for more information.
615
812
 
616
813
  #### Redirects respond as plain text with message
617
814
 
618
- `#redirect` now uses `text/plain` regardless of whether that format has
619
- been enabled. This prevents formatters from attempting to serialize the
620
- message body and allows for a descriptive message body to be provided - and
621
- optionally overridden - that better fulfills the theme of the HTTP spec.
815
+ `#redirect` now uses `text/plain` regardless of whether that format has been enabled. This prevents formatters from attempting to serialize the message body and allows for a descriptive message body to be provided - and optionally overridden - that better fulfills the theme of the HTTP spec.
622
816
 
623
817
  See [#1194](https://github.com/ruby-grape/grape/pull/1194) for more information.
624
818
 
@@ -652,10 +846,7 @@ end
652
846
 
653
847
  See [#1029](https://github.com/ruby-grape/grape/pull/1029) for more information.
654
848
 
655
- There is a known issue because of this change. When Grape is used with an older
656
- than 1.2.4 version of [warden](https://github.com/hassox/warden) there may be raised
657
- the following exception having the [rack-mount](https://github.com/jm/rack-mount) gem's
658
- lines as last ones in the backtrace:
849
+ There is a known issue because of this change. When Grape is used with an older than 1.2.4 version of [warden](https://github.com/hassox/warden) there may be raised the following exception having the [rack-mount](https://github.com/jm/rack-mount) gem's lines as last ones in the backtrace:
659
850
 
660
851
  ```
661
852
  NoMethodError: undefined method `[]' for nil:NilClass
data/grape.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
4
4
  require 'grape/version'
5
5
 
6
6
  Gem::Specification.new do |s|
@@ -14,10 +14,10 @@ Gem::Specification.new do |s|
14
14
  s.description = 'A Ruby framework for rapid API development with great conventions.'
15
15
  s.license = 'MIT'
16
16
  s.metadata = {
17
- 'bug_tracker_uri' => 'https://github.com/ruby-grape/grape/issues',
18
- 'changelog_uri' => "https://github.com/ruby-grape/grape/blob/v#{s.version}/CHANGELOG.md",
17
+ 'bug_tracker_uri' => 'https://github.com/ruby-grape/grape/issues',
18
+ 'changelog_uri' => "https://github.com/ruby-grape/grape/blob/v#{s.version}/CHANGELOG.md",
19
19
  'documentation_uri' => "https://www.rubydoc.info/gems/grape/#{s.version}",
20
- 'source_code_uri' => "https://github.com/ruby-grape/grape/tree/v#{s.version}"
20
+ 'source_code_uri' => "https://github.com/ruby-grape/grape/tree/v#{s.version}"
21
21
  }
22
22
 
23
23
  s.add_runtime_dependency 'activesupport'
@@ -32,5 +32,5 @@ Gem::Specification.new do |s|
32
32
  s.files += Dir['lib/**/*']
33
33
  s.test_files = Dir['spec/**/*']
34
34
  s.require_paths = ['lib']
35
- s.required_ruby_version = '>= 2.4.0'
35
+ s.required_ruby_version = '>= 2.5.0'
36
36
  end
@@ -10,12 +10,11 @@ module Grape
10
10
  include Grape::DSL::API
11
11
 
12
12
  class << self
13
- attr_reader :instance
14
- attr_reader :base
13
+ attr_reader :instance, :base
15
14
  attr_accessor :configuration
16
15
 
17
16
  def given(conditional_option, &block)
18
- evaluate_as_instance_with_configuration(block, lazy: true) if conditional_option && block_given?
17
+ evaluate_as_instance_with_configuration(block, lazy: true) if conditional_option && block
19
18
  end
20
19
 
21
20
  def mounted(&block)
@@ -28,7 +27,7 @@ module Grape
28
27
  end
29
28
 
30
29
  def to_s
31
- (base && base.to_s) || super
30
+ base&.to_s || super
32
31
  end
33
32
 
34
33
  def base_instance?
@@ -82,6 +81,7 @@ module Grape
82
81
 
83
82
  def compile!
84
83
  return if instance
84
+
85
85
  LOCK.synchronize { compile unless instance }
86
86
  end
87
87
 
@@ -103,7 +103,7 @@ module Grape
103
103
  def nest(*blocks, &block)
104
104
  blocks.reject!(&:nil?)
105
105
  if blocks.any?
106
- evaluate_as_instance_with_configuration(block) if block_given?
106
+ evaluate_as_instance_with_configuration(block) if block
107
107
  blocks.each { |b| evaluate_as_instance_with_configuration(b) }
108
108
  reset_validations!
109
109
  else
@@ -114,9 +114,7 @@ module Grape
114
114
  def evaluate_as_instance_with_configuration(block, lazy: false)
115
115
  lazy_block = Grape::Util::LazyBlock.new do |configuration|
116
116
  value_for_configuration = configuration
117
- if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy?
118
- self.configuration = value_for_configuration.evaluate
119
- end
117
+ self.configuration = value_for_configuration.evaluate if value_for_configuration.respond_to?(:lazy?) && value_for_configuration.lazy?
120
118
  response = instance_eval(&block)
121
119
  self.configuration = value_for_configuration
122
120
  response
@@ -179,7 +177,8 @@ module Grape
179
177
  # X-Cascade. Default :cascade is true.
180
178
  def cascade?
181
179
  return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.key?(:cascade)
182
- return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options) && self.class.namespace_inheritable(:version_options).key?(:cascade)
180
+ return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options)&.key?(:cascade)
181
+
183
182
  true
184
183
  end
185
184
 
@@ -192,47 +191,23 @@ module Grape
192
191
  # will return an HTTP 405 response for any HTTP method that the resource
193
192
  # cannot handle.
194
193
  def add_head_not_allowed_methods_and_options_methods
195
- routes_map = {}
196
-
197
- self.class.endpoints.each do |endpoint|
198
- routes = endpoint.routes
199
- routes.each do |route|
200
- # using the :any shorthand produces [nil] for route methods, substitute all manually
201
- route_key = route.pattern.to_regexp
202
- routes_map[route_key] ||= {}
203
- route_settings = routes_map[route_key]
204
- route_settings[:pattern] = route.pattern
205
- route_settings[:requirements] = route.requirements
206
- route_settings[:path] = route.origin
207
- route_settings[:methods] ||= []
208
- if route.request_method == '*' || route_settings[:methods].include?('*')
209
- route_settings[:methods] = Grape::Http::Headers::SUPPORTED_METHODS
210
- else
211
- route_settings[:methods] << route.request_method
212
- end
213
- route_settings[:endpoint] = route.app
214
- end
215
- end
216
-
194
+ versioned_route_configs = collect_route_config_per_pattern
217
195
  # The paths we collected are prepared (cf. Path#prepare), so they
218
196
  # contain already versioning information when using path versioning.
219
197
  # Disable versioning so adding a route won't prepend versioning
220
198
  # informations again.
221
199
  without_root_prefix do
222
200
  without_versioning do
223
- routes_map.each_value do |config|
224
- methods = config[:methods]
225
- allowed_methods = methods.dup
201
+ versioned_route_configs.each do |config|
202
+ next if config[:options][:matching_wildchar]
226
203
 
227
- unless self.class.namespace_inheritable(:do_not_route_head)
228
- allowed_methods |= [Grape::Http::Headers::HEAD] if allowed_methods.include?(Grape::Http::Headers::GET)
229
- end
204
+ allowed_methods = config[:methods].dup
205
+
206
+ allowed_methods |= [Grape::Http::Headers::HEAD] if !self.class.namespace_inheritable(:do_not_route_head) && allowed_methods.include?(Grape::Http::Headers::GET)
230
207
 
231
208
  allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods)
232
209
 
233
- unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS)
234
- config[:endpoint].options[:options_route_enabled] = true
235
- end
210
+ config[:endpoint].options[:options_route_enabled] = true unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS)
236
211
 
237
212
  attributes = config.merge(allowed_methods: allowed_methods, allow_header: allow_header)
238
213
  generate_not_allowed_method(config[:pattern], **attributes)
@@ -241,6 +216,25 @@ module Grape
241
216
  end
242
217
  end
243
218
 
219
+ def collect_route_config_per_pattern
220
+ all_routes = self.class.endpoints.map(&:routes).flatten
221
+ routes_by_regexp = all_routes.group_by { |route| route.pattern.to_regexp }
222
+
223
+ # Build the configuration based on the first endpoint and the collection of methods supported.
224
+ routes_by_regexp.values.map do |routes|
225
+ last_route = routes.last # Most of the configuration is taken from the last endpoint
226
+ matching_wildchar = routes.any? { |route| route.request_method == '*' }
227
+ {
228
+ options: { matching_wildchar: matching_wildchar },
229
+ pattern: last_route.pattern,
230
+ requirements: last_route.requirements,
231
+ path: last_route.origin,
232
+ endpoint: last_route.app,
233
+ methods: matching_wildchar ? Grape::Http::Headers::SUPPORTED_METHODS : routes.map(&:request_method)
234
+ }
235
+ end
236
+ end
237
+
244
238
  # Generate a route that returns an HTTP 405 response for a user defined
245
239
  # path on methods not specified
246
240
  def generate_not_allowed_method(pattern, allowed_methods: [], **attributes)
@@ -251,8 +245,6 @@ module Grape
251
245
  Grape::Http::Headers::SUPPORTED_METHODS_WITHOUT_OPTIONS
252
246
  end
253
247
  not_allowed_methods = supported_methods - allowed_methods
254
- return if not_allowed_methods.empty?
255
-
256
248
  @router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes)
257
249
  end
258
250
 
data/lib/grape/api.rb CHANGED
@@ -10,6 +10,18 @@ module Grape
10
10
  # Class methods that we want to call on the API rather than on the API object
11
11
  NON_OVERRIDABLE = (Class.new.methods + %i[call call! configuration compile! inherited]).freeze
12
12
 
13
+ class Boolean
14
+ def self.build(val)
15
+ return nil if val != true && val != false
16
+
17
+ new
18
+ end
19
+ end
20
+
21
+ class Instance
22
+ Boolean = Grape::API::Boolean
23
+ end
24
+
13
25
  class << self
14
26
  attr_accessor :base_instance, :instances
15
27
 
@@ -20,17 +32,18 @@ module Grape
20
32
 
21
33
  # When inherited, will create a list of all instances (times the API was mounted)
22
34
  # It will listen to the setup required to mount that endpoint, and replicate it on any new instance
23
- def inherited(api, base_instance_parent = Grape::API::Instance)
24
- api.initial_setup(base_instance_parent)
35
+ def inherited(api)
36
+ super
37
+
38
+ api.initial_setup(Grape::API == self ? Grape::API::Instance : @base_instance)
25
39
  api.override_all_methods!
26
- make_inheritable(api)
27
40
  end
28
41
 
29
42
  # Initialize the instance variables on the remountable class, and the base_instance
30
43
  # an instance that will be used to create the set up but will not be mounted
31
44
  def initial_setup(base_instance_parent)
32
45
  @instances = []
33
- @setup = []
46
+ @setup = Set.new
34
47
  @base_parent = base_instance_parent
35
48
  @base_instance = mount_instance
36
49
  end
@@ -68,15 +81,6 @@ module Grape
68
81
  instance_for_rack.call(*args, &block)
69
82
  end
70
83
 
71
- # Allows an API to itself be inheritable:
72
- def make_inheritable(api)
73
- # When a child API inherits from a parent API.
74
- def api.inherited(child_api)
75
- # The instances of the child API inherit from the instances of the parent API
76
- Grape::API.inherited(child_api, base_instance)
77
- end
78
- end
79
-
80
84
  # Alleviates problems with autoloading by tring to search for the constant
81
85
  def const_missing(*args)
82
86
  if base_instance.const_defined?(*args)
@@ -87,10 +91,10 @@ module Grape
87
91
  end
88
92
 
89
93
  # The remountable class can have a configuration hash to provide some dynamic class-level variables.
90
- # For instance, a descripcion could be done using: `desc configuration[:description]` if it may vary
94
+ # For instance, a description could be done using: `desc configuration[:description]` if it may vary
91
95
  # depending on where the endpoint is mounted. Use with care, if you find yourself using configuration
92
96
  # too much, you may actually want to provide a new API rather than remount it.
93
- def mount_instance(opts = {})
97
+ def mount_instance(**opts)
94
98
  instance = Class.new(@base_parent)
95
99
  instance.configuration = Grape::Util::EndpointConfiguration.new(opts[:configuration] || {})
96
100
  instance.base = self
@@ -141,7 +145,7 @@ module Grape
141
145
  # Adds a new stage to the set up require to get a Grape::API up and running
142
146
  def add_setup(method, *args, &block)
143
147
  setup_step = { method: method, args: args, block: block }
144
- @setup << setup_step
148
+ @setup += [setup_step]
145
149
  last_response = nil
146
150
  @instances.each do |instance|
147
151
  last_response = replay_step_on(instance, setup_step)
@@ -151,6 +155,7 @@ module Grape
151
155
 
152
156
  def replay_step_on(instance, setup_step)
153
157
  return if skip_immediate_run?(instance, setup_step[:args])
158
+
154
159
  args = evaluate_arguments(instance.configuration, *setup_step[:args])
155
160
  response = instance.send(setup_step[:method], *args, &setup_step[:block])
156
161
  if skip_immediate_run?(instance, [response])
data/lib/grape/cookies.rb CHANGED
@@ -33,9 +33,11 @@ module Grape
33
33
  @cookies.each(&block)
34
34
  end
35
35
 
36
+ # rubocop:disable Layout/SpaceBeforeBrackets
36
37
  def delete(name, **opts)
37
38
  options = opts.merge(value: 'deleted', expires: Time.at(0))
38
39
  self.[]=(name, options)
39
40
  end
41
+ # rubocop:enable Layout/SpaceBeforeBrackets
40
42
  end
41
43
  end
@@ -59,7 +59,7 @@ module Grape
59
59
  # end
60
60
  # end
61
61
  #
62
- # This will make sure that the ApiLogger is opened and close around every
62
+ # This will make sure that the ApiLogger is opened and closed around every
63
63
  # request
64
64
  # @param ensured_block [Proc] The block to be executed after every api_call
65
65
  def finally(&block)