rackables 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,50 @@
1
+ = Rackables
2
+
3
+ == Middleware
4
+
5
+ Rackables bundles the following Rack middlewares:
6
+
7
+ * Rackables::Branch - Conditionally re-routes the Rack stack at runtime to an alternate endpoint
8
+ * Rackables::CacheControl - Sets response Cache-Control header
9
+ * Rackables::DefaultCharset - Sets charset directive in Content-Type header
10
+ * Rackables::Get - Allows creation of simple endpoints and path routing with a syntax similar to Sinatra's get method
11
+ * Rackables::HideExceptions - Rescues exceptions with a static exception page
12
+ * Rackables::TrailingSlashRedirect - 301 Redirects requests paths with a trailing slash
13
+
14
+ == More
15
+
16
+ Rackables also bundles a Rack environment inquirer similar to Rails.env:
17
+
18
+ Rack.env # => "development"
19
+ Rack.env.development? # => true
20
+
21
+ == Use
22
+
23
+ Requiring 'rackables' will add autoloads for all Rackables middlewares. To use the Rack.env inquirer,
24
+ you must explicitly require "rackables/more/env_inquirer".
25
+
26
+ Example config.ru:
27
+
28
+ require 'rackables'
29
+ require 'rackables/more/env_inquirer'
30
+ require 'my_endpont_app'
31
+
32
+ if Rack.env.development?
33
+ use Rack::ShowExceptions
34
+ else
35
+ use Rackables::HideExceptions, 'public/500.html'
36
+ end
37
+
38
+ use Rackables::TrailingSlashRedirect
39
+ use Rackables::CacheControl, :public, :max_age => 60
40
+ use Rackables::DefaultCharset, 'utf-8'
41
+
42
+ use Rackables::Get, '/ping_monitor' do
43
+ "pong"
44
+ end
45
+
46
+ use Rackables::Branch do |env|
47
+ Rack::Lobster if env["PATH_INFO"] =~ /^\/lobster$/
48
+ end
49
+
50
+ run MyEndpointApp
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.0
@@ -1,6 +1,8 @@
1
1
  module Rackables
2
- autoload :CacheControl, 'rackables/cache_control'
3
- autoload :DefaultCharset, 'rackables/default_charset'
4
- autoload :PublicExceptionPage, 'rackables/public_exception_page'
5
- autoload :TrailingSlashRedirect, 'rackables/trailing_slash_redirect'
2
+ autoload :Branch, 'rackables/branch'
3
+ autoload :CacheControl, 'rackables/cache_control'
4
+ autoload :DefaultCharset, 'rackables/default_charset'
5
+ autoload :Get, 'rackables/get'
6
+ autoload :HideExceptions, 'rackables/hide_exceptions'
7
+ autoload :TrailingSlashRedirect, 'rackables/trailing_slash_redirect'
6
8
  end
@@ -0,0 +1,39 @@
1
+ module Rackables
2
+ # Rackables::Branch lets you conditionally re-route the Rack stack at runtime to
3
+ # an alternate endpoint.
4
+ #
5
+ # You initialize this middleware with a block, which should either 1. return a
6
+ # valid rack endpoint, when want to branch away from the current Rack pipeline,
7
+ # or 2. nil/false, when you want to continue on. The block is passed the current
8
+ # Rack env hash.
9
+ #
10
+ # config.ru usage example:
11
+ #
12
+ # use Rackables::Branch do |env|
13
+ # ApiApp if env["PATH_INFO"] =~ /\.xml$/
14
+ # end
15
+ #
16
+ # run MyEndpointApp
17
+ #
18
+ # A slightly more complex example with multiple endpoints:
19
+ #
20
+ # use Rackables::Branch do |env|
21
+ # if env['PATH_INFO'] =~ %r{^\/foo\/(bar|baz)(.*)$/}
22
+ # env['PATH_INFO'] = $2
23
+ # {'bar' => BarApp, 'baz' => BazApp}[$1]
24
+ # end
25
+ # end
26
+ #
27
+ # run MyEndpointApp
28
+ class Branch
29
+ def initialize(app, &block)
30
+ @app = app
31
+ @block = block
32
+ end
33
+
34
+ def call(env)
35
+ app = @block.call(env) || @app
36
+ app.call(env)
37
+ end
38
+ end
39
+ end
@@ -1,21 +1,65 @@
1
1
  module Rackables
