rackables 0.1.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.
- data/README +6 -0
- data/Rakefile +22 -0
- data/VERSION +1 -0
- data/lib/rackables.rb +6 -0
- data/lib/rackables/cache_control.rb +21 -0
- data/lib/rackables/default_charset.rb +19 -0
- data/lib/rackables/public_exception_page.rb +72 -0
- data/lib/rackables/trailing_slash_redirect.rb +20 -0
- data/test/fixtures/custom_500.html +1 -0
- data/test/test_cache_control.rb +27 -0
- data/test/test_default_charset.rb +27 -0
- data/test/test_public_exception_page.rb +30 -0
- data/test/test_trailing_slash_redirect.rb +39 -0
- metadata +70 -0
data/README
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
Rake::TestTask.new do |t|
|
4
|
+
t.libs << "test"
|
5
|
+
t.test_files = FileList['test/test*.rb']
|
6
|
+
t.verbose = true
|
7
|
+
end
|
8
|
+
task :default => :test
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'jeweler'
|
12
|
+
Jeweler::Tasks.new do |gemspec|
|
13
|
+
gemspec.name = "rackables"
|
14
|
+
gemspec.summary = "Bundle of useful Rack middleware"
|
15
|
+
gemspec.description = "Bundles Rack middleware: CacheControl, DefaultCharset, PublicExceptionPage, TrailingSlashRedirect"
|
16
|
+
gemspec.email = "gbuesing@gmail.com"
|
17
|
+
gemspec.homepage = "http://github.com/gbuesing/rackables"
|
18
|
+
gemspec.authors = ["Geoff Buesing"]
|
19
|
+
end
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
22
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/rackables.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Rackables
|
2
|
+
class CacheControl
|
3
|
+
# Works well with Varnish
|
4
|
+
# TODO: accomodate more options than just public, max-age=
|
5
|
+
def initialize(app, value, opts)
|
6
|
+
@app = app
|
7
|
+
@value = value
|
8
|
+
@opts = opts
|
9
|
+
end
|
10
|
+
|
11
|
+
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}"
|
17
|
+
end
|
18
|
+
[status, headers, body]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rackables
|
2
|
+
class DefaultCharset
|
3
|
+
HAS_CHARSET = /charset=/
|
4
|
+
|
5
|
+
def initialize(app, value = 'utf-8')
|
6
|
+
@app = app
|
7
|
+
@value = value
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
status, headers, body = @app.call(env)
|
12
|
+
content_type = headers['Content-Type']
|
13
|
+
if content_type && content_type !~ HAS_CHARSET
|
14
|
+
headers['Content-Type'] = "#{content_type}; charset=#{@value}"
|
15
|
+
end
|
16
|
+
[status, headers, body]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Rackables
|
2
|
+
# Returns a user-friendly exception page
|
3
|
+
# Should be included at the very top of the middleware pipeline so that unhandled exceptions from anywhere down the pipeline are rescued
|
4
|
+
# In development, you'd want to use Rack::ShowExceptions instead of this (config.ru example):
|
5
|
+
#
|
6
|
+
# if ENV['RACK_ENV'] == 'development'
|
7
|
+
# use Rack::ShowExceptions
|
8
|
+
# else
|
9
|
+
# use Rackables::PublicExceptionPage
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# The default HTML included here is a copy of the 500 page included with Rails
|
13
|
+
# You can optionally specify your own file, ex:
|
14
|
+
#
|
15
|
+
# use Rackables::PublicExceptionPage, "public/500.html"
|
16
|
+
class PublicExceptionPage
|
17
|
+
|
18
|
+
def initialize(app, file_path = nil)
|
19
|
+
@app = app
|
20
|
+
@file_path = file_path
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(env)
|
24
|
+
@app.call(env)
|
25
|
+
rescue ::Exception
|
26
|
+
[500, {'Content-Type' => 'text/html', 'Content-Length' => html.length.to_s}, [html]]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def html
|
32
|
+
@html ||= @file_path ? ::File.read(@file_path) : default_html
|
33
|
+
end
|
34
|
+
|
35
|
+
# Exception page from Rails (500.html)
|
36
|
+
def default_html
|
37
|
+
<<-EOV
|
38
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
39
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
40
|
+
|
41
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
42
|
+
|
43
|
+
<head>
|
44
|
+
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
45
|
+
<title>We're sorry, but something went wrong (500)</title>
|
46
|
+
<style type="text/css">
|
47
|
+
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
|
48
|
+
div.dialog {
|
49
|
+
width: 25em;
|
50
|
+
padding: 0 4em;
|
51
|
+
margin: 4em auto 0 auto;
|
52
|
+
border: 1px solid #ccc;
|
53
|
+
border-right-color: #999;
|
54
|
+
border-bottom-color: #999;
|
55
|
+
}
|
56
|
+
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
|
57
|
+
</style>
|
58
|
+
</head>
|
59
|
+
|
60
|
+
<body>
|
61
|
+
<!-- This file lives in public/500.html -->
|
62
|
+
<div class="dialog">
|
63
|
+
<h1>We're sorry, but something went wrong.</h1>
|
64
|
+
<p>We've been notified about this issue and we'll take a look at it shortly.</p>
|
65
|
+
</div>
|
66
|
+
</body>
|
67
|
+
</html>
|
68
|
+
EOV
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rackables
|
2
|
+
class TrailingSlashRedirect
|
3
|
+
HAS_TRAILING_SLASH = %r{^/(.*)/$}
|
4
|
+
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
if env['PATH_INFO'] =~ HAS_TRAILING_SLASH
|
11
|
+
location = "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}/#{$1}"
|
12
|
+
location = "#{location}?#{env['QUERY_STRING']}" if env['QUERY_STRING'].to_s =~ /\S/
|
13
|
+
[301, {"Location" => location}, []]
|
14
|
+
else
|
15
|
+
@app.call(env)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<h1>Custom 500 page</h1>
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rackables'
|
3
|
+
|
4
|
+
class TestCacheControl < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_sets_cache_control
|
7
|
+
app = Proc.new { [200, {}, []]}
|
8
|
+
middleware = Rackables::CacheControl.new(app, :public, :max_age => 5)
|
9
|
+
status, headers, body = middleware.call({})
|
10
|
+
assert_equal 'public, max-age=5', headers['Cache-Control']
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_sets_cache_control_with_value_specified_as_proc
|
14
|
+
app = Proc.new { [200, {}, []]}
|
15
|
+
middleware = Rackables::CacheControl.new(app, :public, :max_age => Proc.new {5})
|
16
|
+
status, headers, body = middleware.call({})
|
17
|
+
assert_equal 'public, max-age=5', headers['Cache-Control']
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_respects_existing_cache_control_value
|
21
|
+
app = Proc.new { [200, {'Cache-Control' => 'private'}, []]}
|
22
|
+
middleware = Rackables::CacheControl.new(app, :public, :max_age => 5)
|
23
|
+
status, headers, body = middleware.call({})
|
24
|
+
assert_equal 'private', headers['Cache-Control']
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rackables'
|
3
|
+
|
4
|
+
class TestDefaultCharset < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_adds_utf8_charset_by_default
|
7
|
+
app = Proc.new { [200, {'Content-Type' => 'text/html'}, []]}
|
8
|
+
middleware = Rackables::DefaultCharset.new(app)
|
9
|
+
status, headers, body = middleware.call({})
|
10
|
+
assert_equal 'text/html; charset=utf-8', headers['Content-Type']
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_adds_specified_charset
|
14
|
+
app = Proc.new { [200, {'Content-Type' => 'text/html'}, []]}
|
15
|
+
middleware = Rackables::DefaultCharset.new(app, 'us-ascii')
|
16
|
+
status, headers, body = middleware.call({})
|
17
|
+
assert_equal 'text/html; charset=us-ascii', headers['Content-Type']
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_does_not_overwrite_existing_charset_value
|
21
|
+
app = Proc.new { [200, {'Content-Type' => 'text/html; charset=us-ascii'}, []]}
|
22
|
+
middleware = Rackables::DefaultCharset.new(app, 'iso-8859-2')
|
23
|
+
status, headers, body = middleware.call({})
|
24
|
+
assert_equal 'text/html; charset=us-ascii', headers['Content-Type']
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rackables'
|
3
|
+
|
4
|
+
class TestPublicExceptionPage < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_returns_downstream_app_response_when_no_exception_raised
|
7
|
+
app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]}
|
8
|
+
middleware = Rackables::PublicExceptionPage.new(app)
|
9
|
+
status, headers, body = middleware.call({})
|
10
|
+
assert_equal 200, status
|
11
|
+
assert_equal ['Downstream app'], body
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_returns_500_with_default_exception_page_when_exception_raised_by_downstream_app
|
15
|
+
app = Proc.new { raise }
|
16
|
+
middleware = Rackables::PublicExceptionPage.new(app)
|
17
|
+
status, headers, body = middleware.call({})
|
18
|
+
assert_equal 500, status
|
19
|
+
assert_match /We\'re sorry, but something went wrong/, body[0]
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_returns_500_with_custom_exception_page_when_file_path_specified
|
23
|
+
app = Proc.new { raise }
|
24
|
+
middleware = Rackables::PublicExceptionPage.new(app, 'test/fixtures/custom_500.html')
|
25
|
+
status, headers, body = middleware.call({})
|
26
|
+
assert_equal 500, status
|
27
|
+
assert_match /Custom 500 page/, body[0]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rackables'
|
3
|
+
|
4
|
+
class TestTrailingSlashRedirect < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_passes_to_downstream_app_when_no_trailing_slash_on_path_info
|
7
|
+
app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]}
|
8
|
+
middleware = Rackables::TrailingSlashRedirect.new(app)
|
9
|
+
status, headers, body = middleware.call({'PATH_INFO' => '/foo'})
|
10
|
+
assert_equal 200, status
|
11
|
+
assert_equal ['Downstream app'], body
|
12
|
+
assert_nil headers['Location']
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_returns_301_when_trailing_slash_on_path_info
|
16
|
+
app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]}
|
17
|
+
middleware = Rackables::TrailingSlashRedirect.new(app)
|
18
|
+
status, headers, body = middleware.call({'rack.url_scheme' => 'http', 'HTTP_HOST' => 'bar.com', 'PATH_INFO' => '/foo/'})
|
19
|
+
assert_equal 301, status
|
20
|
+
assert_equal 'http://bar.com/foo', headers['Location']
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_301_respects_query_string
|
24
|
+
app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]}
|
25
|
+
middleware = Rackables::TrailingSlashRedirect.new(app)
|
26
|
+
status, headers, body = middleware.call({'rack.url_scheme' => 'http', 'HTTP_HOST' => 'bar.com', 'PATH_INFO' => '/foo/', 'QUERY_STRING' => 'baz=hi'})
|
27
|
+
assert_equal 301, status
|
28
|
+
assert_equal 'http://bar.com/foo?baz=hi', headers['Location']
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_301_respects_https
|
32
|
+
app = Proc.new { [200, {'Content-Type' => 'text/html'}, ['Downstream app']]}
|
33
|
+
middleware = Rackables::TrailingSlashRedirect.new(app)
|
34
|
+
status, headers, body = middleware.call({'rack.url_scheme' => 'https', 'HTTP_HOST' => 'bar.com', 'PATH_INFO' => '/foo/', 'QUERY_STRING' => 'baz=hi'})
|
35
|
+
assert_equal 301, status
|
36
|
+
assert_equal 'https://bar.com/foo?baz=hi', headers['Location']
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rackables
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Geoff Buesing
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-12 00:00:00 -06:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: "Bundles Rack middleware: CacheControl, DefaultCharset, PublicExceptionPage, TrailingSlashRedirect"
|
17
|
+
email: gbuesing@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
files:
|
25
|
+
- README
|
26
|
+
- Rakefile
|
27
|
+
- VERSION
|
28
|
+
- lib/rackables.rb
|
29
|
+
- lib/rackables/cache_control.rb
|
30
|
+
- lib/rackables/default_charset.rb
|
31
|
+
- lib/rackables/public_exception_page.rb
|
32
|
+
- lib/rackables/trailing_slash_redirect.rb
|
33
|
+
- test/fixtures/custom_500.html
|
34
|
+
- test/test_cache_control.rb
|
35
|
+
- test/test_default_charset.rb
|
36
|
+
- test/test_public_exception_page.rb
|
37
|
+
- test/test_trailing_slash_redirect.rb
|
38
|
+
has_rdoc: true
|
39
|
+
homepage: http://github.com/gbuesing/rackables
|
40
|
+
licenses: []
|
41
|
+
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options:
|
44
|
+
- --charset=UTF-8
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
requirements: []
|
60
|
+
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 1.3.5
|
63
|
+
signing_key:
|
64
|
+
specification_version: 3
|
65
|
+
summary: Bundle of useful Rack middleware
|
66
|
+
test_files:
|
67
|
+
- test/test_cache_control.rb
|
68
|
+
- test/test_default_charset.rb
|
69
|
+
- test/test_public_exception_page.rb
|
70
|
+
- test/test_trailing_slash_redirect.rb
|