Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions lib/grape/middleware/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ class Base
attr_reader :app, :env, :options

# @param [Rack Application] app The standard argument for a Rack middleware.
# @param [Hash] options A hash of options, simply stored for use by subclasses.
# @param [Hash] options Options forwarded to the subclass. When the
# subclass declares an `Options` Data class, the kwargs are routed
# through it. Otherwise they are deep-merged with the subclass's
# `DEFAULT_OPTIONS` Hash (legacy path) and frozen.
def initialize(app, **options)
@app = app
@options = merge_default_options(options).freeze
@options = build_options(options)
@app_response = nil
end

Expand Down Expand Up @@ -78,6 +81,14 @@ def merge_headers(response)
end
end

def build_options(options)
# Search ancestors so subclasses (e.g. Versioner::Path → Versioner::Base)
# inherit their parent's Options Data class without redeclaring it.
return self.class::Options.new(**options) if self.class.const_defined?(:Options)

merge_default_options(options).freeze
end

def merge_default_options(options)
if respond_to?(:default_options)
default_options.deep_merge(options)
Expand Down
66 changes: 29 additions & 37 deletions lib/grape/middleware/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,42 @@ class Error < Base
extend Forwardable
include PrecomputedContentTypes

DEFAULT_OPTIONS = {
all_rescue_handler: nil,
base_only_rescue_handlers: nil,
default_error_formatter: nil,
default_message: '',
default_status: 500,
error_formatters: nil,
format: :txt,
grape_exceptions_rescue_handler: nil,
internal_grape_exceptions_rescue_handler: nil,
rescue_all: false,
rescue_grape_exceptions: false,
rescue_handlers: nil,
rescue_options: Grape::DSL::RescueOptions.new
}.freeze

attr_reader :all_rescue_handler, :base_only_rescue_handlers, :default_error_formatter,
:default_message, :default_status, :error_formatters, :format,
:grape_exceptions_rescue_handler, :internal_grape_exceptions_rescue_handler,
:rescue_all, :rescue_grape_exceptions, :rescue_handlers, :rescue_options
Options = Data.define(
:all_rescue_handler, :base_only_rescue_handlers, :content_types,
:default_error_formatter, :default_message, :default_status,
:error_formatters, :format,
:grape_exceptions_rescue_handler, :internal_grape_exceptions_rescue_handler,
:rescue_all, :rescue_grape_exceptions, :rescue_handlers, :rescue_options
) do
def initialize(
all_rescue_handler: nil, base_only_rescue_handlers: nil, content_types: nil,
default_error_formatter: nil, default_message: '', default_status: 500,
error_formatters: nil, format: :txt,
grape_exceptions_rescue_handler: nil, internal_grape_exceptions_rescue_handler: nil,
rescue_all: false, rescue_grape_exceptions: false, rescue_handlers: nil,
rescue_options: nil
)
# `rescue_options:` arrives nil from `Endpoint#error_middleware_options`
# when no `rescue_from` has been called — fall back to the documented
# defaults rather than letting nil propagate to `def_delegator
# :rescue_options, :backtrace`.
rescue_options ||= Grape::DSL::RescueOptions.new
super
end
end

def_delegators :options,
:all_rescue_handler, :base_only_rescue_handlers, :default_error_formatter,
:default_message, :default_status, :error_formatters, :format,
:grape_exceptions_rescue_handler, :internal_grape_exceptions_rescue_handler,
:rescue_all, :rescue_grape_exceptions, :rescue_handlers, :rescue_options

# +:backtrace+ / +:original_exception+ on the rescue options become
# +#include_backtrace+ / +#include_original_exception+ on the middleware,
# which is what the formatter call site reads.
def_delegator :rescue_options, :backtrace, :include_backtrace
def_delegator :rescue_options, :original_exception, :include_original_exception

def initialize(app, **options)
super
@all_rescue_handler = @options[:all_rescue_handler]
@base_only_rescue_handlers = @options[:base_only_rescue_handlers]
@default_error_formatter = @options[:default_error_formatter]
@default_message = @options[:default_message]
@default_status = @options[:default_status]
@error_formatters = @options[:error_formatters]
@format = @options[:format]
@grape_exceptions_rescue_handler = @options[:grape_exceptions_rescue_handler]
@internal_grape_exceptions_rescue_handler = @options[:internal_grape_exceptions_rescue_handler]
@rescue_all = @options[:rescue_all]
@rescue_grape_exceptions = @options[:rescue_grape_exceptions]
@rescue_handlers = @options[:rescue_handlers]
@rescue_options = @options[:rescue_options] || Grape::DSL::RescueOptions.new
end

def call!(env)
@env = env
error_response(catch(:error) { return @app.call(@env) })
Expand Down
25 changes: 8 additions & 17 deletions lib/grape/middleware/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,18 @@
module Grape
module Middleware
class Formatter < Base
extend Forwardable
include PrecomputedContentTypes

