rackables 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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