grape 2.0.0 → 2.4.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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +151 -1
  3. data/CONTRIBUTING.md +1 -1
  4. data/README.md +404 -334
  5. data/UPGRADING.md +279 -7
  6. data/grape.gemspec +8 -8
  7. data/lib/grape/api/instance.rb +34 -66
  8. data/lib/grape/api.rb +47 -70
  9. data/lib/grape/content_types.rb +13 -10
  10. data/lib/grape/cookies.rb +31 -24
  11. data/lib/grape/dry_types.rb +0 -2
  12. data/lib/grape/dsl/api.rb +0 -2
  13. data/lib/grape/dsl/desc.rb +49 -44
  14. data/lib/grape/dsl/headers.rb +2 -2
  15. data/lib/grape/dsl/helpers.rb +8 -4
  16. data/lib/grape/dsl/inside_route.rb +67 -54
  17. data/lib/grape/dsl/parameters.rb +10 -9
  18. data/lib/grape/dsl/request_response.rb +14 -18
  19. data/lib/grape/dsl/routing.rb +34 -17
  20. data/lib/grape/dsl/validations.rb +13 -0
  21. data/lib/grape/endpoint.rb +120 -118
  22. data/lib/grape/{util/env.rb → env.rb} +0 -5
  23. data/lib/grape/error_formatter/base.rb +51 -21
  24. data/lib/grape/error_formatter/json.rb +7 -15
  25. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  26. data/lib/grape/error_formatter/txt.rb +11 -17
  27. data/lib/grape/error_formatter/xml.rb +3 -13
  28. data/lib/grape/error_formatter.rb +5 -25
  29. data/lib/grape/exceptions/base.rb +18 -30
  30. data/lib/grape/exceptions/conflicting_types.rb +11 -0
  31. data/lib/grape/exceptions/invalid_parameters.rb +11 -0
  32. data/lib/grape/exceptions/too_deep_parameters.rb +11 -0
  33. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  34. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  35. data/lib/grape/exceptions/validation.rb +5 -6
  36. data/lib/grape/exceptions/validation_array_errors.rb +1 -0
  37. data/lib/grape/exceptions/validation_errors.rb +4 -6
  38. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +2 -5
  39. data/lib/grape/extensions/hash.rb +7 -2
  40. data/lib/grape/extensions/hashie/mash.rb +3 -5
  41. data/lib/grape/formatter/base.rb +16 -0
  42. data/lib/grape/formatter/json.rb +4 -6
  43. data/lib/grape/formatter/serializable_hash.rb +1 -1
  44. data/lib/grape/formatter/txt.rb +3 -5
  45. data/lib/grape/formatter/xml.rb +4 -6
  46. data/lib/grape/formatter.rb +7 -25
  47. data/lib/grape/{util/json.rb → json.rb} +1 -3
  48. data/lib/grape/locale/en.yml +46 -42
  49. data/lib/grape/middleware/auth/base.rb +11 -34
  50. data/lib/grape/middleware/auth/dsl.rb +23 -31
  51. data/lib/grape/middleware/base.rb +41 -23
  52. data/lib/grape/middleware/error.rb +77 -76
  53. data/lib/grape/middleware/formatter.rb +48 -79
  54. data/lib/grape/middleware/globals.rb +1 -3
  55. data/lib/grape/middleware/stack.rb +26 -37
  56. data/lib/grape/middleware/versioner/accept_version_header.rb +6 -33
  57. data/lib/grape/middleware/versioner/base.rb +74 -0
  58. data/lib/grape/middleware/versioner/header.rb +59 -126
  59. data/lib/grape/middleware/versioner/param.rb +4 -25
  60. data/lib/grape/middleware/versioner/path.rb +10 -34
  61. data/lib/grape/middleware/versioner.rb +7 -14
  62. data/lib/grape/namespace.rb +4 -5
  63. data/lib/grape/params_builder/base.rb +18 -0
  64. data/lib/grape/params_builder/hash.rb +11 -0
  65. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  66. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  67. data/lib/grape/params_builder.rb +32 -0
  68. data/lib/grape/parser/base.rb +16 -0
  69. data/lib/grape/parser/json.rb +6 -8
  70. data/lib/grape/parser/xml.rb +6 -8
  71. data/lib/grape/parser.rb +5 -23
  72. data/lib/grape/path.rb +38 -60
  73. data/lib/grape/request.rb +161 -30
  74. data/lib/grape/router/base_route.rb +39 -0
  75. data/lib/grape/router/greedy_route.rb +20 -0
  76. data/lib/grape/router/pattern.rb +45 -31
  77. data/lib/grape/router/route.rb +28 -57
  78. data/lib/grape/router.rb +56 -43
  79. data/lib/grape/util/base_inheritable.rb +4 -4
  80. data/lib/grape/util/cache.rb +0 -3
  81. data/lib/grape/util/endpoint_configuration.rb +1 -1
  82. data/lib/grape/util/header.rb +13 -0
  83. data/lib/grape/util/inheritable_values.rb +0 -2
  84. data/lib/grape/util/lazy/block.rb +29 -0
  85. data/lib/grape/util/lazy/value.rb +38 -0
  86. data/lib/grape/util/lazy/value_array.rb +21 -0
  87. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  88. data/lib/grape/util/lazy/value_hash.rb +21 -0
  89. data/lib/grape/util/media_type.rb +70 -0
  90. data/lib/grape/util/registry.rb +27 -0
  91. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  92. data/lib/grape/util/stackable_values.rb +1 -6
  93. data/lib/grape/util/strict_hash_configuration.rb +3 -3
  94. data/lib/grape/validations/attributes_doc.rb +38 -36
  95. data/lib/grape/validations/attributes_iterator.rb +1 -0
  96. data/lib/grape/validations/contract_scope.rb +34 -0
  97. data/lib/grape/validations/params_scope.rb +36 -32
  98. data/lib/grape/validations/types/array_coercer.rb +0 -2
  99. data/lib/grape/validations/types/dry_type_coercer.rb +9 -15
  100. data/lib/grape/validations/types/json.rb +0 -2
  101. data/lib/grape/validations/types/primitive_coercer.rb +0 -2
  102. data/lib/grape/validations/types/set_coercer.rb +0 -3
  103. data/lib/grape/validations/types.rb +0 -3
  104. data/lib/grape/validations/validator_factory.rb +2 -2
  105. data/lib/grape/validations/validators/allow_blank_validator.rb +1 -1
  106. data/lib/grape/validations/validators/base.rb +8 -11
  107. data/lib/grape/validations/validators/coerce_validator.rb +1 -1
  108. data/lib/grape/validations/validators/contract_scope_validator.rb +41 -0
  109. data/lib/grape/validations/validators/default_validator.rb +6 -2
  110. data/lib/grape/validations/validators/exactly_one_of_validator.rb +1 -1
  111. data/lib/grape/validations/validators/except_values_validator.rb +2 -2
  112. data/lib/grape/validations/validators/length_validator.rb +49 -0
  113. data/lib/grape/validations/validators/presence_validator.rb +1 -1
  114. data/lib/grape/validations/validators/regexp_validator.rb +2 -2
  115. data/lib/grape/validations/validators/values_validator.rb +20 -57
  116. data/lib/grape/validations.rb +8 -21
  117. data/lib/grape/version.rb +1 -1
  118. data/lib/grape/{util/xml.rb → xml.rb} +1 -1
  119. data/lib/grape.rb +42 -274
  120. metadata +45 -44
  121. data/lib/grape/eager_load.rb +0 -20
  122. data/lib/grape/http/headers.rb +0 -71
  123. data/lib/grape/middleware/helpers.rb +0 -12
  124. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  125. data/lib/grape/router/attribute_translator.rb +0 -63
  126. data/lib/grape/util/lazy_block.rb +0 -27
  127. data/lib/grape/util/lazy_object.rb +0 -43
  128. data/lib/grape/util/lazy_value.rb +0 -91
  129. data/lib/grape/util/registrable.rb +0 -15
  130. data/lib/grape/validations/types/build_coercer.rb +0 -94