2
2
  class CacheControl
3
- # Works well with Varnish
4
- # TODO: accomodate more options than just public, max-age=
5
- def initialize(app, value, opts)
3
+ # Lets you set the Cache-Control response header from middleware. Does not overwrite
4
+ # existing Cache-Control response header, if it already has been set.
5
+ #
6
+ # Examples:
7
+ #
8
+ # use Rackables::CacheControl, :public, :max_age => 5
9
+ # # => Cache-Control: public, max-age=5
10
+ #
11
+ # use Rackables::CacheControl, :private, :must_revalidate, :community => "UCI"
12
+ # # => Cache-Control: private, must-revalidate, community="UCI"
13
+ #
14
+ # Values specified as a Proc will be called at runtime for each request:
15
+ #
16
+ # use Rackables::CacheControl, :public, :max_age => Proc.new { rand(6) + 3 }
17
+ def initialize(app, *directives)
6
18
  @app = app
7
- @value = value
8
- @opts = opts
19
+ @hash = extract_hash!(directives)
20
+ @directives = directives
21
+ extract_non_callable_values_from_hash!
22
+ stringify_hash_keys!
23
+ stringify_directives!
9
24
  end
10
25
 
11
26
  def call(env)
12
- status, headers, body = @app.call(env)
13
- if headers['Cache-Control'].nil?
14
- max_age = @opts[:max_age]
15
- max_age = max_age.call if max_age.respond_to?(:call)
16
- headers['Cache-Control'] = "#{@value}, max-age=#{max_age}"
27
+ response = @app.call(env)
28
+ headers = response[1]
29
+ unless headers.has_key?('Cache-Control')
30
+ value = @hash.empty? ? @directives : "#{@directives}, #{stringify_hash}"
31
+ headers['Cache-Control'] = value
17
32
  end
18
- [status, headers, body]
33
+ response
19
34
  end
35
+
36
+ private
37
+ def extract_hash!(array)
38
+ array.last.kind_of?(Hash) ? array.pop : {}
39
+ end
40
+
41
+ def extract_non_callable_values_from_hash!
42
+ @hash.reject! { |k,v| v == false }
43
+ @hash.reject! { |k,v| @directives << k if v == true }
44
+ @hash.reject! { |k,v| @directives << "#{k}=#{v.inspect}" if !v.respond_to?(:call) }
45
+ end
46
+
47
+ def stringify_hash_keys!
48
+ @hash.each do |key, value|
49
+ @hash[stringify_directive(key)] = @hash.delete(key)
50
+ end
51
+ end
52
+
53
+ def stringify_directives!
54
+ @directives = @directives.map {|d| stringify_directive(d)}.join(', ')
55
+ end
56
+
57
+ def stringify_hash
58
+ @hash.inject([]) {|arr, (k, v)| arr << "#{k}=#{v.call.inspect}"}.join(', ')
59
+ end
60
+
61
+ def stringify_directive(directive)
62
+ directive.to_s.tr('_','-')
63
+ end
20
64
  end
21
65
  end
@@ -1,4 +1,8 @@
1
1
  module Rackables
2
+ # Adds the specified charset to the Content-Type header, if one isn't
3
+ # already there.
4
+ #
5
+ # Ideal for use with Sinatra, which by default doesn't set charset
2
6
  class DefaultCharset
3
7
  HAS_CHARSET = /charset=/
4
8
 
@@ -8,12 +12,13 @@ module Rackables
8
12
  end
9
13
 
10
14
  def call(env)
11
- status, headers, body = @app.call(env)
15
+ response = @app.call(env)
16
+ headers = response[1]
12
17
  content_type = headers['Content-Type']
13
18
  if content_type && content_type !~ HAS_CHARSET