DEFAULT_OPTIONS = {
content_types: nil,
default_format: :txt,
format: nil,
formatters: nil,
parsers: nil
}.freeze
Options = Data.define(:content_types, :default_format, :format, :formatters, :parsers) do
def initialize(content_types: nil, default_format: :txt, format: nil, formatters: nil, parsers: nil)
super
end
end

ALL_MEDIA_TYPES = '*/*'

attr_reader :default_format, :format, :formatters, :parsers

def initialize(app, **options)
super
@default_format = @options[:default_format]
@format = @options[:format]
@formatters = @options[:formatters]
@parsers = @options[:parsers]
end
def_delegators :options, :default_format, :format, :formatters, :parsers

def before
negotiate_content_type
Expand Down Expand Up @@ -101,7 +92,7 @@ def read_rack_input(body)
fmt = media_type ? mime_types[media_type] : default_format

throw :error, Grape::Exceptions::ErrorResponse.new(status: 415, message: "The provided content-type '#{media_type}' is not supported.") unless content_type_for(fmt)
parser = Grape::Parser.parser_for fmt, options[:parsers]
parser = Grape::Parser.parser_for fmt, parsers
return env[Grape::Env::API_REQUEST_BODY] = body unless parser

begin
Expand Down
11 changes: 6 additions & 5 deletions lib/grape/middleware/precomputed_content_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ module Grape
module Middleware
# Include in a middleware subclass that needs content-type negotiation.
# Provides +content_types+ / +mime_types+ / +content_type_for+ /
# +content_type+ resolved from +options[:content_types]+ and
# +options[:format]+, and warms those caches on the parent instance at
# initialization so per-request +dup+s inherit them (avoiding
# +content_type+ resolved from +options.content_types+ and
# +options.format+ — so the consuming middleware's +Options+ Data class
# must declare both fields. Warms those caches on the parent instance
# at initialization so per-request +dup+s inherit them (avoiding
# ~1 µs/request of +with_indifferent_access+ recomputation).
#
# Opt-in: plain +Grape::Middleware::Base+ subclasses that don't need
Expand All @@ -20,7 +21,7 @@ def initialize(app, **options)
end

def content_types
@content_types ||= Grape::ContentTypes.content_types_for(options[:content_types])
@content_types ||= Grape::ContentTypes.content_types_for(options.content_types)
end

def mime_types
Expand All @@ -32,7 +33,7 @@ def content_type_for(format)
end

def content_type
content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || 'text/html'
content_type_for(env[Grape::Env::API_FORMAT] || options.format) || 'text/html'
end

private
Expand Down
26 changes: 13 additions & 13 deletions lib/grape/middleware/versioner/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ class Base < Grape::Middleware::Base
extend Forwardable
include Grape::Middleware::PrecomputedContentTypes

DEFAULT_OPTIONS = {
mount_path: nil,
pattern: /.*/i,
prefix: nil,
version_options: Grape::DSL::VersionOptions.new
}.freeze
Options = Data.define(
:content_types, :format, :mount_path, :pattern, :prefix, :version_options, :versions
) do
def initialize(
content_types: nil, format: nil, mount_path: nil, pattern: /.*/i, prefix: nil,
version_options: Grape::DSL::VersionOptions.new, versions: nil
)
super
end
end

CASCADE_PASS_HEADER = { 'X-Cascade' => 'pass' }.freeze

Expand All @@ -21,18 +25,14 @@ def self.inherited(klass)
Versioner.register(klass)
end

attr_reader :available_media_types, :error_headers, :mount_path, :pattern,
:prefix, :version_options, :versions
attr_reader :available_media_types, :error_headers, :versions

def_delegators :options, :mount_path, :pattern, :prefix, :version_options
def_delegators :version_options, :cascade, :parameter, :strict, :vendor

def initialize(app, **options)
super
@version_options = @options[:version_options]
@mount_path = @options[:mount_path]
@pattern = @options[:pattern]
@prefix = @options[:prefix]
@versions = @options[:versions]&.map(&:to_s) # making sure versions are strings to ease potential match
@versions = self.options.versions&.map(&:to_s) # making sure versions are strings to ease potential match
@error_headers = cascade ? CASCADE_PASS_HEADER : {}
@available_media_types = build_available_media_types
end
Expand Down
1 change: 0 additions & 1 deletion spec/grape/middleware/formatter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,6 @@ def self.call(_, _)
it 'adds the backtrace and original_exception to the error output' do
subject = described_class.new(
app,
rescue_options: { backtrace: true, original_exception: true },
parsers: { json: ->(_object, _env) { raise StandardError, 'fail' } }
)
io = StringIO.new('{invalid}')
Expand Down
Loading