@@ -1,30 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/dsl/headers'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  class Base
8
- include Helpers
6
+ include Grape::DSL::Headers
9
7
 
10
8
  attr_reader :app, :env, :options
11
9
 
12
- TEXT_HTML = 'text/html'
13
-
14
- include Grape::DSL::Headers
15
-
16
10
  # @param [Rack Application] app The standard argument for a Rack middleware.
17
11
  # @param [Hash] options A hash of options, simply stored for use by subclasses.
18
- def initialize(app, *options)
12
+ def initialize(app, **options)
19
13
  @app = app
20
- @options = options.any? ? default_options.merge(options.shift) : default_options
14
+ @options = merge_default_options(options)
21
15
  @app_response = nil
22
16
  end
23
17
 
24
- def default_options
25
- {}
26
- end
27
-
28
18
  def call(env)
29
19
  dup.call!(env).to_a
30
20
  end
@@ -57,28 +47,42 @@ module Grape
57
47
  # @return [Response, nil] a Rack SPEC response or nil to call the application afterwards.
58
48
  def after; end
59
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
+
60
58
  def response
61
59
  return @app_response if @app_response.is_a?(Rack::Response)
62
60
 
63
- @app_response = Rack::Response.new(@app_response[2], @app_response[0], @app_response[1])
61
+ @app_response = Rack::Response[*@app_response]
64
62
  end
