grape 2.3.0 → 3.0.0

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/CONTRIBUTING.md +2 -10
  4. data/README.md +106 -43
  5. data/UPGRADING.md +90 -1
  6. data/grape.gemspec +4 -4
  7. data/lib/grape/api/instance.rb +51 -73
  8. data/lib/grape/api.rb +56 -89
  9. data/lib/grape/cookies.rb +31 -25
  10. data/lib/grape/dry_types.rb +48 -4
  11. data/lib/grape/dsl/callbacks.rb +8 -58
  12. data/lib/grape/dsl/desc.rb +8 -67
  13. data/lib/grape/dsl/headers.rb +1 -1
  14. data/lib/grape/dsl/helpers.rb +60 -65
  15. data/lib/grape/dsl/inside_route.rb +26 -61
  16. data/lib/grape/dsl/logger.rb +3 -6
  17. data/lib/grape/dsl/middleware.rb +22 -40
  18. data/lib/grape/dsl/parameters.rb +10 -19
  19. data/lib/grape/dsl/request_response.rb +136 -139
  20. data/lib/grape/dsl/routing.rb +230 -194
  21. data/lib/grape/dsl/settings.rb +22 -134
  22. data/lib/grape/dsl/validations.rb +37 -45
  23. data/lib/grape/endpoint.rb +91 -126
  24. data/lib/grape/error_formatter/base.rb +2 -0
  25. data/lib/grape/exceptions/base.rb +1 -1
  26. data/lib/grape/exceptions/conflicting_types.rb +11 -0
  27. data/lib/grape/exceptions/invalid_parameters.rb +11 -0
  28. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  29. data/lib/grape/exceptions/too_deep_parameters.rb +11 -0
  30. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  31. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  32. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  33. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +2 -5
  34. data/lib/grape/extensions/hash.rb +2 -1
  35. data/lib/grape/extensions/hashie/mash.rb +3 -5
  36. data/lib/grape/locale/en.yml +44 -44
  37. data/lib/grape/middleware/auth/base.rb +11 -32
  38. data/lib/grape/middleware/auth/dsl.rb +22 -29
  39. data/lib/grape/middleware/base.rb +30 -11
  40. data/lib/grape/middleware/error.rb +14 -32
  41. data/lib/grape/middleware/formatter.rb +40 -72
  42. data/lib/grape/middleware/stack.rb +28 -38
  43. data/lib/grape/middleware/versioner/accept_version_header.rb +2 -4
  44. data/lib/grape/middleware/versioner/base.rb +30 -56
  45. data/lib/grape/middleware/versioner/header.rb +2 -2
  46. data/lib/grape/middleware/versioner/param.rb +2 -3
  47. data/lib/grape/middleware/versioner/path.rb +1 -1
  48. data/lib/grape/namespace.rb +11 -0
  49. data/lib/grape/params_builder/base.rb +20 -0
  50. data/lib/grape/params_builder/hash.rb +11 -0
  51. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  52. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  53. data/lib/grape/params_builder.rb +32 -0
  54. data/lib/grape/request.rb +161 -22
  55. data/lib/grape/router/route.rb +1 -1
  56. data/lib/grape/router.rb +27 -8
  57. data/lib/grape/util/api_description.rb +56 -0
  58. data/lib/grape/util/base_inheritable.rb +5 -2
  59. data/lib/grape/util/inheritable_setting.rb +7 -0
  60. data/lib/grape/util/media_type.rb +1 -1
  61. data/lib/grape/util/registry.rb +1 -1
  62. data/lib/grape/validations/contract_scope.rb +2 -2
  63. data/lib/grape/validations/params_documentation.rb +50 -0
  64. data/lib/grape/validations/params_scope.rb +46 -56
  65. data/lib/grape/validations/types/array_coercer.rb +2 -3
  66. data/lib/grape/validations/types/dry_type_coercer.rb +4 -11
  67. data/lib/grape/validations/types/primitive_coercer.rb +1 -28
  68. data/lib/grape/validations/types.rb +10 -25
  69. data/lib/grape/validations/validators/base.rb +2 -9
  70. data/lib/grape/validations/validators/except_values_validator.rb +1 -1
  71. data/lib/grape/validations/validators/presence_validator.rb +1 -1
  72. data/lib/grape/validations/validators/regexp_validator.rb +1 -1
  73. data/lib/grape/version.rb +1 -1
  74. data/lib/grape.rb +18 -9
  75. metadata +35 -20
  76. data/lib/grape/api/helpers.rb +0 -9
  77. data/lib/grape/dsl/api.rb +0 -19
  78. data/lib/grape/dsl/configuration.rb +0 -15
  79. data/lib/grape/error_formatter/jsonapi.rb +0 -7
  80. data/lib/grape/http/headers.rb +0 -56
  81. data/lib/grape/middleware/helpers.rb +0 -12
  82. data/lib/grape/parser/jsonapi.rb +0 -7
  83. data/lib/grape/types/invalid_value.rb +0 -8
  84. data/lib/grape/util/lazy/object.rb +0 -45
  85. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  86. data/lib/grape/validations/attributes_doc.rb +0 -60
