From e387910ba1c625b5f4a8338d12a4e72ee014cb0c Mon Sep 17 00:00:00 2001 From: Gaius Novus Date: Fri, 29 May 2009 00:06:33 -0400 Subject: added support for Rack::Builder to act as a middleware; added specs --- lib/rack/builder.rb | 139 ++++++++++++++++++++++++++++++++++---------- test/spec_rack_builder.rb | 28 +++++++++ 2 files changed, 135 insertions(+), 32 deletions(-) diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb index 295235e..c46b8b7 100644 --- a/lib/rack/builder.rb +++ b/lib/rack/builder.rb @@ -1,8 +1,8 @@ module Rack # Rack::Builder implements a small DSL to iteratively construct Rack - # applications. + # applications and middlewares. # - # Example: + # Example (as an application) # # app = Rack::Builder.new { # use Rack::CommonLogger @@ -13,51 +13,126 @@ module Rack # end # } # - # Or + # Or (as another application) # # app = Rack::Builder.app do # use Rack::CommonLogger # lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'OK'] } # end # + # Or (as a middleware within an outer application) + # app = Rack::Builder.app do + # use Rack::CommonLogger # for all requests + # use Rack::ShowExceptions # for all requests + # use Rack::Builder do + # map '/secret' do + # use MySpecialLogger # only for secret stuff + # use Rack::Auth::Basic do |u,p| # only for secret stuff + # ... + # end + # # no lambda or run! + # end + # end + # lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'OK'] } + # end + # # +use+ adds a middleware to the stack, +run+ dispatches to an application. # You can use +map+ to construct a Rack::URLMap in a convenient way. - class Builder - def initialize(&block) - @ins = [] - instance_eval(&block) if block_given? + module Builder + + # A factory method for creating new builders. If +underlying_app+ is + # passed (either directly, or as part of a "use" call within another + # Builder), creates a Middleware version; otherwise, creates an + # Application version. + def self.new(underlying_app = nil, &block) + if underlying_app + Rack::Builder::Middleware.new(underlying_app, &block) + else + Rack::Builder::Application.new(&block) + end end - + + # Create a builder that acts as an application endpoint. def self.app(&block) - self.new(&block).to_app - end - - def use(middleware, *args, &block) - @ins << lambda { |app| middleware.new(app, *args, &block) } + new(&block).to_app end - - def run(app) - @ins << app #lambda { |nothing| app } + + # Create a builder that acts as a middleware. + def self.middleware(underlying_app, &block) + new(underlying_app, &block).to_middleware end - - def map(path, &block) - if @ins.last.kind_of? Hash - @ins.last[path] = self.class.new(&block).to_app - else - @ins << {} - map(path, &block) + + class Application + + include Builder # just in case clients ask app.kind_of?(Rack::Builder) + + def initialize(&block) + @ins = [] + instance_eval(&block) if block_given? end + + def use(middleware, *args, &block) + @ins << lambda { |app| middleware.new(app, *args, &block) } + end + + def to_app + @ins[-1] = Rack::URLMap.new(@ins.last) if Hash === @ins.last + inner_app = @ins.last + @ins[0...-1].reverse.inject(inner_app) { |a, e| e.call(a) } + end + + def run(app) + @ins << app + end + + def map(path, &block) + if @ins.last.kind_of? Hash + @ins.last[path] = self.class.new(&block).to_app + else + @ins << {} + map(path, &block) + end + end + + def call(env) + to_app.call(env) + end + end - - def to_app - @ins[-1] = Rack::URLMap.new(@ins.last) if Hash === @ins.last - inner_app = @ins.last - @ins[0...-1].reverse.inject(inner_app) { |a, e| e.call(a) } - end - - def call(env) - to_app.call(env) + + class Middleware + + include Builder # just in case clients ask app.kind_of?(Rack::Builder) + + def initialize(underlying_app, &block) + @middlewares = [] + @app = underlying_app + @map = { '/' => @app } + instance_eval(&block) if block_given? + end + + def to_middleware + @middlewares.reverse.inject(Rack::URLMap.new(@map)) { |a, m| m.call(a) } + end + + def use(middleware, *args, &block) + @middlewares << lambda { |app| middleware.new(app, *args, &block) } + end + + def map(path, &block) + @map[path] = self.class.new(@app, &block).to_middleware + end + + def call(env) + to_middleware.call(env) + end + + def run(*args) + raise NoMethodError.new("Rack::Builder used as a middleware can't run apps.") + end + end + end end diff --git a/test/spec_rack_builder.rb b/test/spec_rack_builder.rb index 3fad981..bb2efa0 100644 --- a/test/spec_rack_builder.rb +++ b/test/spec_rack_builder.rb @@ -80,5 +80,33 @@ context "Rack::Builder" do Rack::MockRequest.new(app).get("/").status.should.equal 200 Rack::MockRequest.new(app).get("/").should.be.server_error end + + specify "can be used as both an app and as middleware" do + app = Rack::Builder.app do + use Rack::Builder do + map '/secret' do + use Rack::Auth::Basic do |username, password| + 'secret' == password + end + end + end + run lambda { |env| [ 200, {}, 'Hi' ] } + end + + Rack::MockRequest.new(app).get('/').status.should.equal 200 + Rack::MockRequest.new(app).get('/secret/stuff').status.should.equal 401 + end + + specify "does not support #run when used as middleware" do + assert_raises(NoMethodError) do + Rack::Builder.app do + use Rack::ShowExceptions + use Rack::Builder do + run lambda { |env| raise "Shouldn't get here" } + end + run lambda { |env| [ 200, {}, "This endpoint is fine." ] } + end + end + end end -- 1.5.4