65
63
 
66
- def content_type_for(format)
67
- HashWithIndifferentAccess.new(content_types)[format]
64
+ def content_types
65
+ @content_types ||= Grape::ContentTypes.content_types_for(options[:content_types])
68
66
  end
69
67
 
70
- def content_types
71
- ContentTypes.content_types_for(options[:content_types])
68
+ def mime_types
69
+ @mime_types ||= Grape::ContentTypes.mime_types_for(content_types)
70
+ end
71
+
72
+ def content_type_for(format)
73
+ content_types_indifferent_access[format]
72
74
  end
73
75
 
74
76
  def content_type
75
- 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'
76
78
  end
77
79
 
78
- def mime_types
79
- @mime_type ||= content_types.each_pair.with_object({}) do |(k, v), types_without_params|
80
- types_without_params[v.split(';').first] = k
81
- end
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
82
86
  end
83
87
 
84
88
  private
@@ -91,6 +95,20 @@ module Grape
91
95
  when Array then response[1].merge!(headers)
92
96
  end
93
97
  end
98
+
99
+ def content_types_indifferent_access
100
+ @content_types_indifferent_access ||= content_types.with_indifferent_access
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
94
112
  end
95
113
  end
96
114
  end
@@ -1,139 +1,140 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
- require 'active_support/core_ext/string/output_safety'
5
-
6
3
  module Grape
7
4
  module Middleware
8
5
  class Error < Base
9
- def default_options
10
- {
11
- default_status: 500, # default status returned on error
12
- default_message: '',
13
- format: :txt,
14
- helpers: nil,
15
- formatters: {},
16
- error_formatters: {},
17
- rescue_all: false, # true to rescue all exceptions
18
- rescue_grape_exceptions: false,
19
- rescue_subclasses: true, # rescue subclasses of exceptions listed
20
- rescue_options: {
21
- backtrace: false, # true to display backtrace, true to let Grape handle Grape::Exceptions
22
- original_exception: false # true to display exception
23
- },
24
- rescue_handlers: {}, # rescue handler blocks
25
- base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class
26
- all_rescue_handler: nil # rescue handler block to rescue from all exceptions
27
- }
28
- end
29
-
30
- def initialize(app, *options)
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
18
+
19
+ def initialize(app, **options)
31
20
  super
32
- self.class.send(:include, @options[:helpers]) if @options[:helpers]
21
+ self.class.include(options[:helpers]) if options[:helpers]
33
22
  end
34
23
 
35
24
  def call!(env)
36
25
  @env = env
37
- begin
38
- error_response(catch(:error) do
39
- return @app.call(@env)
40
- end)
41
- rescue Exception => e # rubocop:disable Lint/RescueException
42
- handler =
43
- rescue_handler_for_base_only_class(e.class) ||
44
- rescue_handler_for_class_or_its_ancestor(e.class) ||
45
- rescue_handler_for_grape_exception(e.class) ||
46
- rescue_handler_for_any_class(e.class) ||
47
- raise
48
-
49
- run_rescue_handler(handler, e)
50
- end
26
+ error_response(catch(:error) { return @app.call(@env) })
27
+ rescue Exception => e # rubocop:disable Lint/RescueException
28
+ run_rescue_handler(find_handler(e.class), e, @env[Grape::Env::API_ENDPOINT])
51
29
  end