14
19
  headers['Content-Type'] = "#{content_type}; charset=#{@value}"
15
20
  end
16
- [status, headers, body]
21
+ response
17
22
  end
18
23
  end
19
24
  end
@@ -0,0 +1,45 @@
1
+ module Rackables
2
+ # Simplest example:
3
+ #
4
+ # use Rackables::Get, '/ping_monitor' do
5
+ # 'pong'
6
+ # end
7
+ #
8
+ # Rack::Response object is yielded as second argument to block:
9
+ #
10
+ # use Rackables::Get, '/json' do |env, response|
11
+ # response['Content-Type'] = 'application/json'
12
+ # %({"foo": "bar"})
13
+ # end
14
+ #
15
+ # Example with regular expression -- match data object is yielded as third argument to block
16
+ #
17
+ # use Rackables::Get, %r{^/(john|paul|george|ringo)} do |env, response, match|
18
+ # "Hello, #{match[1]}"
19
+ # end
20
+ #
21
+ # A false/nil return from block will not return a response; control will continue down the
22
+ # Rack stack:
23
+ #
24
+ # use Rackables::Get, '/api_key' do |env, response|
25
+ # '12345' if env['myapp.user'].authorized?
26
+ # end
27
+ class Get
28
+ def initialize(app, path, &block)
29
+ @app = app
30
+ @path = path
31
+ @block = block
32
+ end
33
+
34
+ def call(env)
35
+ path, method = env['PATH_INFO'].to_s, env['REQUEST_METHOD']
36
+ if method == 'GET' && ((@path.is_a?(Regexp) && match = @path.match(path)) || @path == path)
37
+ response = ::Rack::Response.new
38
+ response.body = @block.call(env, response, match)
39
+ response.body ? response.finish : @app.call(env)
40
+ else
41
+ @app.call(env)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -6,14 +6,14 @@ module Rackables
6
6
  # if ENV['RACK_ENV'] == 'development'
7
7
  # use Rack::ShowExceptions
8
8
  # else
9
- # use Rackables::PublicExceptionPage
9
+ # use Rackables::HideExceptions
10
10
  # end
11
11
  #
12
12
  # The default HTML included here is a copy of the 500 page included with Rails
13
13
  # You can optionally specify your own file, ex:
14
14
  #
15
- # use Rackables::PublicExceptionPage, "public/500.html"
16
- class PublicExceptionPage
15
+ # use Rackables::HideExceptions, "public/500.html"
16
+ class HideExceptions
17
17
 
18
18
  def initialize(app, file_path = nil)
19
19
  @app = app
@@ -0,0 +1,28 @@
1
+ # This file not required by default -- to add this feature, you need to explicitly require, ex:
2
+ #
3
+ # require 'rackables/more/env_inquirer'
4
+ module Rack
5
+ # Adds pretty reader and query methods for ENV['RACK_ENV'] value.
6
+ # A copy of Rails' Rails.env for Rack apps.
7
+ #
8
+ # Examples:
9
+ #
10
+ # Rack.env # => "development"
11
+ # Rack.env.development? # => true
12
+ def self.env
13
+ @env ||= Utils::StringInquirer.new(ENV["RACK_ENV"] || "development")
14
+ end
15
+
16
+ module Utils
17
+ # TAKEN FROM ACTIVE SUPPORT
18
+ class StringInquirer < String
19
+ def method_missing(method_name, *arguments)
20
+ if method_name.to_s[-1,1] == "?"
21
+ self == method_name.to_s[0..-2]
22
+ else
23
+ super
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,4 +1,7 @@
1
1
  module Rackables
2
+ # Request paths with a trailing slash are 301 redirected to the version without, e.g.:
3
+ #
4
+ # GET /foo/ # => 301 redirects to /foo
2
5
  class TrailingSlashRedirect
3
6
  HAS_TRAILING_SLASH = %r{^/(.*)/$}
4
7
 