@@ -1,59 +1,59 @@
1
+ ---
1
2
  en:
2
3
  grape:
3
4
  errors:
4
- format: ! '%{attributes} %{message}'
5
+ format: '%{attributes} %{message}'
5
6
  messages:
6
- coerce: 'is invalid'
7
- presence: 'is missing'
8
- regexp: 'is invalid'
7
+ all_or_none: 'provide all or none of parameters'
8
+ at_least_one: 'are missing, at least one parameter must be provided'
9
9
  blank: 'is empty'
10
- values: 'does not have a valid value'
10
+ coerce: 'is invalid'
11
+ conflicting_types: 'query params contains conflicting types'
12
+ empty_message_body: 'empty message body supplied with %{body_format} content-type'
13
+ exactly_one: 'are missing, exactly one parameter must be provided'
11
14
  except_values: 'has a value not allowed'
12
- same_as: 'is not the same as %{parameter}'
15
+ incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
16
+ invalid_accept_header:
17
+ problem: 'invalid accept header'
18
+ resolution: '%{message}'
19
+ invalid_formatter: 'cannot convert %{klass} to %{to_format}'
20
+ invalid_message_body:
21
+ problem: 'message body does not match declared format'
22
+ resolution: 'when specifying %{body_format} as content-type, you must pass valid %{body_format} in the request''s ''body'' '
23
+ invalid_parameters: 'query params contains invalid format or byte sequence'
24
+ invalid_response: 'Invalid response'
25
+ invalid_version_header:
26
+ problem: 'invalid version header'
27
+ resolution: '%{message}'
28
+ invalid_versioner_option:
29
+ problem: 'unknown :using for versioner: %{strategy}'
30
+ resolution: 'available strategy for :using is :path, :header, :accept_version_header, :param'
31
+ invalid_with_option_for_represent:
32
+ problem: 'you must specify an entity class in the :with option'
33
+ resolution: 'eg: represent User, :with => Entity::User'
13
34
  length: 'is expected to have length within %{min} and %{max}'
14
35
  length_is: 'is expected to have length exactly equal to %{is}'
15
- length_min: 'is expected to have length greater than or equal to %{min}'
16
36
  length_max: 'is expected to have length less than or equal to %{max}'
17
- missing_vendor_option:
18
- problem: 'missing :vendor option'
19
- summary: 'when version using header, you must specify :vendor option'
20
- resolution: "eg: version 'v1', using: :header, vendor: 'twitter'"
37
+ length_min: 'is expected to have length greater than or equal to %{min}'
38
+ missing_group_type: 'group type is required'
21
39
  missing_mime_type:
22
40
  problem: 'missing mime type for %{new_format}'
23
- resolution:
24
- "you can choose existing mime type from Grape::ContentTypes::CONTENT_TYPES
25
- or add your own with content_type :%{new_format}, 'application/%{new_format}'
26
- "
27
- invalid_with_option_for_represent:
28
- problem: 'you must specify an entity class in the :with option'
29
- resolution: 'eg: represent User, :with => Entity::User'
41
+ resolution: 'you can choose existing mime type from Grape::ContentTypes::CONTENT_TYPES or add your own with content_type :%{new_format}, ''application/%{new_format}'' '
30
42
  missing_option: 'you must specify :%{option} options'