52
30
 
53
- def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
54
- headers = headers.reverse_merge(Rack::CONTENT_TYPE => content_type)
55
- rack_response(format_message(message, backtrace, original_exception), status, headers)
31
+ private
32
+
33
+ def rack_response(status, headers, message)
34
+ message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == 'text/html'
35
+ Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), Grape::Util::Header.new.merge(headers))
56
36
  end
57
37
 
58
- def default_rescue_handler(e)
59
- error_response(message: e.message, backtrace: e.backtrace, original_exception: e)
38
+ def format_message(message, backtrace, original_exception = nil)
39
+ format = env[Grape::Env::API_FORMAT] || options[:format]
40
+ formatter = Grape::ErrorFormatter.formatter_for(format, options[:error_formatters], options[:default_error_formatter])
41
+ return formatter.call(message, backtrace, options, env, original_exception) if formatter
42
+
43
+ throw :error,
44
+ status: 406,
45
+ message: "The requested format '#{format}' is not supported.",
46
+ backtrace: backtrace,
47
+ original_exception: original_exception
48
+ end
49
+
50
+ def find_handler(klass)
51
+ rescue_handler_for_base_only_class(klass) ||
52
+ rescue_handler_for_class_or_its_ancestor(klass) ||
53
+ rescue_handler_for_grape_exception(klass) ||
54
+ rescue_handler_for_any_class(klass) ||
55
+ raise
60
56
  end
61
57
 
62
- # TODO: This method is deprecated. Refactor out.
63
58
  def error_response(error = {})
64
59
  status = error[:status] || options[:default_status]
60
+ env[Grape::Env::API_ENDPOINT].status(status) # error! may not have been called
65
61
  message = error[:message] || options[:default_message]
66
- headers = { Rack::CONTENT_TYPE => content_type }
67
- headers.merge!(error[:headers]) if error[:headers].is_a?(Hash)
62
+ headers = { Rack::CONTENT_TYPE => content_type }.tap do |h|
63
+ h.merge!(error[:headers]) if error[:headers].is_a?(Hash)
64
+ end
68
65
  backtrace = error[:backtrace] || error[:original_exception]&.backtrace || []
69
66
  original_exception = error.is_a?(Exception) ? error : error[:original_exception] || nil
70
- rack_response(format_message(message, backtrace, original_exception), status, headers)
67
+ rack_response(status, headers, format_message(message, backtrace, original_exception))
71
68
  end
72
69
 
73
- def rack_response(message, status = options[:default_status], headers = { Rack::CONTENT_TYPE => content_type })
74
- message = ERB::Util.html_escape(message) if headers[Rack::CONTENT_TYPE] == TEXT_HTML
75
- Rack::Response.new([message], Rack::Utils.status_code(status), headers)
70
+ def default_rescue_handler(exception)
71
+ error_response(message: exception.message, backtrace: exception.backtrace, original_exception: exception)
76
72
  end
77
73
 
78
- def format_message(message, backtrace, original_exception = nil)
79
- format = env[Grape::Env::API_FORMAT] || options[:format]
80
- formatter = Grape::ErrorFormatter.formatter_for(format, **options)
81
- throw :error,
82
- status: 406,
83
- message: "The requested format '#{format}' is not supported.",
84
- backtrace: backtrace,
85
- original_exception: original_exception unless formatter
86
- formatter.call(message, backtrace, options, env, original_exception)
87
- end
88
-
89
- private
90
-
91
74
  def rescue_handler_for_base_only_class(klass)
92
- error, handler = options[:base_only_rescue_handlers].find { |err, _handler| klass == err }
75
+ error, handler = options[:base_only_rescue_handlers]&.find { |err, _handler| klass == err }
93
76
 
94
77
  return unless error
95
78
 
96
- handler || :default_rescue_handler
79
+ handler || method(:default_rescue_handler)
97
80
  end