@@ -0,0 +1,26 @@
1
+ require 'test/unit'
2
+ require 'rackables'
3
+
4
+ class TestBranch < Test::Unit::TestCase
5
+
6
+ def test_calls_app_returned_from_block
7
+ app = Proc.new { [200, {}, ["Downstream app"]] }
8
+ blog = Proc.new { [200, {}, ["Blog"]] }
9
+ middleware = Rackables::Branch.new(app) do |env|
10
+ blog if env['PATH_INFO'] =~ /^\/blog/
11
+ end
12
+ status, headers, body = middleware.call('PATH_INFO' => '/blog')
13
+ assert_equal 'Blog', body[0]
14
+ end
15
+
16
+ def test_calls_app_passed_in_to_initialize_when_block_returns_false
17
+ app = Proc.new { [200, {}, ["Downstream app"]] }
18
+ blog = Proc.new { [200, {}, ["Blog"]] }
19
+ middleware = Rackables::Branch.new(app) do |env|
20
+ blog if env['PATH_INFO'] =~ /^\/blog/
21
+ end
22
+ status, headers, body = middleware.call('PATH_INFO' => '/foo')
23
+ assert_equal 'Downstream app', body[0]
24
+ end
25
+
26
+ end
@@ -3,21 +3,51 @@ require 'rackables'
3
3
 
4
4
  class TestCacheControl < Test::Unit::TestCase
5
5
 
6
- def test_sets_cache_control
6
+ def test_sets_cache_control_with_value_set_as_integer
7
7
  app = Proc.new { [200, {}, []]}
8
8
  middleware = Rackables::CacheControl.new(app, :public, :max_age => 5)
9
9
  status, headers, body = middleware.call({})
10
10
  assert_equal 'public, max-age=5', headers['Cache-Control']
11
11
  end
12
+
13
+ def test_sets_cache_control_with_value_set_as_string
14
+ app = Proc.new { [200, {}, []]}
15
+ middleware = Rackables::CacheControl.new(app, :public, :community => "UCI")
16
+ status, headers, body = middleware.call({})
17
+ assert_equal %(public, community="UCI"), headers['Cache-Control']
18
+ end
12
19
 
13
- def test_sets_cache_control_with_value_specified_as_proc
20
+ def test_sets_cache_control_with_value_specified_as_proc_which_is_called_at_runtime
14
21
  app = Proc.new { [200, {}, []]}
15
- middleware = Rackables::CacheControl.new(app, :public, :max_age => Proc.new {5})
22
+ value = 7 #compile-time value
23
+ middleware = Rackables::CacheControl.new(app, :public, :max_age => Proc.new {value})
24
+ value = 5 #runtime value
16
25
  status, headers, body = middleware.call({})
17
26
  assert_equal 'public, max-age=5', headers['Cache-Control']
18
27
  end
28
+
29
+ def test_sets_cache_control_with_multiple_values
30
+ app = Proc.new { [200, {}, []]}
31
+ middleware = Rackables::CacheControl.new(app, :private, :no_cache, :must_revalidate)
32
+ status, headers, body = middleware.call({})
33
+ assert_equal %(private, no-cache, must-revalidate), headers['Cache-Control']
34
+ end
35
+
36
+ def test_sets_true_value
37
+ app = Proc.new { [200, {}, []]}
38
+ middleware = Rackables::CacheControl.new(app, :private, :must_revalidate => true)
39
+ status, headers, body = middleware.call({})
40
+ assert_equal 'private, must-revalidate', headers['Cache-Control']
41
+ end
42
+
43
+ def test_does_not_set_false_value
44
+ app = Proc.new { [200, {}, []]}
45
+ middleware = Rackables::CacheControl.new(app, :private, :must_revalidate => false)
46
+ status, headers, body = middleware.call({})
47
+ assert_equal 'private', headers['Cache-Control']
48
+ end
19
49
 
20
- def test_respects_existing_cache_control_value
50
+ def test_respects_existing_cache_control_header
21
51
  app = Proc.new { [200, {'Cache-Control' => 'private'}, []]}
22
52
  middleware = Rackables::CacheControl.new(app, :public, :max_age => 5)