31
- invalid_formatter: 'cannot convert %{klass} to %{to_format}'
32
- invalid_versioner_option:
33
- problem: 'unknown :using for versioner: %{strategy}'
34
- resolution: 'available strategy for :using is :path, :header, :accept_version_header, :param'
35
- unknown_validator: 'unknown validator: %{validator_type}'
43
+ missing_vendor_option:
44
+ problem: 'missing :vendor option'
45
+ resolution: 'eg: version ''v1'', using: :header, vendor: ''twitter'''
46
+ summary: 'when version using header, you must specify :vendor option'
47
+ mutual_exclusion: 'are mutually exclusive'
48
+ presence: 'is missing'
49
+ regexp: 'is invalid'
50
+ same_as: 'is not the same as %{parameter}'
51
+ too_deep_parameters: 'query params are recursively nested over the specified limit (%{limit})'
52
+ too_many_multipart_files: 'the number of uploaded files exceeded the system''s configured limit (%{limit})'
53
+ unknown_auth_strategy: 'unknown auth strategy: %{strategy}'
36
54
  unknown_options: 'unknown options: %{options}'
37
55
  unknown_parameter: 'unknown parameter: %{param}'
38
- incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
39
- mutual_exclusion: 'are mutually exclusive'
40
- at_least_one: 'are missing, at least one parameter must be provided'
41
- exactly_one: 'are missing, exactly one parameter must be provided'
42
- all_or_none: 'provide all or none of parameters'
43
- missing_group_type: 'group type is required'
56
+ unknown_params_builder: 'unknown params_builder: %{params_builder_type}'
57
+ unknown_validator: 'unknown validator: %{validator_type}'
44
58
  unsupported_group_type: 'group type must be Array, Hash, JSON or Array[JSON]'
45
- invalid_message_body:
46
- problem: "message body does not match declared format"
47
- resolution:
48
- "when specifying %{body_format} as content-type, you must pass valid
49
- %{body_format} in the request's 'body'
50
- "
51
- empty_message_body: 'empty message body supplied with %{body_format} content-type'
52
- too_many_multipart_files: "the number of uploaded files exceeded the system's configured limit (%{limit})"
53
- invalid_accept_header:
54
- problem: 'invalid accept header'
55
- resolution: '%{message}'
56
- invalid_version_header:
57
- problem: 'invalid version header'
58
- resolution: '%{message}'
59
- invalid_response: 'Invalid response'
59
+ values: 'does not have a valid value'
@@ -3,40 +3,19 @@
3
3
  module Grape
4
4
  module Middleware
5
5
  module Auth
6
- class Base
7
- include Helpers
8
-
9
- attr_accessor :options, :app, :env
10
-
11
- def initialize(app, *options)
12
- @app = app
13
- @options = options.shift
14
- end
15
-
16
- def call(env)
17
- dup._call(env)
6
+ class Base < Grape::Middleware::Base
7
+ def initialize(app, **options)
8
+ super
9
+ @auth_strategy = Grape::Middleware::Auth::Strategies[options[:type]].tap do |auth_strategy|
10
+ raise Grape::Exceptions::UnknownAuthStrategy.new(strategy: options[:type]) unless auth_strategy
11
+ end
18
12
  end
19
13
 
20
- def _call(env)
21
- self.env = env
22
-
23
- if options.key?(:type)
24
- auth_proc = options[:proc]
25
- auth_proc_context = context
26
-
27
- strategy_info = Grape::Middleware::Auth::Strategies[options[:type]]
28
-
29
- throw(:error, status: 401, message: 'API Authorization Failed.') if strategy_info.blank?
30
-
31
- strategy = strategy_info.create(@app, options) do |*args|
32
- auth_proc_context.instance_exec(*args, &auth_proc)
33
- end
34
-
35
- strategy.call(env)
36
-
37
- else
38
- app.call(env)
39
- end
14
+ def call!(env)
15
+ @env = env
16
+ @auth_strategy.create(app, options) do |*args|
17
+ context.instance_exec(*args, &options[:proc])
18
+ end.call(env)
40
19
  end
41
20
  end
42
21
  end
@@ -4,40 +4,33 @@ module Grape
4
4
  module Middleware
5
5
  module Auth
6
6
  module DSL
7
- extend ActiveSupport::Concern
7
+ def auth(type = nil, options = {}, &block)
8
+ namespace_inheritable = inheritable_setting.namespace_inheritable
9
+ return namespace_inheritable[:auth] unless type
8
10
 
