Support for prepending/appending middlewares (#1176)

* Extract router_options

* Remove unneeded rubocop:disable

* Fix Hanami.rack_app to always return rack builder

* Add support for prepanding/appending middlewares
This commit is contained in:
Peter Solnica 2022-06-30 07:27:56 +02:00 committed by GitHub
parent c816219bcd
commit 597a37b6fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 241 additions and 115 deletions

View File

@ -261,16 +261,13 @@ module Hanami
def load_router
require_relative "application/router"
Router.new(
routes: load_routes,
resolver: router_resolver,
**configuration.router.options,
) do
use Hanami.application[:rack_monitor]
config = configuration
Hanami.application.config.for_each_middleware do |m, *args, &block|
use(m, *args, &block)
end
Router.new(routes: load_routes, resolver: router_resolver, **router_options) do
use Hanami.application[:rack_monitor]
use config.sessions.middleware if config.sessions.enabled?
middleware_stack.update(config.middleware_stack)
end
end
@ -288,6 +285,10 @@ module Hanami
proc {}
end
def router_options
configuration.router.options
end
def router_resolver
config.router.resolver.new(slice: self)
end

View File

@ -8,6 +8,9 @@ module Hanami
# Hanami application router
# @since 2.0.0
class Router < ::Hanami::Router
# @api private
attr_reader :middleware_stack
# @since 2.0.0
# @api private
def initialize(routes:, middleware_stack: Routing::Middleware::Stack.new, **kwargs, &blk)
@ -27,14 +30,14 @@ module Hanami
# @since 2.0.0
# @api private
def use(middleware, *args, &blk)
@middleware_stack.use(middleware, *args, &blk)
def use(...)
middleware_stack.use(...)
end
# @since 2.0.0
# @api private
def scope(*args, &blk)
@middleware_stack.with(args.first) do
middleware_stack.with(args.first) do
super
end
end
@ -52,9 +55,7 @@ module Hanami
# @since 2.0.0
# @api private
def to_rack_app
return self if @middleware_stack.empty?
@middleware_stack.to_rack_app(self)
middleware_stack.to_rack_app(self)
end
end
end

View File

@ -20,6 +20,10 @@ module Hanami
ROOT_PREFIX = "/"
private_constant :ROOT_PREFIX
# @since 2.0.0
# @api private
attr_reader :stack
# @since 2.0.0
# @api private
def initialize
@ -29,8 +33,27 @@ module Hanami
# @since 2.0.0
# @api private
def use(middleware, *args, &blk)
@stack[@prefix].push([middleware, args, blk])
def use(middleware, *args, before: nil, after: nil, &blk)
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
@ -45,7 +68,7 @@ module Hanami
# @since 2.0.0
# @api private
def to_rack_app(app) # rubocop:disable Metrics/MethodLength
def to_rack_app(app)
s = self
Rack::Builder.new do
@ -58,7 +81,7 @@ module Hanami
run app
end
end.to_app
end
end
# @since 2.0.0
@ -82,6 +105,13 @@ module Hanami
builder.map(prefix, &blk)
end
end
private
# @since 2.0.0
def index_of(middleware)
@stack[@prefix].index { |(m, *)| m.equal?(middleware) }
end
end
end
end

View File

@ -9,10 +9,10 @@ require "pathname"
require_relative "settings/dotenv_store"
require_relative "configuration/logger"
require_relative "configuration/middleware"
require_relative "configuration/router"
require_relative "configuration/sessions"
require_relative "constants"
require_relative "application/routing/middleware/stack"
module Hanami
# Hanami application configuration
@ -28,7 +28,13 @@ module Hanami
attr_reader :env
attr_reader :actions
# @api public
attr_reader :middleware
# @api private
alias_method :middleware_stack, :middleware
attr_reader :router
attr_reader :views, :assets
@ -57,7 +63,7 @@ module Hanami
Actions.new
}
@middleware = Middleware.new
@middleware = Application::Routing::Middleware::Stack.new
@router = Router.new(self)
@ -113,13 +119,6 @@ module Hanami
setting :base_url, default: "http://0.0.0.0:2300", constructor: -> url { URI(url) }
def for_each_middleware(&blk)
stack = middleware.stack.dup
stack += sessions.middleware if sessions.enabled?
stack.each(&blk)
end
setting :sessions, default: :null, constructor: -> *args { Sessions.new(*args) }
private

View File

@ -1,20 +0,0 @@
# frozen_string_literal: true
module Hanami
class Configuration
# Hanami application configured Rack middleware
#
# @since 2.0.0
class Middleware
attr_reader :stack
def initialize
@stack = []
end
def use(middleware, *args, &block)
stack.push([middleware, *args, block].compact)
end
end
end
end

View File

@ -30,7 +30,7 @@ module Hanami
end
def middleware
[[storage_middleware, options]]
[storage_middleware, options, nil]
end
private

View File