23
53
  status, headers, body = middleware.call({})
@@ -0,0 +1,80 @@
1
+ require 'test/unit'
2
+ require 'rackables'
3
+ require 'rubygems'
4
+ require 'rack'
5
+
6
+ class TestGet < Test::Unit::TestCase
7
+
8
+ def setup
9
+ @app = Proc.new { [200, {}, ["Downstream app"]] }
10
+ end
11
+
12
+ def test_calls_downstream_app_when_no_match
13
+ middleware = Rackables::Get.new(@app, '/foo') { 'bar' }
14
+ status, headers, body = middleware.call('PATH_INFO' => '/bar', 'REQUEST_METHOD' => 'GET')
15
+ assert_equal 200, status
16
+ assert_equal 'Downstream app', body[0]
17
+ end
18
+
19
+ def test_calls_downstream_app_when_path_matches_but_request_method_is_not_get
20
+ middleware = Rackables::Get.new(@app, '/foo') { 'bar' }
21
+ status, headers, body = middleware.call('PATH_INFO' => '/foo', 'REQUEST_METHOD' => 'POST')
22
+ assert_equal 200, status
23
+ assert_equal 'Downstream app', body[0]
24
+ end
25
+
26
+ def test_calls_downstream_app_when_path_matches_but_block_returns_nil
27
+ middleware = Rackables::Get.new(@app, '/foo') { }
28
+ status, headers, body = middleware.call('PATH_INFO' => '/foo', 'REQUEST_METHOD' => 'GET')
29
+ assert_equal 200, status
30
+ assert_equal 'Downstream app', body[0]
31
+ end
32
+
33
+ def test_get_returns_response_when_string_path_arg_matches_path_info
34
+ middleware = Rackables::Get.new(@app, '/foo') { 'bar' }
35
+ status, headers, body = middleware.call('PATH_INFO' => '/foo', 'REQUEST_METHOD' => 'GET')
36
+ assert_equal 200, status
37
+ assert_equal 'bar', body.body
38
+ end
39
+
40
+ def test_get_returns_response_when_string_path_arg_matches_regex
41
+ middleware = Rackables::Get.new(@app, /foo/) { 'bar' }
42
+ status, headers, body = middleware.call('PATH_INFO' => '/bar/foo', 'REQUEST_METHOD' => 'GET')
43
+ assert_equal 200, status
44
+ assert_equal 'bar', body.body
45
+ end
46
+
47
+ def test_block_yields_env
48
+ env = {'PATH_INFO' => '/foo', 'REQUEST_METHOD' => 'GET'}
49
+ middleware = Rackables::Get.new(@app, '/foo') do |e, resp|
50
+ assert_equal env, e
51
+ end
52
+ status, headers, body = middleware.call(env)
53
+ end
54
+
55
+ def test_block_yields_response_obj
56
+ env = {'PATH_INFO' => '/foo', 'REQUEST_METHOD' => 'GET'}
57
+ middleware = Rackables::Get.new(@app, '/foo') do |e, resp|
58
+ assert_instance_of ::Rack::Response, resp
59
+ end
60
+ status, headers, body = middleware.call(env)
61
+ end
62
+
63
+ def test_block_yields_match_data
64
+ env = {'PATH_INFO' => '/foobar', 'REQUEST_METHOD' => 'GET'}
65
+ middleware = Rackables::Get.new(@app, /foo(.+)/) do |e, resp, match|
66
+ assert_instance_of MatchData, match
67
+ assert_equal 'bar', match[1]
68
+ end
69
+ status, headers, body = middleware.call(env)
70
+ end
71
+
72
+ def test_response_honors_headers_set_in_block
73
+ middleware = Rackables::Get.new(@app, '/foo') {|e, r| r['X-Foo'] = 'bar'; 'baz' }
74
+ status, headers, body = middleware.call('PATH_INFO' => '/foo', 'REQUEST_METHOD' => 'GET')
75
+ assert_equal 200, status
76
+ assert_equal 'bar', headers['X-Foo']
77
+ assert_equal 'baz', body.body
78
+ end
79
+
80
+ end
@@ -1,11 +1,11 @@
1
1
  require 'test/unit'