9
- module ClassMethods
10
- # Add an authentication type to the API. Currently
11
- # only `:http_basic`, `:http_digest` are supported.
12
- def auth(type = nil, options = {}, &block)
13
- if type
14
- namespace_inheritable(:auth, options.reverse_merge(type: type.to_sym, proc: block))
15
- use Grape::Middleware::Auth::Base, namespace_inheritable(:auth)
16
- else
17
- namespace_inheritable(:auth)
18
- end
19
- end
20
-
21
- # Add HTTP Basic authorization to the API.
22
- #
23
- # @param [Hash] options A hash of options.
24
- # @option options [String] :realm "API Authorization" The HTTP Basic realm.
25
- def http_basic(options = {}, &block)
26
- options[:realm] ||= 'API Authorization'
27
- auth :http_basic, options, &block
28
- end
11
+ namespace_inheritable[:auth] = options.reverse_merge(type: type.to_sym, proc: block)
12
+ use Grape::Middleware::Auth::Base, namespace_inheritable[:auth]
13
+ end
29
14
 
30
- def http_digest(options = {}, &block)
31
- options[:realm] ||= 'API Authorization'
15
+ # Add HTTP Basic authorization to the API.
16
+ #
17
+ # @param [Hash] options A hash of options.
18
+ # @option options [String] :realm "API Authorization" The HTTP Basic realm.
19
+ def http_basic(options = {}, &block)
20
+ options[:realm] ||= 'API Authorization'
21
+ auth :http_basic, options, &block
22
+ end
32
23
 
33
- if options[:realm].respond_to?(:values_at)
34
- options[:realm][:opaque] ||= 'secret'
35
- else
36
- options[:opaque] ||= 'secret'
37
- end
24
+ def http_digest(options = {}, &block)
25
+ options[:realm] ||= 'API Authorization'
38
26
 
39
- auth :http_digest, options, &block
27
+ if options[:realm].respond_to?(:values_at)
28
+ options[:realm][:opaque] ||= 'secret'
29
+ else
30
+ options[:opaque] ||= 'secret'
40
31
  end
32
+
33
+ auth :http_digest, options, &block
41
34
  end
42
35
  end
43
36
  end
@@ -3,25 +3,18 @@
3
3
  module Grape
4
4
  module Middleware
5
5
  class Base
6
- include Helpers
7
6
  include Grape::DSL::Headers
8
7
 
9
8
  attr_reader :app, :env, :options
10
9
 
11
- TEXT_HTML = 'text/html'
12
-
13
10
  # @param [Rack Application] app The standard argument for a Rack middleware.
14
11
  # @param [Hash] options A hash of options, simply stored for use by subclasses.
15
- def initialize(app, *options)
12
+ def initialize(app, **options)
16
13
  @app = app
17
- @options = options.any? ? default_options.deep_merge(options.shift) : default_options
14
+ @options = merge_default_options(options)
18
15
  @app_response = nil
19
16
  end
20
17
 
21
- def default_options
22
- {}
23
- end
24
-
25
18
  def call(env)
26
19
  dup.call!(env).to_a
27
20
  end
@@ -54,10 +47,18 @@ module Grape
54
47
  # @return [Response, nil] a Rack SPEC response or nil to call the application afterwards.
55
48
  def after; end
56
49
 
50
+ def rack_request
51
+ @rack_request ||= Rack::Request.new(env)
52
+ end
53
+
54
+ def context
55
+ env[Grape::Env::API_ENDPOINT]
56
+ end
57
+
57
58
  def response
58
59
  return @app_response if @app_response.is_a?(Rack::Response)
59
60
 
60
- @app_response = Rack::Response.new(@app_response[2], @app_response[0], @app_response[1])
61
+ @app_response = Rack::Response[*@app_response]
61
62
  end
62
63
 
63
64
  def content_types
@@ -73,7 +74,15 @@ module Grape
73
74
  end
74
75
 
75
76
  def content_type
76
- content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || TEXT_HTML
77
+ content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || 'text/html'
78
+ end
79
+
80
+ def query_params
81
+ rack_request.GET
82
+ rescue Rack::QueryParser::ParamsTooDeepError
83
+ raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit)
84
+ rescue Rack::Utils::ParameterTypeError
85
+ raise Grape::Exceptions::ConflictingTypes
77
86
  end
78
87
 
79
88
  private
@@ -90,6 +99,16 @@ module Grape
90
99
  def content_types_indifferent_access
91
100
  @content_types_indifferent_access ||= content_types.with_indifferent_access
92
101
  end
102
+
103
+ def merge_default_options(options)
104
+ if respond_to?(:default_options)
105
+ default_options.deep_merge(options)
106
+ elsif self.class.const_defined?(:DEFAULT_OPTIONS)
107
+ self.class::DEFAULT_OPTIONS.deep_merge(options)
108
+ else
109
+ options
110
+ end
111
+ end
93
112
  end
