hanami/lib/hanami/slice/routing/middleware/stack.rb

224 lines
6.2 KiB
Ruby

# frozen_string_literal: true
require "hanami/middleware"
require "hanami/errors"
module Hanami
class Slice
module Routing
# @since 2.0.0
# @api private
module Middleware
# Wraps a rack app with a middleware stack
#
# We use this class to add middlewares to the rack application generated from
# {Hanami::Slice::Router}.
#
# ```
# stack = Hanami::Slice::Routing::Middleware::Stack.new
# stack.use(Rack::ContentType, "text/html")
# stack.to_rack_app(a_rack_app)
# ```
#
# Middlewares can be mounted on specific paths:
#
# ```
# stack.with("/api") do
# stack.use(Rack::ContentType, "application/json")
# end
# ```
#
# @see Hanami::Config#middleware
#
# @since 2.0.0
# @api private
class Stack
include Enumerable
# @since 2.0.0
# @api private
ROOT_PREFIX = "/"
private_constant :ROOT_PREFIX
# @since 2.0.0
# @api private
attr_reader :stack
# Returns an array of Ruby namespaces from which to load middleware classes specified by
# symbol names given to {#use}.
#
# Defaults to `[Hanami::Middleware]`.
#
# @return [Array<Object>]
#
# @api public
# @since 2.0.0
attr_reader :namespaces
# @since 2.0.0
# @api private
def initialize
@prefix = ROOT_PREFIX
@stack = Hash.new { |hash, key| hash[key] = [] }
@namespaces = [Hanami::Middleware]
end
# @since 2.0.0
# @api private
def initialize_copy(source)
super
@prefix = source.instance_variable_get(:@prefix).dup
@stack = stack.dup
@namespaces = namespaces.dup
end
# Adds a middleware to the stack.
#
# @example
# # Using a symbol name; adds Hanami::Middleware::BodyParser.new([:json])
# middleware.use :body_parser, :json
#
# # Using a class name
# middleware.use MyMiddleware
#
# # Adding a middleware before or after others
# middleware.use MyMiddleware, before: SomeMiddleware
# middleware.use MyMiddleware, after: OtherMiddleware
#
# @param spec [Symbol, Class] the middleware name or class name
# @param args [Array, nil] Arguments to pass to the middleware's `.new` method
# @param before [Class, nil] an optional (already added) middleware class to add the
# middleware before
# @param after [Class, nil] an optional (already added) middleware class to add the
# middleware after
#
# @return [self]
#
# @api public
# @since 2.0.0
def use(spec, *args, before: nil, after: nil, &blk)
middleware = resolve_middleware_class(spec)
item = [middleware, args, blk]
if before
@stack[@prefix].insert((idx = index_of(before)).zero? ? 0 : idx - 1, item)
elsif after
@stack[@prefix].insert(index_of(after) + 1, item)
else
@stack[@prefix].push([middleware, args, blk])
end
self
end
# @since 2.0.0
# @api private
def update(other)
other.stack.each do |prefix, items|
stack[prefix].concat(items)
end
self
end
# @since 2.0.0
# @api private
def with(path)
prefix = @prefix
@prefix = path
yield
ensure
@prefix = prefix
end
# @since 2.0.0
# @api private
def to_rack_app(app)
unless Hanami.bundled?("rack")
raise "Add \"rack\" to your `Gemfile` to run Hanami as a rack app"
end
require "rack/builder"
s = self
Rack::Builder.new do
s.each do |prefix, stack|
s.mapped(self, prefix) do
stack.each do |middleware, args, blk|
use(middleware, *args, &blk)
end
end
run app
end
end
end
# @since 2.0.0
# @api private
def empty?
@stack.empty?
end
# @since 2.0.0
# @api private
def each(&blk)
@stack.each(&blk)
end
# @since 2.0.0
# @api private
def mapped(builder, prefix, &blk)
if prefix == ROOT_PREFIX
builder.instance_eval(&blk)
else
builder.map(prefix, &blk)
end
end
private
# @since 2.0.0
def index_of(middleware)
@stack[@prefix].index { |(m, *)| m.equal?(middleware) }
end
# @since 2.0.0
def resolve_middleware_class(spec)
case spec
when Symbol then load_middleware_class(spec)
when Class, Module then spec
else
if spec.respond_to?(:call)
spec
else
raise UnsupportedMiddlewareSpecError, spec
end
end
end
# @since 2.0.0
def load_middleware_class(spec)
begin
require "hanami/middleware/#{spec}"
rescue LoadError # rubocop:disable Lint/SuppressedException
end
class_name = Hanami::Utils::String.classify(spec.to_s)
namespace = namespaces.detect { |ns| ns.const_defined?(class_name) }
if namespace
namespace.const_get(class_name)
else
raise(
UnsupportedMiddlewareSpecError,
"Failed to find corresponding middleware class for `#{spec}` in #{namespaces.join(', ')}"
)
end
end
end
end
end
end
end