grape 1.3.0 → 1.3.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +4 -6
  4. data/UPGRADING.md +61 -16
  5. data/lib/grape/api/instance.rb +11 -7
  6. data/lib/grape/api.rb +2 -2
  7. data/lib/grape/content_types.rb +34 -0
  8. data/lib/grape/dsl/helpers.rb +1 -1
  9. data/lib/grape/dsl/inside_route.rb +10 -9
  10. data/lib/grape/dsl/parameters.rb +4 -4
  11. data/lib/grape/dsl/routing.rb +6 -4
  12. data/lib/grape/exceptions/base.rb +0 -4
  13. data/lib/grape/exceptions/validation_errors.rb +11 -12
  14. data/lib/grape/http/headers.rb +26 -0
  15. data/lib/grape/middleware/base.rb +1 -3
  16. data/lib/grape/middleware/stack.rb +2 -1
  17. data/lib/grape/middleware/versioner/header.rb +4 -4
  18. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  19. data/lib/grape/middleware/versioner/path.rb +1 -1
  20. data/lib/grape/namespace.rb +12 -2
  21. data/lib/grape/path.rb +13 -3
  22. data/lib/grape/request.rb +12 -7
  23. data/lib/grape/router/pattern.rb +17 -16
  24. data/lib/grape/router/route.rb +4 -5
  25. data/lib/grape/router.rb +24 -14
  26. data/lib/grape/util/base_inheritable.rb +13 -6
  27. data/lib/grape/util/cache.rb +20 -0
  28. data/lib/grape/util/lazy_object.rb +43 -0
  29. data/lib/grape/util/reverse_stackable_values.rb +2 -0
  30. data/lib/grape/util/stackable_values.rb +7 -20
  31. data/lib/grape/validations/params_scope.rb +1 -1
  32. data/lib/grape/validations/types/build_coercer.rb +4 -3
  33. data/lib/grape/validations/types/custom_type_coercer.rb +1 -1
  34. data/lib/grape/validations/types/file.rb +15 -12
  35. data/lib/grape/validations/types/json.rb +40 -36
  36. data/lib/grape/validations/types/primitive_coercer.rb +6 -3
  37. data/lib/grape/validations/types.rb +6 -5
  38. data/lib/grape/validations/validators/coerce.rb +14 -12
  39. data/lib/grape/validations/validators/exactly_one_of.rb +4 -2
  40. data/lib/grape/validations/validators/regexp.rb +1 -1
  41. data/lib/grape/version.rb +1 -1
  42. data/lib/grape.rb +2 -3
  43. data/spec/grape/api_spec.rb +7 -6
  44. data/spec/grape/exceptions/validation_errors_spec.rb +2 -2
  45. data/spec/grape/middleware/formatter_spec.rb +2 -2
  46. data/spec/grape/middleware/stack_spec.rb +9 -0
  47. data/spec/grape/path_spec.rb +4 -4
  48. data/spec/grape/validations/instance_behaivour_spec.rb +1 -1
  49. data/spec/grape/validations/types/primitive_coercer_spec.rb +75 -0
  50. data/spec/grape/validations/types_spec.rb +1 -1
  51. data/spec/grape/validations/validators/coerce_spec.rb +160 -78
  52. data/spec/grape/validations/validators/exactly_one_of_spec.rb +12 -12
  53. data/spec/grape/validations_spec.rb +8 -12
  54. data/spec/spec_helper.rb +3 -0
  55. data/spec/support/eager_load.rb +19 -0
  56. metadata +12 -6
  57. data/lib/grape/util/content_types.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 826e1365751c85c3f1f7d433e842a76c8a7f5dc6c4a1b13128679e4d56b2ccf5
4
- data.tar.gz: af8aef528c40764a53a3e397c306835745943a1d376ab3738bf7aa2547ef75d1
3
+ metadata.gz: a1ba205b72fd823b11e700753891784fdfbd6791f591b790a99ac3433cb8fe23
4
+ data.tar.gz: '09e64a0d93415b28fdb179fbb5d7ef0d03e00dcb976fd885c0c655d6ac041345'
5
5
  SHA512:
6
- metadata.gz: 0af9a855214f1fbacd92403111aa1f4761b7257adf6ca9dbd6ab4e1cb889469b76a489971d058a58e98d3bf6906dacb67eb8a5eae11aad16782f318b3453988b
7
- data.tar.gz: 25004acd2431ab7cd174466e82ca5240b888ca045036227909ed9c8c886cd8706f1d3b696f4b1ede8eaa69c68c156fe064557afea8cdf53c9d39b136db7ae4b1
6
+ metadata.gz: 06fcd2157bcfcc6e71876df09b34a318c600761b79fb99ad77739521d056950ee8391dfd050efd43618f7f99f11990623e276b32d15a27e92015ec3a56c6c08b
7
+ data.tar.gz: 5463bbd0347950765c9d92e859965e90dd69d7d1f781fd188386cf39597c196f5ba2dc697dbee4f896a70f6498c1e43034a33db8460ea9a719218a7d5df46d6f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,41 @@
1
+ ### 1.3.2 (2020/04/12)
2
+
3
+ #### Features
4
+ * [#2020](https://github.com/ruby-grape/grape/pull/2020): Reduce array allocation - [@ericproulx](https://github.com/ericproulx).
5
+ * [#2015](https://github.com/ruby-grape/grape/pull/2014): Reduce MatchData allocation - [@ericproulx](https://github.com/ericproulx).
6
+ * [#2014](https://github.com/ruby-grape/grape/pull/2014): Reduce total allocated arrays - [@ericproulx](https://github.com/ericproulx).
7
+ * [#2011](https://github.com/ruby-grape/grape/pull/2011): Reduce total retained regexes - [@ericproulx](https://github.com/ericproulx).
8
+
9
+ #### Fixes
10
+
11
+ * [#2033](https://github.com/ruby-grape/grape/pull/2033): Ensure `Float` params are correctly coerced to `BigDecimal` - [@tlconnor](https://github.com/tlconnor).
12
+ * [#2031](https://github.com/ruby-grape/grape/pull/2031): Fix a regression with an array of a custom type - [@dnesteryuk](https://github.com/dnesteryuk).
13
+ * [#2026](https://github.com/ruby-grape/grape/pull/2026): Fix a regression in `coerce_with` when coercion returns `nil` - [@misdoro](https://github.com/misdoro).
14
+ * [#2025](https://github.com/ruby-grape/grape/pull/2025): Fix Decimal type category - [@kdoya](https://github.com/kdoya).
15
+ * [#2019](https://github.com/ruby-grape/grape/pull/2019): Avoid coercing parameter with multiple types to an empty Array - [@stanhu](https://github.com/stanhu).
16
+
17
+ ### 1.3.1 (2020/03/11)
18
+
19
+ #### Features
20
+
21
+ * [#2005](https://github.com/ruby-grape/grape/pull/2005): Content types registrable - [@ericproulx](https://github.com/ericproulx).
22
+ * [#2003](https://github.com/ruby-grape/grape/pull/2003): Upgraded Rubocop to 0.80.1 - [@ericproulx](https://github.com/ericproulx).
23
+ * [#2002](https://github.com/ruby-grape/grape/pull/2002): Objects allocation optimization (lazy_lookup) - [@ericproulx](https://github.com/ericproulx).
24
+
25
+ #### Fixes
26
+
27
+ * [#2006](https://github.com/ruby-grape/grape/pull/2006): Fix explicit rescue StandardError - [@ericproulx](https://github.com/ericproulx).
28
+ * [#2004](https://github.com/ruby-grape/grape/pull/2004): Rubocop fixes - [@ericproulx](https://github.com/ericproulx).
29
+ * [#1995](https://github.com/ruby-grape/grape/pull/1995): Fix: "undefined instance variables" and "method redefined" warnings - [@nbeyer](https://github.com/nbeyer).
30
+ * [#1994](https://github.com/ruby-grape/grape/pull/1993): Fix typos in README - [@bellmyer](https://github.com/bellmyer).
31
+ * [#1993](https://github.com/ruby-grape/grape/pull/1993): Lazy join allow header - [@ericproulx](https://github.com/ericproulx).
32
+ * [#1987](https://github.com/ruby-grape/grape/pull/1987): Re-add exactly_one_of mutually exclusive error message - [@ZeroInputCtrl](https://github.com/ZeroInputCtrl).
33
+ * [#1977](https://github.com/ruby-grape/grape/pull/1977): Skip validation for a file if it is optional and nil - [@dnesteryuk](https://github.com/dnesteryuk).
34
+ * [#1976](https://github.com/ruby-grape/grape/pull/1976): Ensure classes/modules listed for autoload really exist - [@dnesteryuk](https://github.com/dnesteryuk).
35
+ * [#1971](https://github.com/ruby-grape/grape/pull/1971): Fix BigDecimal coercion - [@FlickStuart](https://github.com/FlickStuart).
36
+ * [#1968](https://github.com/ruby-grape/grape/pull/1968): Fix args forwarding in Grape::Middleware::Stack#merge_with for ruby 2.7.0 - [@dm1try](https://github.com/dm1try).
37
+ * [#1988](https://github.com/ruby-grape/grape/pull/1988): Refactored the full_messages method and stop overriding full_message - [@hosseintoussi](https://github.com/hosseintoussi).
38
+
1
39
  ### 1.3.0 (2020/01/11)
2
40
 
3
41
  #### Features
data/README.md CHANGED
@@ -154,8 +154,7 @@ content negotiation, versioning and much more.
154
154
 
155
155
  ## Stable Release
156
156
 
157
- You're reading the documentation for the stable release of Grape, **1.3.0**.
158
- Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version.
157
+ You're reading the documentation for the stable release of Grape, 1.3.2.
159
158
 
160
159
  ## Project Resources
161
160
 
@@ -1722,7 +1721,7 @@ params do
1722
1721
  end
1723
1722
  ```
1724
1723
 
1725
- Every validation will have it's own instance of the validator, which means that the validator can have a state.
1724
+ Every validation will have its own instance of the validator, which means that the validator can have a state.
1726
1725
 
1727
1726
  ### Validation Errors
1728
1727
 
@@ -3188,14 +3187,13 @@ applies to the current namespace and any children, but not parents.
3188
3187
  ```ruby
3189
3188
  http_basic do |username, password|
3190
3189
  # verify user's password here
3191
- { 'test' => 'password1' }[username] == password
3190
+ # IMPORTANT: make sure you use a comparison method which isn't prone to a timing attack
3192
3191
  end
3193
3192
  ```
3194
3193
 
3195
3194
  ```ruby
3196
3195
  http_digest({ realm: 'Test Api', opaque: 'app secret' }) do |username|
3197
3196
  # lookup the user's password here
3198
- { 'user1' => 'password1' }[username]
3199
3197
  end
3200
3198
  ```
3201
3199
 
@@ -3301,7 +3299,7 @@ end
3301
3299
 
3302
3300
  Blocks can be executed before or after every API call, using `before`, `after`,
3303
3301
  `before_validation` and `after_validation`.
3304
- If the API fails the `after` call will not be trigered, if you need code to execute for sure
3302
+ If the API fails the `after` call will not be triggered, if you need code to execute for sure
3305
3303
  use the `finally`.
3306
3304
 
3307
3305
  Before and after callbacks execute in the following order:
data/UPGRADING.md CHANGED
@@ -9,38 +9,83 @@ After adding dry-types, Ruby 2.4 or newer is required.
9
9
 
10
10
  #### Coercion
11
11
 
12
- [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, explicitly add it to your `Gemfile`. Also, if Virtus is used for defining custom types
12
+ [Virtus](https://github.com/solnic/virtus) has been replaced by
13
+ [dry-types](https://dry-rb.org/gems/dry-types/1.2/) for parameter
14
+ coercion. If your project depends on Virtus outside of Grape, explicitly
15
+ add it to your `Gemfile`.
16
+
17
+ Here's an example of how to migrate a custom type from Virtus to dry-types:
13
18
 
14
19
  ```ruby
15
- class User
16
- include Virtus.model
20
+ # Legacy Grape parser
21
+ class SecureUriType < Virtus::Attribute
22
+ def coerce(input)
23
+ URI.parse value
24
+ end
17
25
 
18
- attribute :id, Integer
19
- attribute :name, String
26
+ def value_coerced?(input)
27
+ value.is_a? String
28
+ end
20
29
  end
21
30
 
22
- # somewhere in your API
23
31
  params do
24
- requires :user, type: User
32
+ requires :secure_uri, type: SecureUri
25
33
  end
26
34
  ```
27
35
 
28
- Add a class-level `parse` method to the model:
36
+ To use dry-types, we need to:
29
37
 
30
- ```ruby
31
- class User
32
- include Virtus.model
38
+ 1. Remove the inheritance of `Virtus::Attribute`
39
+ 1. Rename `coerce` to `self.parse`
40
+ 1. Rename `value_coerced?` to `self.parsed?`
33
41
 
34
- attribute :id, Integer
35
- attribute :name, String
42
+ The custom type must have a class-level `parse` method to the model. A
43
+ class-level `parsed?` is needed if the parsed type differs from the
44
+ defined type. In the example below, since `SecureUri` is not the same
45
+ as `URI::HTTPS`, `self.parsed?` is needed:
36
46
 
37
- def self.parse(attrs)
38
- new(attrs)
47
+ ```ruby
48
+ # New dry-types parser
49
+ class SecureUri
50
+ def self.parse(value)
51
+ URI.parse value
52
+ end
53
+
54
+ def self.parsed?(value)
55
+ value.is_a? URI::HTTPS
39
56
  end
40
57
  end
58
+
59
+ params do
60
+ requires :secure_uri, type: SecureUri
61
+ end
62
+ ```
63
+
64
+ #### Ensure that Array types have explicit coercions
65
+
66
+ Unlike Virtus, dry-types does not perform any implict coercions. If you
67
+ have any uses of `Array[String]`, `Array[Integer]`, etc. be sure they
68
+ use a `coerce_with` block. For example:
69
+
70
+ ```ruby
71
+ requires :values, type: Array[String]
41
72
  ```
42
73
 
43
- Custom types which don't depend on Virtus don't require any changes.
74
+ It's quite common to pass a comma-separated list, such as `tag1,tag2` as
75
+ `values`. Previously Virtus would implicitly coerce this to
76
+ `Array(values)` so that `["tag1,tag2"]` would pass the type checks, but
77
+ with `dry-types` the values are no longer coerced for you. To fix this,
78
+ you might do:
79
+
80
+ ```ruby
81
+ requires :values, type: Array[String], coerce_with: ->(val) { val.split(',').map(&:strip) }
82
+ ```
83
+
84
+ Likewise, for `Array[Integer]`, you might do:
85
+
86
+ ```ruby
87
+ requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(',').map(&:strip).map(&:to_i) }
88
+ ```
44
89
 
45
90
  For more information see [#1920](https://github.com/ruby-grape/grape/pull/1920).
46
91
 
@@ -74,7 +74,7 @@ module Grape
74
74
  # (see #cascade?)
75
75
  def cascade(value = nil)
76
76
  if value.nil?
77
- inheritable_setting.namespace_inheritable.keys.include?(:cascade) ? !namespace_inheritable(:cascade).nil? : true
77
+ inheritable_setting.namespace_inheritable.key?(:cascade) ? !namespace_inheritable(:cascade).nil? : true
78
78
  else
79
79
  namespace_inheritable(:cascade, value)
80
80
  end
@@ -178,7 +178,7 @@ module Grape
178
178
  # errors from reaching upstream. This is effectivelly done by unsetting
179
179
  # X-Cascade. Default :cascade is true.
180
180
  def cascade?
181
- return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.keys.include?(:cascade)
181
+ return self.class.namespace_inheritable(:cascade) if self.class.inheritable_setting.namespace_inheritable.key?(:cascade)
182
182
  return self.class.namespace_inheritable(:version_options)[:cascade] if self.class.namespace_inheritable(:version_options) && self.class.namespace_inheritable(:version_options).key?(:cascade)
183
183
  true
184
184
  end
@@ -209,7 +209,7 @@ module Grape
209
209
  route_settings[:endpoint] = route.app
210
210
 
211
211
  # using the :any shorthand produces [nil] for route methods, substitute all manually
212
- route_settings[:methods] = %w[GET PUT POST DELETE PATCH HEAD OPTIONS] if route_settings[:methods].include?('*')
212
+ route_settings[:methods] = Grape::Http::Headers::SUPPORTED_METHODS if route_settings[:methods].include?('*')
213
213
  end
214
214
  end
215
215
 
@@ -227,7 +227,7 @@ module Grape
227
227
  allowed_methods |= [Grape::Http::Headers::HEAD] if allowed_methods.include?(Grape::Http::Headers::GET)
228
228
  end
229
229
 
230
- allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods).join(', ')
230
+ allow_header = (self.class.namespace_inheritable(:do_not_route_options) ? allowed_methods : [Grape::Http::Headers::OPTIONS] | allowed_methods)
231
231
 
232
232
  unless self.class.namespace_inheritable(:do_not_route_options) || allowed_methods.include?(Grape::Http::Headers::OPTIONS)
233
233
  config[:endpoint].options[:options_route_enabled] = true
@@ -243,9 +243,13 @@ module Grape
243
243
  # Generate a route that returns an HTTP 405 response for a user defined
244
244
  # path on methods not specified
245
245
  def generate_not_allowed_method(pattern, allowed_methods: [], **attributes)
246
- not_allowed_methods = %w[GET PUT POST DELETE PATCH HEAD] - allowed_methods
247
- not_allowed_methods << Grape::Http::Headers::OPTIONS if self.class.namespace_inheritable(:do_not_route_options)
248
-
246
+ supported_methods =
247
+ if self.class.namespace_inheritable(:do_not_route_options)
248
+ Grape::Http::Headers::SUPPORTED_METHODS
249
+ else
250
+ Grape::Http::Headers::SUPPORTED_METHODS_WITHOUT_OPTIONS
251
+ end
252
+ not_allowed_methods = supported_methods - allowed_methods
249
253
  return if not_allowed_methods.empty?
250
254
 
251
255
  @router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes)
data/lib/grape/api.rb CHANGED
@@ -8,7 +8,7 @@ module Grape
8
8
  # should subclass this class in order to build an API.
9
9
  class API
10
10
  # Class methods that we want to call on the API rather than on the API object
11
- NON_OVERRIDABLE = (Class.new.methods + %i[call call! configuration compile!]).freeze
11
+ NON_OVERRIDABLE = (Class.new.methods + %i[call call! configuration compile! inherited]).freeze
12
12
 
13
13
  class << self
14
14
  attr_accessor :base_instance, :instances
@@ -175,7 +175,7 @@ module Grape
175
175
  if argument.respond_to?(:lazy?) && argument.lazy?
176
176
  argument.evaluate_from(configuration)
177
177
  elsif argument.is_a?(Hash)
178
- argument.map { |key, value| [key, evaluate_arguments(configuration, value).first] }.to_h
178
+ argument.transform_values { |value| evaluate_arguments(configuration, value).first }
179
179
  elsif argument.is_a?(Array)
180
180
  evaluate_arguments(configuration, *argument)
181
181
  else
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'grape/util/registrable'
4
+
5
+ module Grape
6
+ module ContentTypes
7
+ extend Util::Registrable
8
+
9
+ # Content types are listed in order of preference.
10
+ CONTENT_TYPES = {
11
+ xml: 'application/xml',
12
+ serializable_hash: 'application/json',
13
+ json: 'application/json',
14
+ binary: 'application/octet-stream',
15
+ txt: 'text/plain'
16
+ }.freeze
17
+
18
+ class << self
19
+ def content_types_for_settings(settings)
20
+ return if settings.blank?
21
+
22
+ settings.each_with_object({}) { |value, result| result.merge!(value) }
23
+ end
24
+
25
+ def content_types_for(from_settings)
26
+ if from_settings.present?
27
+ from_settings
28
+ else
29
+ Grape::ContentTypes::CONTENT_TYPES.merge(default_elements)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -94,7 +94,7 @@ module Grape
94
94
  protected
95
95
 
96
96
  def process_named_params
97
- return unless @named_params && @named_params.any?
97
+ return unless instance_variable_defined?(:@named_params) && @named_params && @named_params.any?
98
98
  api.namespace_stackable(:named_params, @named_params)
99
99
  end
100
100
  end
@@ -177,17 +177,17 @@ module Grape
177
177
  def status(status = nil)
178
178
  case status
179
179
  when Symbol
180
- raise ArgumentError, "Status code :#{status} is invalid." unless Rack::Utils::SYMBOL_TO_STATUS_CODE.keys.include?(status)
180
+ raise ArgumentError, "Status code :#{status} is invalid." unless Rack::Utils::SYMBOL_TO_STATUS_CODE.key?(status)
181
181
  @status = Rack::Utils.status_code(status)
182
182
  when Integer
183
183
  @status = status
184
184
  when nil
185
- return @status if @status
185
+ return @status if instance_variable_defined?(:@status) && @status
186
186
  case request.request_method.to_s.upcase
187
187
  when Grape::Http::Headers::POST
188
188
  201
189
189
  when Grape::Http::Headers::DELETE
190
- if @body.present?
190
+ if instance_variable_defined?(:@body) && @body.present?
191
191
  200
192
192
  else
193
193
  204
@@ -238,7 +238,7 @@ module Grape
238
238
  @body = ''
239
239
  status 204
240
240
  else
241
- @body
241
+ instance_variable_defined?(:@body) ? @body : nil
242
242
  end
243
243
  end
244
244
 
@@ -272,7 +272,7 @@ module Grape
272
272
  warn '[DEPRECATION] Argument as FileStreamer-like object is deprecated. Use path to file instead.'
273
273
  @file = Grape::ServeFile::FileResponse.new(value)
274
274
  else
275
- @file
275
+ instance_variable_defined?(:@file) ? @file : nil
276
276
  end
277
277
  end
278
278
 
@@ -331,11 +331,12 @@ module Grape
331
331
  end
332
332
 
333
333
  representation = { root => representation } if root
334
+
334
335
  if key
335
- representation = (@body || {}).merge(key => representation)
336
- elsif entity_class.present? && @body
336
+ representation = (body || {}).merge(key => representation)
337
+ elsif entity_class.present? && body
337
338
  raise ArgumentError, "Representation of type #{representation.class} cannot be merged." unless representation.respond_to?(:merge)
338
- representation = @body.merge(representation)
339
+ representation = body.merge(representation)
339
340
  end
340
341
 
341
342
  body representation
@@ -387,7 +388,7 @@ module Grape
387
388
  def entity_representation_for(entity_class, object, options)
388
389
  embeds = { env: env }
389
390
  embeds[:version] = env[Grape::Env::API_VERSION] if env[Grape::Env::API_VERSION]
390
- entity_class.represent(object, **embeds.merge(options))
391
+ entity_class.represent(object, embeds.merge(options))
391
392
  end
392
393
  end
393
394
  end
@@ -127,7 +127,7 @@ module Grape
127
127
 
128
128
  opts = attrs.extract_options!.clone
129
129
  opts[:presence] = { value: true, message: opts[:message] }
130
- opts = @group.merge(opts) if @group
130
+ opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group
131
131
 
132
132
  if opts[:using]
133
133
  require_required_and_optional_fields(attrs.first, opts)
@@ -146,7 +146,7 @@ module Grape
146
146
 
147
147
  opts = attrs.extract_options!.clone
148
148
  type = opts[:type]
149
- opts = @group.merge(opts) if @group
149
+ opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group
150
150
 
151
151
  # check type for optional parameter group
152
152
  if attrs && block_given?
@@ -243,8 +243,8 @@ module Grape
243
243
  # @return hash of parameters relevant for the current scope
244
244
  # @api private
245
245
  def params(params)
246
- params = @parent.params(params) if @parent
247
- params = map_params(params, @element) if @element
246
+ params = @parent.params(params) if instance_variable_defined?(:@parent) && @parent
247
+ params = map_params(params, @element) if instance_variable_defined?(:@element) && @element
248
248
  params
249
249
  end
250
250
 
@@ -51,7 +51,7 @@ module Grape
51
51
  end
52
52
  end
53
53
 
54
- @versions.last unless @versions.nil?
54
+ @versions.last if instance_variable_defined?(:@versions) && @versions
55
55
  end
56
56
 
57
57
  # Define a root URL prefix for your entire API.
@@ -142,11 +142,11 @@ module Grape
142
142
  reset_validations!
143
143
  end
144
144
 
145
- %w[get post put head delete options patch].each do |meth|
146
- define_method meth do |*args, &block|
145
+ Grape::Http::Headers::SUPPORTED_METHODS.each do |supported_method|
146
+ define_method supported_method.downcase do |*args, &block|
147
147
  options = args.extract_options!
148
148
  paths = args.first || ['/']
149
- route(meth.upcase, paths, options, &block)
149
+ route(supported_method, paths, options, &block)
150
150
  end
151
151
  end
152
152
 
@@ -163,6 +163,8 @@ module Grape
163
163
  # end
164
164
  # end
165
165
  def namespace(space = nil, options = {}, &block)
166
+ @namespace_description = nil unless instance_variable_defined?(:@namespace_description) && @namespace_description
167
+
166
168
  if space || block_given?
167
169
  within_namespace do
168
170
  previous_namespace_description = @namespace_description
@@ -57,10 +57,6 @@ module Grape
57
57
  end.join(', ')
58
58
  end
59
59
 
60
- def translate_attribute(key, **options)
61
- translate("#{BASE_ATTRIBUTES_KEY}.#{key}", default: key, **options)
62
- end
63
-
64
60
  def translate_message(key, **options)
65
61
  case key
66
62
  when Symbol
@@ -5,6 +5,9 @@ require 'grape/exceptions/base'
5
5
  module Grape
6
6
  module Exceptions
7
7
  class ValidationErrors < Grape::Exceptions::Base
8
+ ERRORS_FORMAT_KEY = 'grape.errors.format'
9
+ DEFAULT_ERRORS_FORMAT = '%{attributes} %{message}'
10
+
8
11
  include Enumerable
9
12
 
10
13
  attr_reader :errors
@@ -41,21 +44,17 @@ module Grape
41
44
  end
42
45
 
43
46
  def full_messages
44
- messages = map { |attributes, error| full_message(attributes, error) }
47
+ messages = map do |attributes, error|
48
+ I18n.t(
49
+ ERRORS_FORMAT_KEY,
50
+ default: DEFAULT_ERRORS_FORMAT,
51
+ attributes: translate_attributes(attributes),
52
+ message: error.message
53
+ )
54
+ end
45
55
  messages.uniq!
46
56
  messages
47
57
  end
48
-
49
- private
50
-
51
- def full_message(attributes, error)
52
- I18n.t(
53
- 'grape.errors.format',
54
- default: '%{attributes} %{message}',
55
- attributes: attributes.count == 1 ? translate_attribute(attributes.first) : translate_attributes(attributes),
56
- message: error.message
57
- )
58
- end
59
58
  end
60
59
  end
61
60
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'grape/util/lazy_object'
4
+
3
5
  module Grape
4
6
  module Http
5
7
  module Headers
@@ -19,6 +21,7 @@ module Grape
19
21
  OPTIONS = 'OPTIONS'
20
22
 
21
23
  SUPPORTED_METHODS = [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS].freeze
24
+ SUPPORTED_METHODS_WITHOUT_OPTIONS = Grape::Util::LazyObject.new { [GET, POST, PUT, PATCH, DELETE, HEAD].freeze }
22
25
 
23
26
  HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'
24
27
  X_CASCADE = 'X-Cascade'
@@ -27,6 +30,29 @@ module Grape
27
30
 
28
31
  FORMAT = 'format'
29
32
 
33
+ HTTP_HEADERS = Grape::Util::LazyObject.new do
34
+ common_http_headers = %w[
35
+ Version
36
+ Host
37
+ Connection
38
+ Cache-Control
39
+ Dnt
40
+ Upgrade-Insecure-Requests
41
+ User-Agent
42
+ Sec-Fetch-Dest
43
+ Accept
44
+ Sec-Fetch-Site
45
+ Sec-Fetch-Mode
46
+ Sec-Fetch-User
47
+ Accept-Encoding
48
+ Accept-Language
49
+ Cookie
50
+ ].freeze
51
+ common_http_headers.each_with_object({}) do |header, response|
52
+ response["HTTP_#{header.upcase.tr('-', '_')}"] = header
53
+ end.freeze
54
+ end
55
+
30
56
  def self.find_supported_method(route_method)
31
57
  Grape::Http::Headers::SUPPORTED_METHODS.detect { |supported_method| supported_method.casecmp(route_method).zero? }
32
58
  end
@@ -74,11 +74,9 @@ module Grape
74
74
  end
75
75
 
76
76
  def mime_types
77
- types_without_params = {}
78
- content_types.each_pair do |k, v|
77
+ @mime_type ||= content_types.each_pair.with_object({}) do |(k, v), types_without_params|
79
78
  types_without_params[v.split(';').first] = k
80
79
  end
81
- types_without_params
82
80
  end
83
81
 
84
82
  private
@@ -78,7 +78,8 @@ module Grape
78
78
  def merge_with(middleware_specs)
79
79
  middleware_specs.each do |operation, *args|
80
80
  if args.last.is_a?(Proc)
81
- public_send(operation, *args, &args.pop)
81
+ last_proc = args.pop
82
+ public_send(operation, *args, &last_proc)
82
83
  else
83
84
  public_send(operation, *args)
84
85
  end
@@ -26,10 +26,10 @@ module Grape
26
26
  # route.
27
27
  class Header < Base
28
28
  VENDOR_VERSION_HEADER_REGEX =
29
- /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/
29
+ /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze
30
30
 
31
- HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/
32
- HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/
31
+ HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/.freeze
32
+ HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/.freeze
33
33
 
34
34
  def before
35
35
  strict_header_checks if strict?
@@ -63,7 +63,7 @@ module Grape
63
63
 
64
64
  def an_accept_header_with_version_and_vendor_is_present?
65
65
  header.qvalues.keys.any? do |h|
66
- VENDOR_VERSION_HEADER_REGEX =~ h.sub('application/', '')
66
+ VENDOR_VERSION_HEADER_REGEX.match?(h.sub('application/', ''))
67
67
  end
68
68
  end
69
69
 
@@ -3,11 +3,12 @@
3
3
  module Rack
4
4
  module Accept
5
5
  module Header
6
+ ALLOWED_CHARACTERS = %r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$}.freeze
6
7
  class << self
7
8
  # Corrected version of https://github.com/mjackson/rack-accept/blob/master/lib/rack/accept/header.rb#L40-L44
8
9
  def parse_media_type(media_type)
9
10
  # see http://tools.ietf.org/html/rfc6838#section-4.2 for allowed characters in media type names
10
- m = media_type.to_s.match(%r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$})
11
+ m = media_type&.match(ALLOWED_CHARACTERS)
11
12
  m ? [m[1], m[2], m[3] || ''] : []
12
13
  end
13
14
  end
@@ -36,7 +36,7 @@ module Grape
36
36
 
37
37
  pieces = path.split('/')
38
38
  potential_version = pieces[1]
39
- return unless potential_version =~ options[:pattern]
39
+ return unless potential_version&.match?(options[:pattern])
40
40
  throw :error, status: 404, message: '404 API Version Not Found' if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
41
41
  env[Grape::Env::API_VERSION] = potential_version
42
42
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'grape/util/cache'
4
+
3
5
  module Grape
4
6
  # A container for endpoints or other namespaces, which allows for both
5
7
  # logical grouping of endpoints as well as sharing common configuration.
@@ -25,13 +27,21 @@ module Grape
25
27
 
26
28
  # (see ::joined_space_path)
27
29
  def self.joined_space(settings)
28
- (settings || []).map(&:space).join('/')
30
+ settings&.map(&:space)
29
31
  end
30
32
 
31
33
  # Join the namespaces from a list of settings to create a path prefix.
32
34
  # @param settings [Array] list of Grape::Util::InheritableSettings.
33
35
  def self.joined_space_path(settings)
34
- Grape::Router.normalize_path(joined_space(settings))
36
+ Grape::Router.normalize_path(JoinedSpaceCache[joined_space(settings)])
37
+ end
38
+
39
+ class JoinedSpaceCache < Grape::Util::Cache
40
+ def initialize
41
+ @cache = Hash.new do |h, joined_space|
42
+ h[joined_space] = -joined_space.join('/')
43
+ end
44
+ end
35
45
  end
36
46
  end
37
47
  end