grape 1.3.0 → 1.3.2

Sign up to get free protection for your applications and to get access to all the features.
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