94
113
  end
95
114
  end
@@ -3,31 +3,18 @@
3
3
  module Grape
4
4
  module Middleware
5
5
  class Error < Base
6
- def default_options
7
- {
8
- default_status: 500, # default status returned on error
9
- default_message: '',
10
- format: :txt,
11
- helpers: nil,
12
- formatters: {},
13
- error_formatters: {},
14
- rescue_all: false, # true to rescue all exceptions
15
- rescue_grape_exceptions: false,
16
- rescue_subclasses: true, # rescue subclasses of exceptions listed
17
- rescue_options: {
18
- backtrace: false, # true to display backtrace, true to let Grape handle Grape::Exceptions
19
- original_exception: false # true to display exception
20
- },
21
- rescue_handlers: {}, # rescue handler blocks
22
- base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class
23
- all_rescue_handler: nil # rescue handler block to rescue from all exceptions
24
- }
25
- end
26
-
27
- def initialize(app, *options)
28
- super
29
- self.class.include(@options[:helpers]) if @options[:helpers]
30
- end
6
+ DEFAULT_OPTIONS = {
7
+ default_status: 500,
8
+ default_message: '',
9
+ format: :txt,
10
+ rescue_all: false,
11
+ rescue_grape_exceptions: false,
12
+ rescue_subclasses: true,
13
+ rescue_options: {
14
+ backtrace: false,
15
+ original_exception: false
16
+ }.freeze
17
+ }.freeze
31
18
 
32
19
  def call!(env)
33
20
  @env = env
@@ -39,7 +26,7 @@ module Grape
39
26
  private
40
27
 
41
28
  def rack_response(status, headers, message)
42
- message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == TEXT_HTML
29
+ message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == 'text/html'
43
30
  Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), Grape::Util::Header.new.merge(headers))
44
31
  end
45
32
 
@@ -111,12 +98,7 @@ module Grape
111
98
  end
112
99
 
113
100
  def run_rescue_handler(handler, error, endpoint)
114
- if handler.instance_of?(Symbol)
115
- raise NoMethodError, "undefined method '#{handler}'" unless respond_to?(handler)
116
-
117
- handler = public_method(handler)
118
- end
119
-
101
+ handler = endpoint.public_method(handler) if handler.instance_of?(Symbol)
120
102
  response = catch(:error) do
121
103
  handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler)
122
104
  end
@@ -3,16 +3,11 @@
3
3
  module Grape
4
4
  module Middleware
5
5
  class Formatter < Base
6
- CHUNKED = 'chunked'
7
- FORMAT = 'format'
8
-
9
- def default_options
10
- {
11
- default_format: :txt,
12
- formatters: {},
13
- parsers: {}
14
- }
15
- end
6
+ DEFAULT_OPTIONS = {
7
+ default_format: :txt
8
+ }.freeze
9
+
10
+ ALL_MEDIA_TYPES = '*/*'
16
11
 
17
12
  def before
18
13
  negotiate_content_type
@@ -69,34 +64,27 @@ module Grape
69
64
  end
70
65
  end
71
66
 
72
- def request
73
- @request ||= Rack::Request.new(env)
74
- end
75
-
76
- # store read input in env['api.request.input']
77
67
  def read_body_input
78
- return unless
79
- (request.post? || request.put? || request.patch? || request.delete?) &&
80
- (!request.form_data? || !request.media_type) &&
81
- !request.parseable_data? &&
82
- (request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
83
-
84
- return unless (input = env[Rack::RACK_INPUT])
68
+ input = rack_request.body # reads RACK_INPUT
69
+ return if input.nil?
70
+ return unless read_body_input?
85
71
 
86
- rewind_input input
72
+ input.try(:rewind)
87
73
  body = env[Grape::Env::API_REQUEST_INPUT] = input.read
88
74
  begin
89
- read_rack_input(body) if body && !body.empty?
75
+ read_rack_input(body)
90
76
  ensure
91
- rewind_input input
77
+ input.try(:rewind)
92
78
  end
93
79
  end
94
80
 
95
- # store parsed input in env['api.request.body']
96
81
  def read_rack_input(body)
97
- fmt = request.media_type ? mime_types[request.media_type] : options[:default_format]
82
+ return if body.empty?
98
83
 
99
- throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt)
84
+ media_type = rack_request.media_type
85
+ fmt = media_type ? mime_types[media_type] : options[:default_format]
86
+
87
+ throw :error, status: 415, message: "The provided content-type '#{media_type}' is not supported." unless content_type_for(fmt)
100
88
  parser = Grape::Parser.parser_for fmt, options[:parsers]