98
81
 
99
82
  def rescue_handler_for_class_or_its_ancestor(klass)
100
- error, handler = options[:rescue_handlers].find { |err, _handler| klass <= err }
83
+ error, handler = options[:rescue_handlers]&.find { |err, _handler| klass <= err }
101
84
 
102
85
  return unless error
103
86
 
104
- handler || :default_rescue_handler
87
+ handler || method(:default_rescue_handler)
105
88
  end
106
89
 
107
90
  def rescue_handler_for_grape_exception(klass)
108
91
  return unless klass <= Grape::Exceptions::Base
109
- return :error_response if klass == Grape::Exceptions::InvalidVersionHeader
92
+ return method(:error_response) if klass == Grape::Exceptions::InvalidVersionHeader
110
93
  return unless options[:rescue_grape_exceptions] || !options[:rescue_all]
111
94
 
112
- options[:grape_exceptions_rescue_handler] || :error_response
95
+ options[:grape_exceptions_rescue_handler] || method(:error_response)
113
96
  end
114
97
 
115
98
  def rescue_handler_for_any_class(klass)
116
99
  return unless klass <= StandardError
117
100
  return unless options[:rescue_all] || options[:rescue_grape_exceptions]
118
101
 
119
- options[:all_rescue_handler] || :default_rescue_handler
102
+ options[:all_rescue_handler] || method(:default_rescue_handler)
120
103
  end
121
104
 
122
- def run_rescue_handler(handler, error)
105
+ def run_rescue_handler(handler, error, endpoint)
123
106
  if handler.instance_of?(Symbol)
124
107
  raise NoMethodError, "undefined method '#{handler}'" unless respond_to?(handler)
125
108
 
126
109
  handler = public_method(handler)
127
110
  end
128
111
 
129
- response = handler.arity.zero? ? instance_exec(&handler) : instance_exec(error, &handler)
112
+ response = catch(:error) do
113
+ handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler)
114
+ end
130
115
 
131
- if response.is_a?(Rack::Response)
116
+ if error?(response)
117
+ error_response(response)
118
+ elsif response.is_a?(Rack::Response)
132
119
  response
133
120
  else
134
- run_rescue_handler(:default_rescue_handler, Grape::Exceptions::InvalidResponse.new)
121
+ run_rescue_handler(method(:default_rescue_handler), Grape::Exceptions::InvalidResponse.new, endpoint)
135
122
  end
136
123
  end
124
+
125
+ def error!(message, status = options[:default_status], headers = {}, backtrace = [], original_exception = nil)
126
+ env[Grape::Env::API_ENDPOINT].status(status) # not error! inside route
127
+ rack_response(
128
+ status, headers.reverse_merge(Rack::CONTENT_TYPE => content_type),
129
+ format_message(message, backtrace, original_exception)
130
+ )
131
+ end
132
+
133
+ def error?(response)
134
+ return false unless response.is_a?(Hash)
135
+
136
+ response.key?(:message) && response.key?(:status) && response.key?(:headers)
137
+ end
137
138
  end
138
139
  end
139
140
  end
@@ -1,19 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  class Formatter < Base
8
- CHUNKED = 'chunked'
9
-
10
- def default_options
11
- {
12
- default_format: :txt,
13
- formatters: {},
14
- parsers: {}
15
- }
16
- end
6
+ DEFAULT_OPTIONS = {
7
+ default_format: :txt
8
+ }.freeze
17
9
 
18
10
  def before
19
11
  negotiate_content_type
@@ -26,7 +18,7 @@ module Grape
26
18
  status, headers, bodies = *@app_response
27
19
 
28
20
  if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
29
- @app_response
21
+ [status, headers, []]
30
22
  else
31
23
  build_formatted_response(status, headers, bodies)
32
24
  end
@@ -54,8 +46,8 @@ module Grape
54
46
  end
55
47
 
56
48
  def fetch_formatter(headers, options)