2
2
  require 'rackables'
3
3
 
4
- class TestPublicExceptionPage < Test::Unit::TestCase
4
+ class TestHideExceptions < Test::Unit::TestCase
5
5
 
6
6
  def test_returns_downstream_app_response_when_no_exception_raised
7
7
  app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]}
8
- middleware = Rackables::PublicExceptionPage.new(app)
8
+ middleware = Rackables::HideExceptions.new(app)
9
9
  status, headers, body = middleware.call({})
10
10
  assert_equal 200, status
11
11
  assert_equal ['Downstream app'], body
@@ -13,7 +13,7 @@ class TestPublicExceptionPage < Test::Unit::TestCase
13
13
 
14
14
  def test_returns_500_with_default_exception_page_when_exception_raised_by_downstream_app
15
15
  app = Proc.new { raise }
16
- middleware = Rackables::PublicExceptionPage.new(app)
16
+ middleware = Rackables::HideExceptions.new(app)
17
17
  status, headers, body = middleware.call({})
18
18
  assert_equal 500, status
19
19
  assert_match /We\'re sorry, but something went wrong/, body[0]
@@ -21,7 +21,7 @@ class TestPublicExceptionPage < Test::Unit::TestCase
21
21
 
22
22
  def test_returns_500_with_custom_exception_page_when_file_path_specified
23
23
  app = Proc.new { raise }
24
- middleware = Rackables::PublicExceptionPage.new(app, 'test/fixtures/custom_500.html')
24
+ middleware = Rackables::HideExceptions.new(app, 'test/fixtures/custom_500.html')
25
25
  status, headers, body = middleware.call({})
26
26
  assert_equal 500, status
27
27
  assert_match /Custom 500 page/, body[0]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rackables
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geoff Buesing
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-12 00:00:00 -06:00
12
+ date: 2010-01-15 00:00:00 -06:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -20,20 +20,25 @@ executables: []
20
20
  extensions: []
21
21
 
22
22
  extra_rdoc_files:
23
- - README
23
+ - README.rdoc
24
24
  files:
25
- - README
25
+ - README.rdoc
26
26
  - Rakefile
27
27
  - VERSION
28
28
  - lib/rackables.rb
29
+ - lib/rackables/branch.rb
29
30
  - lib/rackables/cache_control.rb
30
31
  - lib/rackables/default_charset.rb
31
- - lib/rackables/public_exception_page.rb
32
+ - lib/rackables/get.rb
33
+ - lib/rackables/hide_exceptions.rb
34
+ - lib/rackables/more/env_inquirer.rb
32
35
  - lib/rackables/trailing_slash_redirect.rb
33
36
  - test/fixtures/custom_500.html
37
+ - test/test_branch.rb
34
38
  - test/test_cache_control.rb
35
39
  - test/test_default_charset.rb
36
- - test/test_public_exception_page.rb
40
+ - test/test_get.rb
41
+ - test/test_hide_exceptions.rb
37
42
  - test/test_trailing_slash_redirect.rb
38
43
  has_rdoc: true
39
44
  homepage: http://github.com/gbuesing/rackables
@@ -64,7 +69,9 @@ signing_key:
64
69
  specification_version: 3
65
70
  summary: Bundle of useful Rack middleware
66
71
  test_files:
72
+ - test/test_branch.rb
67
73
  - test/test_cache_control.rb
68
74
  - test/test_default_charset.rb
69
- - test/test_public_exception_page.rb
75
+ - test/test_get.rb
76
+ - test/test_hide_exceptions.rb
70
77
  - test/test_trailing_slash_redirect.rb
data/README DELETED
@@ -1,6 +0,0 @@
1
- Bundles the following Rack middlewares:
2
-
3
- Rackables::CacheControl
4
- Rackables::DefaultCharset
5
- Rackables::PublicExceptionPage
6
- Rackables::TrailingSlashRedirect