101
89
  if parser
102
90
  begin
@@ -119,63 +107,43 @@ module Grape
119
107
  end
120
108
  end
121
109
 
110
+ # this middleware will not try to format the following content-types since Rack already handles them
111
+ # when calling Rack's `params` function
112
+ # - application/x-www-form-urlencoded
113
+ # - multipart/form-data
114
+ # - multipart/related
115
+ # - multipart/mixed
116
+ def read_body_input?
117
+ (rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?) &&
118
+ !(rack_request.form_data? && rack_request.content_type) &&
119
+ !rack_request.parseable_data? &&
120
+ (rack_request.content_length.to_i.positive? || rack_request.env['HTTP_TRANSFER_ENCODING'] == 'chunked')
121
+ end
122
+
122
123
  def negotiate_content_type
123
- fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
124
+ fmt = format_from_extension || query_params['format'] || options[:format] || format_from_header || options[:default_format]
124
125
  if content_type_for(fmt)
125
- env[Grape::Env::API_FORMAT] = fmt
126
+ env[Grape::Env::API_FORMAT] = fmt.to_sym
126
127
  else
127
128
  throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
128
129
  end
129
130
  end
130
131
 
131
132
  def format_from_extension
132
- parts = request.path.split('.')
133
+ request_path = rack_request.path.try(:scrub)
134
+ dot_pos = request_path.rindex('.')
135
+ return unless dot_pos
133
136
 
134
- if parts.size > 1
135
- extension = parts.last
136
- # avoid symbol memory leak on an unknown format
137
- return extension.to_sym if content_type_for(extension)
138
- end
139
- nil
140
- end
141
-
142
- def format_from_params
143
- fmt = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[FORMAT]
144
- # avoid symbol memory leak on an unknown format
145
- return fmt.to_sym if content_type_for(fmt)
146
-
147
- fmt
137
+ extension = request_path[(dot_pos + 1)..]
138
+ extension if content_type_for(extension)
148
139
  end
149
140
 
150
141
  def format_from_header
151
- mime_array.each do |t|
152
- return mime_types[t] if mime_types.key?(t)
153
- end
154
- nil
155
- end
156
-
157
- def mime_array
158
- accept = env[Grape::Http::Headers::HTTP_ACCEPT]
159
- return [] unless accept
160
-
161
- accept_into_mime_and_quality = %r{
162
- (
163
- \w+/[\w+.-]+) # eg application/vnd.example.myformat+xml
164
- (?:
165
- (?:;[^,]*?)? # optionally multiple formats in a row
166
- ;\s*q=([\w.]+) # optional "quality" preference (eg q=0.5)
167
- )?
168
- }x
169
-
170
- vendor_prefix_pattern = /vnd\.[^+]+\+/
171
-
172
- accept.scan(accept_into_mime_and_quality)
173
- .sort_by { |_, quality_preference| -(quality_preference ? quality_preference.to_f : 1.0) }
174
- .flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] }
175
- end
142
+ accept_header = env['HTTP_ACCEPT'].try(:scrub)
143
+ return if accept_header.blank? || accept_header == ALL_MEDIA_TYPES
176
144
 
177
- def rewind_input(input)
178
- input.rewind if input.respond_to?(:rewind)
145
+ media_type = Rack::Utils.best_q_match(accept_header, mime_types.keys)
146
+ mime_types[media_type] if media_type
179
147
  end
180
148
  end
181
149
  end
@@ -5,10 +5,11 @@ module Grape
5
5
  # Class to handle the stack of middlewares based on ActionDispatch::MiddlewareStack
6
6
  # It allows to insert and insert after
7
7
  class Stack
8
+ extend Forwardable
8
9
  class Middleware
9
10
  attr_reader :args, :block, :klass
10
11
 
11
- def initialize(klass, *args, &block)
12
+ def initialize(klass, args, block)
12
13
  @klass = klass
13
14
  @args = args
14
15
  @block = block
@@ -31,8 +32,12 @@ module Grape
31
32
  klass.to_s
32
33
  end
33
34
 