57
- api_format = mime_types[headers[Rack::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT]
58
- Grape::Formatter.formatter_for(api_format, **options)
49
+ api_format = env.fetch(Grape::Env::API_FORMAT) { mime_types[headers[Rack::CONTENT_TYPE]] }
50
+ Grape::Formatter.formatter_for(api_format, options[:formatters])
59
51
  end
60
52
 
61
53
  # Set the content type header for the API format if it is not already present.
@@ -70,45 +62,38 @@ module Grape
70
62
  end
71
63
  end
72
64
 
73
- def request
74
- @request ||= Rack::Request.new(env)
75
- end
76
-
77
- # store read input in env['api.request.input']
78
65
  def read_body_input
79
- return unless
80
- (request.post? || request.put? || request.patch? || request.delete?) &&
81
- (!request.form_data? || !request.media_type) &&
82
- !request.parseable_data? &&
83
- (request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
84
-
85
- return unless (input = env[Grape::Env::RACK_INPUT])
66
+ input = rack_request.body # reads RACK_INPUT
67
+ return if input.nil?
68
+ return unless read_body_input?
86
69
 
87
- input.rewind
70
+ input.try(:rewind)
88
71
  body = env[Grape::Env::API_REQUEST_INPUT] = input.read
89
72
  begin
90
- read_rack_input(body) if body && !body.empty?
73
+ read_rack_input(body)
91
74
  ensure
92
- input.rewind
75
+ input.try(:rewind)
93
76
  end
94
77
  end
95
78
 
96
- # store parsed input in env['api.request.body']
97
79
  def read_rack_input(body)
98
- fmt = request.media_type ? mime_types[request.media_type] : options[:default_format]
80
+ return if body.empty?
99
81
 
100
- throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt)
101
- parser = Grape::Parser.parser_for fmt, **options
82
+ media_type = rack_request.media_type
83
+ fmt = media_type ? mime_types[media_type] : options[:default_format]
84
+
85
+ throw :error, status: 415, message: "The provided content-type '#{media_type}' is not supported." unless content_type_for(fmt)
86
+ parser = Grape::Parser.parser_for fmt, options[:parsers]
102
87
  if parser
103
88
  begin
104
89
  body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
105
90
  if body.is_a?(Hash)
106
- env[Grape::Env::RACK_REQUEST_FORM_HASH] = if env.key?(Grape::Env::RACK_REQUEST_FORM_HASH)
107
- env[Grape::Env::RACK_REQUEST_FORM_HASH].merge(body)
108
- else
109
- body
110
- end
111
- env[Grape::Env::RACK_REQUEST_FORM_INPUT] = env[Grape::Env::RACK_INPUT]
91
+ env[Rack::RACK_REQUEST_FORM_HASH] = if env.key?(Rack::RACK_REQUEST_FORM_HASH)
92
+ env[Rack::RACK_REQUEST_FORM_HASH].merge(body)
93
+ else
94
+ body
95
+ end
96
+ env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT]
112
97
  end
113
98
  rescue Grape::Exceptions::Base => e
114
99
  raise e
@@ -120,59 +105,43 @@ module Grape
120
105
  end
121
106
  end
122
107
 
108
+ # this middleware will not try to format the following content-types since Rack already handles them
109
+ # when calling Rack's `params` function
110
+ # - application/x-www-form-urlencoded
111
+ # - multipart/form-data
112
+ # - multipart/related
113
+ # - multipart/mixed
114
+ def read_body_input?
115
+ (rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?) &&
116
+ !(rack_request.form_data? && rack_request.content_type) &&
117
+ !rack_request.parseable_data? &&
118
+ (rack_request.content_length.to_i.positive? || rack_request.env['HTTP_TRANSFER_ENCODING'] == 'chunked')
119
+ end
120
+
123
121
  def negotiate_content_type
124
- fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
122
+ fmt = format_from_extension || query_params['format'] || options[:format] || format_from_header || options[:default_format]
125
123
  if content_type_for(fmt)
126
- env[Grape::Env::API_FORMAT] = fmt
124
+ env[Grape::Env::API_FORMAT] = fmt.to_sym
127
125
  else
128
126
  throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
129
127
  end
130
128
  end
131
129
 
132
130
  def format_from_extension
133
- parts = request.path.split('.')
131
+ request_path = rack_request.path.try(:scrub)
132
+ dot_pos = request_path.rindex('.')
133
+ return unless dot_pos
134
134
 
