From 7ba372e5246c18ebe05c5935b9f6390d0171c7ec Mon Sep 17 00:00:00 2001 From: Geoff Buesing Date: Sun, 17 Jan 2010 09:59:50 -0600 Subject: [PATCH] Rack::Branch - Conditionally re-route the Rack pipeline to an alternate endpoint --- README.rdoc | 1 + lib/rack/contrib.rb | 1 + lib/rack/contrib/branch.rb | 47 ++++++++++++++++++++++++++ test/spec_rack_branch.rb | 79 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 0 deletions(-) create mode 100644 lib/rack/contrib/branch.rb create mode 100644 test/spec_rack_branch.rb diff --git a/README.rdoc b/README.rdoc index ca015ae..a913826 100644 --- a/README.rdoc +++ b/README.rdoc @@ -46,6 +46,7 @@ interface: * Rack::HostMeta - Configures /host-meta using a block * Rack::Cookies - Adds simple cookie jar hash to env * Rack::Access - Limit access based on IP address +* Rack::Branch - Conditionally re-route the Rack pipeline to an alternate endpoint === Use diff --git a/lib/rack/contrib.rb b/lib/rack/contrib.rb index 8905437..83a225e 100644 --- a/lib/rack/contrib.rb +++ b/lib/rack/contrib.rb @@ -10,6 +10,7 @@ module Rack autoload :AcceptFormat, "rack/contrib/accept_format" autoload :Access, "rack/contrib/access" autoload :BounceFavicon, "rack/contrib/bounce_favicon" + autoload :Branch, "rack/contrib/branch" autoload :Cookies, "rack/contrib/cookies" autoload :CSSHTTPRequest, "rack/contrib/csshttprequest" autoload :Deflect, "rack/contrib/deflect" diff --git a/lib/rack/contrib/branch.rb b/lib/rack/contrib/branch.rb new file mode 100644 index 0000000..a9edb00 --- /dev/null +++ b/lib/rack/contrib/branch.rb @@ -0,0 +1,47 @@ +module Rack + # Rack::Branch lets you conditionally re-route the Rack stack at runtime to + # an alternate endpoint. + # + # You initialize this middleware with a block, which should either 1. return a + # valid rack endpoint, when want to branch away from the current Rack pipeline, + # or 2. nil/false, when you want to continue on. The block is passed the current + # Rack env hash. + # + # config.ru usage example: + # + # use Rack::Branch do |env| + # ApiApp if env["PATH_INFO"] =~ /\.xml$/ + # end + # + # run MyEndpointApp + # + # + # X-Cascade Support: + # + # If the app returned from the block responds with an X-Cascade: pass header, + # control will be passed back to the main rack pipeline. + # + # In this contrived example, MyEndpointApp will always be called: + # + # use Rack::Branch do |env| + # Proc.new { [404, {'X-Cascade' => 'pass'}, []] } + # end + # + # run MyEndpointApp + class Branch + def initialize(app, &block) + @app = app + @block = block + end + + def call(env) + if branch_app = @block.call(env) + response = branch_app.call(env) + cascade = response[1]['X-Cascade'] + cascade == 'pass' ? @app.call(env) : response + else + @app.call(env) + end + end + end +end \ No newline at end of file diff --git a/test/spec_rack_branch.rb b/test/spec_rack_branch.rb new file mode 100644 index 0000000..3c63de5 --- /dev/null +++ b/test/spec_rack_branch.rb @@ -0,0 +1,79 @@ +require 'test/spec' +require 'rack/builder' +require 'rack/mock' +require 'rack/contrib/branch' + +context "Rack::Branch" do + + context "Unit Tests" do + setup do + @main_app = Proc.new { [200, {}, ["Main app"]] } + @blog_app = Proc.new { [200, {}, ["Blog app"]] } + @cascade_app = Proc.new { [404, {'X-Cascade' => 'pass'}, []] } + end + + specify "calls app returned from block" do + middleware = Rack::Branch.new(@main_app) { @blog_app } + response = middleware.call({}) + response.last.should == ['Blog app'] + end + + specify "calls app passed in to initialize when block returns false" do + middleware = Rack::Branch.new(@main_app) { false } + response = middleware.call({}) + response.last.should == ['Main app'] + end + + specify "calls app passed in to initialize when block returns nil" do + middleware = Rack::Branch.new(@main_app) { nil } + response = middleware.call({}) + response.last.should == ['Main app'] + end + + specify "calls app passed in to initialize when app returned from block returns X-Cascade: pass header" do + middleware = Rack::Branch.new(@main_app) { @cascade_app } + response = middleware.call({}) + response.last.should == ['Main app'] + end + end + + context "Integration Tests" do + setup do + main_app = Proc.new { Rack::Response.new {|r| r.body = "Main app"}.finish } + blog_app = Proc.new { Rack::Response.new {|r| r.body = "Blog app"}.finish } + cascade_app = Proc.new { Rack::Response.new {|r| r.status = 404; r['X-Cascade'] = 'pass'}.finish } + + outer_app = Rack::Builder.app do + use Rack::Lint + + use Rack::Branch do |env| + blog_app if env["PATH_INFO"] == '/blog' + end + + use Rack::Branch do |env| + cascade_app if env["PATH_INFO"] == '/cascade' + end + + run main_app + end + + @mock_request = Rack::MockRequest.new(outer_app) + end + + specify "returns blog response when path is /blog" do + response = @mock_request.get('/blog') + response.body.should.equal('Blog app') + end + + specify "returns main app response when path is /cascade" do + response = @mock_request.get('/cascade') + response.body.should.equal('Main app') + end + + specify "returns main app response when path is /foo" do + response = @mock_request.get('/foo') + response.body.should.equal('Main app') + end + end + +end -- 1.6.1