34
- def use_in(builder)
35
- builder.use(@klass, *@args, &@block)
35
+ def build(builder)
36
+ # we need to force the ruby2_keywords_hash for middlewares that initialize contains keywords
37
+ # like ActionDispatch::RequestId since middleware arguments are serialized
38
+ # https://rubyapi.org/3.4/o/hash#method-c-ruby2_keywords_hash
39
+ args[-1] = Hash.ruby2_keywords_hash(args[-1]) if args.last.is_a?(Hash) && Hash.respond_to?(:ruby2_keywords_hash)
40
+ builder.use(klass, *args, &block)
36
41
  end
37
42
  end
38
43
 
@@ -40,72 +45,57 @@ module Grape
40
45
 
41
46
  attr_accessor :middlewares, :others
42
47
 
48
+ def_delegators :middlewares, :each, :size, :last, :[]
49
+
43
50
  def initialize
44
51
  @middlewares = []
45
52
  @others = []
46
53
  end
47
54
 
48
- def each(&block)
49
- @middlewares.each(&block)
50
- end
51
-
52
- def size
53
- middlewares.size
54
- end
55
-
56
- def last
57
- middlewares.last
58
- end
59
-
60
- def [](index)
61
- middlewares[index]
62
- end
63
-
64
- def insert(index, *args, &block)
55
+ def insert(index, klass, *args, &block)
65
56
  index = assert_index(index, :before)
66
- middleware = self.class::Middleware.new(*args, &block)
67
- middlewares.insert(index, middleware)
57
+ middlewares.insert(index, self.class::Middleware.new(klass, args, block))
68
58
  end
69
- ruby2_keywords :insert if respond_to?(:ruby2_keywords, true)
70
59
 
71
60
  alias insert_before insert
72
61
 
73
- def insert_after(index, *args, &block)
62
+ def insert_after(index, ...)
74
63
  index = assert_index(index, :after)
75
- insert(index + 1, *args, &block)
64
+ insert(index + 1, ...)
76
65
  end
77
- ruby2_keywords :insert_after if respond_to?(:ruby2_keywords, true)
78
66
 
79
- def use(...)
80
- middleware = self.class::Middleware.new(...)
67
+ def use(klass, *args, &block)
68
+ middleware = self.class::Middleware.new(klass, args, block)
81
69
  middlewares.push(middleware)
82
70
  end
83
71
 
84
72
  def merge_with(middleware_specs)
85
- middleware_specs.each do |operation, *args|
73
+ middleware_specs.each do |operation, klass, *args|
86
74
  if args.last.is_a?(Proc)
87
75
  last_proc = args.pop
88
- public_send(operation, *args, &last_proc)
76
+ public_send(operation, klass, *args, &last_proc)
89
77
  else
90
- public_send(operation, *args)
78
+ public_send(operation, klass, *args)
91
79
  end
92
80
  end
93
81
  end
94
82
 
95
83
  # @return [Rack::Builder] the builder object with our middlewares applied
96
- def build(builder = Rack::Builder.new)
97
- others.shift(others.size).each { |m| merge_with(m) }
98
- middlewares.each do |m|
99
- m.use_in(builder)
84
+ def build
85
+ Rack::Builder.new.tap do |builder|
86
+ others.shift(others.size).each { |m| merge_with(m) }
87
+ middlewares.each do |m|
88
+ m.build(builder)
89
+ end
100
90
  end
101
- builder
102
91
  end
103
92
 
104
93
  # @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later
105
94
  # @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]])
106
95
  def concat(other_specs)
107
- @others << Array(other_specs).reject { |o| o.first == :use }
108
- merge_with(Array(other_specs).select { |o| o.first == :use })
96
+ use, not_use = other_specs.partition { |o| o.first == :use }
97
+ others << not_use
98
+ merge_with(use)
109
99
  end
110
100
 
111
101
  protected
@@ -18,10 +18,8 @@ module Grape
18
18
  # route.
19
19
  class AcceptVersionHeader < Base
20
20
  def before
21
- potential_version = env[Grape::Http::Headers::HTTP_ACCEPT_VERSION]
22
- potential_version = potential_version.scrub unless potential_version.nil?
23
-
24
- not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank?
21
+ potential_version = env['HTTP_ACCEPT_VERSION'].try(:scrub)
22
+ not_acceptable!('Accept-Version header must be set.') if strict && potential_version.blank?
25
23
 
26
24
  return if potential_version.blank?
27
25