@ -0,0 +1,152 @@
# frozen_string_literal: true
require "rack/test"
RSpec.describe "Hanami web app", :application_integration do
include Rack::Test::Methods
let(:app) { Hanami.rack_app }
around do |example|
with_tmp_directory(Dir.mktmpdir, &example)
end
before do
module TestApp
module Middlewares
class Core
def initialize(app)
@app = app
end
end
class Prepare < Core
def call(env)
env["tested"] = []
@app.call(env)
end
end
class AppendOne < Core
def call(env)
env["tested"] << "one"
@app.call(env)
end
end
class AppendTwo < Core
def call(env)
env["tested"] << "two"
@app.call(env)
end
end
end
end
end
specify "Setting middlewares in the config" do
write "config/application.rb", <<~RUBY
require "hanami"
module TestApp
class Application < Hanami::Application
config.logger.stream = File.new("/dev/null", "w")
config.middleware.use Middlewares::AppendOne
config.middleware.use Middlewares::Prepare, before: Middlewares::AppendOne
config.middleware.use Middlewares::AppendTwo, after: Middlewares::AppendOne
end
end
RUBY
write "config/routes.rb", <<~RUBY
require "hanami/router"
module TestApp
class Routes < Hanami::Routes
define do
slice :main, at: "/" do
root to: "home.index"
end
end
end
end
RUBY
write "slices/main/actions/home/index.rb", <<~RUBY
require "hanami/action"
module Main
module Actions
module Home
class Index < Hanami::Action
def handle(req, res)
res.body = req.env["tested"].join(".")
end
end
end
end
end
RUBY
require "hanami/boot"
get "/"
expect(last_response).to be_successful
expect(last_response.body).to eql("one.two")
end
specify "Setting middlewares in the router" do
write "config/application.rb", <<~RUBY
require "hanami"
module TestApp
class Application < Hanami::Application
config.logger.stream = File.new("/dev/null", "w")
end
end
RUBY
write "config/routes.rb", <<~RUBY
require "hanami/router"
module TestApp
class Routes < Hanami::Routes
define do
slice :main, at: "/" do
use TestApp::Middlewares::AppendOne
use TestApp::Middlewares::Prepare, before: TestApp::Middlewares::AppendOne
use TestApp::Middlewares::AppendTwo, after: TestApp::Middlewares::AppendOne
root to: "home.index"
end
end
end
end
RUBY
write "slices/main/actions/home/index.rb", <<~RUBY
require "hanami/action"
module Main
module Actions
module Home
class Index < Hanami::Action
def handle(req, res)
res.body = req.env["tested"].join(".")
end
end
end
end
end
RUBY
require "hanami/boot"
get "/"
expect(last_response).to be_successful
expect(last_response.body).to eql("one.two")
end
end

View File

@ -7,7 +7,34 @@ RSpec.describe "Hanami web app", :application_integration do
let(:app) { Hanami.rack_app }
specify "has rack monitor preconfigured with default request logging" do
specify "Hanami.rack_app returns a rack builder" do
with_tmp_directory do
write "config/application.rb", <<~RUBY
require "hanami"
module TestApp
class Application < Hanami::Application
end
end
RUBY
write "config/routes.rb", <<~RUBY
module TestApp
class Routes < Hanami::Routes
define do
root to: ->(env) { [200, {}, ["OK"]] }
end
end
end
RUBY
require "hanami/boot"
expect(app).to be_instance_of(Rack::Builder)
end
end
specify "Has rack monitor preconfigured with default request logging" do
dir = Dir.mktmpdir
with_tmp_directory(dir) do

View File

@ -5,6 +5,7 @@ SPEC_ROOT = File.expand_path(__dir__).freeze
require_relative "support/coverage" if ENV["COVERAGE"].eql?("true")
require "hanami"
begin; require "byebug"; rescue LoadError; end
require "hanami/utils/file_list"
require "hanami/devtools/unit"

View File

@ -1,65 +0,0 @@
# frozen_string_literal: true
require "hanami/configuration"
RSpec.describe Hanami::Configuration do
subject(:config) { described_class.new(application_name: application_name, env: :development) }
let(:application_name) { "MyApp::Application" }
describe "#middleware" do
it "defaults to a stack with no configured middlewares" do
expect(config.middleware).to be_kind_of(Hanami::Configuration::Middleware)
expect(config.middleware.stack.count).to be(0)
end
describe "#use" do
it "adds a middleware object to the configured stack" do
middleware = -> * {}
expect { config.middleware.use middleware }
.to change { config.middleware.stack }
.to [[middleware]]
end
it "adds a middleware class with options and a block to the configured stack" do
klass, opts, block = Class.new, {options: :here}, -> * {}
expect { config.middleware.use(klass, opts, &block) }
.to change { config.middleware.stack }
.to [[klass, opts, block]]
end
end
end
describe "#for_each_middleware" do
let(:middleware_item_1) { [Class.new, {options: :here}, -> * {}] }
let(:middleware_item_2) { [Class.new, {options: :here}, -> * {}] }
before do
config.middleware.use(*middleware_item_1)
config.middleware.use(*middleware_item_2)
end
context "sessions not enabled" do
it "yields each given middleware" do
expect { |b| config.for_each_middleware(&b) }
.to yield_successive_args middleware_item_1, middleware_item_2
end
end
context "sessions enabled" do
before do
config.sessions = :cookie, {secret: "xyz"}
end
it "yields each given middleware, with the session middleware added last" do
expect { |b| config.for_each_middleware(&b) }
.to yield_successive_args(
middleware_item_1,
middleware_item_2,
array_including(an_object_satisfying { |klass| klass.name =~ /^Rack::Session/ })
)
end
end
end
end