From 03c1d61ff0b15bbada49f22ca492c2094ea1c783 Mon Sep 17 00:00:00 2001 From: Geoff Buesing Date: Sun, 17 Jan 2010 11:47:11 -0600 Subject: [PATCH] Rack::SimpleEndpoint - Create simple endpoints with routing rules, similar to Sinatra actions --- README.rdoc | 1 + lib/rack/contrib.rb | 1 + lib/rack/contrib/simple_endpoint.rb | 81 +++++++++++++++++++++++++++++ test/spec_rack_simple_endpoint.rb | 95 +++++++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 0 deletions(-) create mode 100644 lib/rack/contrib/simple_endpoint.rb create mode 100644 test/spec_rack_simple_endpoint.rb diff --git a/README.rdoc b/README.rdoc index ca015ae..a54de17 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::SimpleEndpoint - Create simple endpoints with routing rules, similar to Sinatra actions === Use diff --git a/lib/rack/contrib.rb b/lib/rack/contrib.rb index 8905437..66ea66b 100644 --- a/lib/rack/contrib.rb +++ b/lib/rack/contrib.rb @@ -26,6 +26,7 @@ module Rack autoload :Runtime, "rack/contrib/runtime" autoload :Sendfile, "rack/contrib/sendfile" autoload :Signals, "rack/contrib/signals" + autoload :SimpleEndpoint, "rack/contrib/simple_endpoint" autoload :TimeZone, "rack/contrib/time_zone" autoload :Evil, "rack/contrib/evil" autoload :Callbacks, "rack/contrib/callbacks" diff --git a/lib/rack/contrib/simple_endpoint.rb b/lib/rack/contrib/simple_endpoint.rb new file mode 100644 index 0000000..135426c --- /dev/null +++ b/lib/rack/contrib/simple_endpoint.rb @@ -0,0 +1,81 @@ +module Rack + # Create simple endpoints with routing rules, similar to Sinatra actions. + # + # Simplest example: + # + # use Rack::SimpleEndpoint, '/ping_monitor' do + # 'pong' + # end + # + # The value returned from the block will be written to the response body, so + # the above example will return "pong" when the request path is /ping_monitor. + # + # HTTP verb requirements can optionally be specified: + # + # use Rack::SimpleEndpoint, '/foo' => :get do + # 'only GET requests will match' + # end + # + # use Rack::SimpleEndpoint, '/bar' => [:get, :post] do + # 'only GET and POST requests will match' + # end + # + # Rack::Request and Rack::Response objects are yielded to block: + # + # use Rack::SimpleEndpoint, '/json' do |req, res| + # res['Content-Type'] = 'application/json' + # %({"foo": "#{req[:foo]}"}) + # end + # + # When path is a Regexp, match data object is yielded as third argument to block + # + # use Rack::SimpleEndpoint, %r{^/(john|paul|george|ringo)} do |req, res, match| + # "Hello, #{match[1]}" + # end + # + # A :pass symbol returned from block will not return a response; control will continue down the + # Rack stack: + # + # use Rack::SimpleEndpoint, '/api_key' do |req, res| + # req.env['myapp.user'].authorized? ? '12345' : :pass + # end + # + # # Unauthorized access to /api_key will be handled by PublicApp + # run PublicApp + class SimpleEndpoint + def initialize(app, arg, &block) + @app = app + @path = extract_path(arg) + @verbs = extract_verbs(arg) + @block = block + end + + def call(env) + match = match_path(env['PATH_INFO']) + if match && valid_method?(env['REQUEST_METHOD']) + req, res = Request.new(env), Response.new + body = @block.call(req, res, (match unless match == true)) + body == :pass ? @app.call(env) : (res.write(body); res.finish) + else + @app.call(env) + end + end + + private + def extract_path(arg) + arg.is_a?(Hash) ? arg.keys.first : arg + end + + def extract_verbs(arg) + arg.is_a?(Hash) ? [arg.values.first].flatten.map {|verb| verb.to_s.upcase} : [] + end + + def match_path(path) + @path.is_a?(Regexp) ? @path.match(path.to_s) : @path == path.to_s + end + + def valid_method?(method) + @verbs.empty? || @verbs.include?(method) + end + end +end \ No newline at end of file diff --git a/test/spec_rack_simple_endpoint.rb b/test/spec_rack_simple_endpoint.rb new file mode 100644 index 0000000..e7cefd9 --- /dev/null +++ b/test/spec_rack_simple_endpoint.rb @@ -0,0 +1,95 @@ +require 'test/spec' +require 'rack' +require 'rack/contrib/simple_endpoint' + +context "Rack::SimpleEndpoint" do + setup do + @app = Proc.new { Rack::Response.new {|r| r.write "Downstream app"}.finish } + end + + specify "calls downstream app when no match" do + endpoint = Rack::SimpleEndpoint.new(@app, '/foo') { 'bar' } + status, headers, body = endpoint.call(Rack::MockRequest.env_for('/baz')) + status.should == 200 + body.body.should == ['Downstream app'] + end + + specify "calls downstream app when path matches but method does not" do + endpoint = Rack::SimpleEndpoint.new(@app, '/foo' => :get) { 'bar' } + status, headers, body = endpoint.call(Rack::MockRequest.env_for('/foo', :method => 'post')) + status.should == 200 + body.body.should == ['Downstream app'] + end + + specify "calls downstream app when path matches but block returns :pass" do + endpoint = Rack::SimpleEndpoint.new(@app, '/foo') { :pass } + status, headers, body = endpoint.call(Rack::MockRequest.env_for('/foo')) + status.should == 200 + body.body.should == ['Downstream app'] + end + + specify "returns endpoint response when path matches" do + endpoint = Rack::SimpleEndpoint.new(@app, '/foo') { 'bar' } + status, headers, body = endpoint.call(Rack::MockRequest.env_for('/foo')) + status.should == 200 + body.body.should == ['bar'] + end + + specify "returns endpoint response when path and single method requirement match" do + endpoint = Rack::SimpleEndpoint.new(@app, '/foo' => :get) { 'bar' } + status, headers, body = endpoint.call(Rack::MockRequest.env_for('/foo')) + status.should == 200 + body.body.should == ['bar'] + end + + specify "returns endpoint response when path and one of multiple method requirements match" do + endpoint = Rack::SimpleEndpoint.new(@app, '/foo' => [:get, :post]) { 'bar' } + status, headers, body = endpoint.call(Rack::MockRequest.env_for('/foo', :method => 'post')) + status.should == 200 + body.body.should == ['bar'] + end + + specify "returns endpoint response when path matches regex" do + endpoint = Rack::SimpleEndpoint.new(@app, /foo/) { 'bar' } + status, headers, body = endpoint.call(Rack::MockRequest.env_for('/bar/foo')) + status.should == 200 + body.body.should == ['bar'] + end + + specify "block yields Rack::Request and Rack::Response objects" do + endpoint = Rack::SimpleEndpoint.new(@app, '/foo') do |req, res| + assert_instance_of ::Rack::Request, req + assert_instance_of ::Rack::Response, res + end + endpoint.call(Rack::MockRequest.env_for('/foo')) + end + + specify "block yields MatchData object when Regex path matcher specified" do + endpoint = Rack::SimpleEndpoint.new(@app, /foo(.+)/) do |req, res, match| + assert_instance_of MatchData, match + assert_equal 'bar', match[1] + end + endpoint.call(Rack::MockRequest.env_for('/foobar')) + end + + specify "block does NOT yield MatchData object when String path matcher specified" do + endpoint = Rack::SimpleEndpoint.new(@app, '/foo') do |req, res, match| + assert_nil match + end + endpoint.call(Rack::MockRequest.env_for('/foo')) + end + + specify "response honors headers set in block" do + endpoint = Rack::SimpleEndpoint.new(@app, '/foo') {|req, res| res['X-Foo'] = 'bar'; 'baz' } + status, headers, body = endpoint.call(Rack::MockRequest.env_for('/foo')) + status.should == 200 + headers['X-Foo'].should == 'bar' + body.body.should == ['baz'] + end + + specify "sets Content-Length header" do + endpoint = Rack::SimpleEndpoint.new(@app, '/foo') {|req, res| 'bar' } + status, headers, body = endpoint.call(Rack::MockRequest.env_for('/foo')) + headers['Content-Length'].should == '3' + end +end -- 1.6.1