135
- if parts.size > 1
136
- extension = parts.last
137
- # avoid symbol memory leak on an unknown format
138
- return extension.to_sym if content_type_for(extension)
139
- end
140
- nil
141
- end
142
-
143
- def format_from_params
144
- fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT]
145
- # avoid symbol memory leak on an unknown format
146
- return fmt.to_sym if content_type_for(fmt)
147
-
148
- fmt
135
+ extension = request_path[dot_pos + 1..]
136
+ extension if content_type_for(extension)
149
137
  end
150
138
 
151
139
  def format_from_header
152
- mime_array.each do |t|
153
- return mime_types[t] if mime_types.key?(t)
154
- end
155
- nil
156
- end
157
-
158
- def mime_array
159
- accept = env[Grape::Http::Headers::HTTP_ACCEPT]
160
- return [] unless accept
161
-
162
- accept_into_mime_and_quality = %r{
163
- (
164
- \w+/[\w+.-]+) # eg application/vnd.example.myformat+xml
165
- (?:
166
- (?:;[^,]*?)? # optionally multiple formats in a row
167
- ;\s*q=([\w.]+) # optional "quality" preference (eg q=0.5)
168
- )?
169
- }x
170
-
171
- vendor_prefix_pattern = /vnd\.[^+]+\+/
140
+ accept_header = env['HTTP_ACCEPT'].try(:scrub)
141
+ return if accept_header.blank?
172
142
 
173
- accept.scan(accept_into_mime_and_quality)
174
- .sort_by { |_, quality_preference| -(quality_preference ? quality_preference.to_f : 1.0) }
175
- .flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] }
143
+ media_type = Rack::Utils.best_q_match(accept_header, mime_types.keys)
144
+ mime_types[media_type] if media_type
176
145
  end
177
146
  end
178
147
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/middleware/base'
4
-
5
3
  module Grape
6
4
  module Middleware
7
5
  class Globals < Base
@@ -9,7 +7,7 @@ module Grape
9
7
  request = Grape::Request.new(@env, build_params_with: @options[:build_params_with])
10
8
  @env[Grape::Env::GRAPE_REQUEST] = request
11
9
  @env[Grape::Env::GRAPE_REQUEST_HEADERS] = request.headers
12
- @env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Grape::Env::RACK_INPUT]
10
+ @env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Rack::RACK_INPUT]
13
11
  end
14
12
  end
15
13
  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,33 +45,17 @@ 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 [](i)
61
- middlewares[i]
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
 
@@ -74,39 +63,39 @@ module Grape
74
63
  index = assert_index(index, :after)
75
64
  insert(index + 1, *args, &block)
76
65
  end
77
- ruby2_keywords :insert_after if respond_to?(:ruby2_keywords, true)
78
66
 
79
- def use(*args, &block)
80
- middleware = self.class::Middleware.new(*args, &block)
67
+ def use(klass, *args, &block)
68
+ middleware = self.class::Middleware.new(klass, args, block)
81
69
  middlewares.push(middleware)
82
70
  end
83
- ruby2_keywords :use if respond_to?(:ruby2_keywords, true)
84
71
 
85
72
  def merge_with(middleware_specs)
86
- middleware_specs.each do |operation, *args|
73
+ middleware_specs.each do |operation, klass, *args|
87
74
  if args.last.is_a?(Proc)
88
75
  last_proc = args.pop
89
- public_send(operation, *args, &last_proc)
76
+ public_send(operation, klass, *args, &last_proc)
90
77
  else
91
- public_send(operation, *args)
78
+ public_send(operation, klass, *args)
92
79
  end
93
80
  end
94
81
  end
95
82
 
96
83
  # @return [Rack::Builder] the builder object with our middlewares applied
97
- def build(builder = Rack::Builder.new)
98
- others.shift(others.size).each { |m| merge_with(m) }
99
- middlewares.each do |m|
100
- 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
101
90
  end
102
- builder
103
91
  end
104
92
 
105
93
  # @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later
106
94
  # @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]])
107
95
  def concat(other_specs)
108
- @others << Array(other_specs).reject { |o| o.first == :use }
109
- 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)
110
99
  end
111
100
 
112
101
  protected