Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Rack::SimpleEndpoint: create simple endpoints with routing rules

This is similar to basic Sinatra actions but in middleware form.
  • Loading branch information...
commit 5496aaea9d875b0d5b7b6c37588328652e55e0df 1 parent 37be751
@gbuesing gbuesing authored rtomayko committed
View
1  README.rdoc
@@ -47,6 +47,7 @@ interface:
* Rack::Cookies - Adds simple cookie jar hash to env
* Rack::Access - Limit access based on IP address
* Rack::ResponseHeaders - Manipulate response headers object at runtime
+* Rack::SimpleEndpoint - Create simple endpoints with routing rules, similar to Sinatra actions
=== Use
View
1  lib/rack/contrib.rb
@@ -27,6 +27,7 @@ def self.release
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"
View
81 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
View
95 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
Please sign in to comment.
Something went wrong with that request. Please try again.