rack-contrib 0.9.0 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ require 'rack'
2
+
3
+ # Rack::RelativeRedirect is a simple middleware that converts relative paths in
4
+ # redirects in absolute urls, so they conform to RFC2616. It allows the user to
5
+ # specify the absolute path to use (with a sensible default), and handles
6
+ # relative paths (those that don't start with a slash) as well.
7
+ class Rack::RelativeRedirect
8
+ SCHEME_MAP = {'http'=>'80', 'https'=>'443'}
9
+ # The default proc used if a block is not provided to .new
10
+ # Just uses the url scheme of the request and the server name.
11
+ DEFAULT_ABSOLUTE_PROC = proc do |env, res|
12
+ port = env['SERVER_PORT']
13
+ scheme = env['rack.url_scheme']
14
+ "#{scheme}://#{env['SERVER_NAME']}#{":#{port}" unless SCHEME_MAP[scheme] == port}"
15
+ end
16
+
17
+ # Initialize a new RelativeRedirect object with the given arguments. Arguments:
18
+ # * app : The next middleware in the chain. This is always called.
19
+ # * &block : If provided, it is called with the environment and the response
20
+ # from the next middleware. It should return a string representing the scheme
21
+ # and server name (such as 'http://example.org').
22
+ def initialize(app, &block)
23
+ @app = app
24
+ @absolute_proc = block || DEFAULT_ABSOLUTE_PROC
25
+ end
26
+
27
+ # Call the next middleware with the environment. If the request was a
28
+ # redirect (response status 301, 302, or 303), and the location header does
29
+ # not start with an http or https url scheme, call the block provided by new
30
+ # and use that to make the Location header an absolute url. If the Location
31
+ # does not start with a slash, make location relative to the path requested.
32
+ def call(env)
33
+ res = @app.call(env)
34
+ if [301,302,303].include?(res[0]) and loc = res[1]['Location'] and !%r{\Ahttps?://}o.match(loc)
35
+ absolute = @absolute_proc.call(env, res)
36
+ res[1]['Location'] = if %r{\A/}.match(loc)
37
+ "#{absolute}#{loc}"
38
+ else
39
+ "#{absolute}#{File.dirname(Rack::Utils.unescape(env['PATH_INFO']))}/#{loc}"
40
+ end
41
+ end
42
+ res
43
+ end
44
+ end
@@ -0,0 +1,59 @@
1
+ require 'fileutils'
2
+ require 'rack'
3
+
4
+ # Rack::ResponseCache is a Rack middleware that caches responses for successful
5
+ # GET requests with no query string to disk or any ruby object that has an
6
+ # []= method (so it works with memcached). When caching to disk, it works similar to
7
+ # Rails' page caching, allowing you to cache dynamic pages to static files that can
8
+ # be served directly by a front end webserver.
9
+ class Rack::ResponseCache
10
+ # The default proc used if a block is not provided to .new
11
+ # It unescapes the PATH_INFO of the environment, and makes sure that it doesn't
12
+ # include '..'. If the Content-Type of the response is text/(html|css|xml),
13
+ # return a path with the appropriate extension (.html, .css, or .xml).
14
+ # If the path ends with a / and the Content-Type is text/html, change the basename
15
+ # of the path to index.html.
16
+ DEFAULT_PATH_PROC = proc do |env, res|
17
+ path = Rack::Utils.unescape(env['PATH_INFO'])
18
+ if !path.include?('..') and match = /text\/((?:x|ht)ml|css)/o.match(res[1]['Content-Type'])
19
+ type = match[1]
20
+ path = "#{path}.#{type}" unless /\.#{type}\z/.match(path)
21
+ path = File.join(File.dirname(path), 'index.html') if type == 'html' and File.basename(path) == '.html'
22
+ path
23
+ end
24
+ end
25
+
26
+ # Initialize a new ReponseCache object with the given arguments. Arguments:
27
+ # * app : The next middleware in the chain. This is always called.
28
+ # * cache : The place to cache responses. If a string is provided, a disk
29
+ # cache is used, and all cached files will use this directory as the root directory.
30
+ # If anything other than a string is provided, it should respond to []=, which will
31
+ # be called with a path string and a body value (the 3rd element of the response).
32
+ # * &block : If provided, it is called with the environment and the response from the next middleware.
33
+ # It should return nil or false if the path should not be cached, and should return
34
+ # the pathname to use as a string if the result should be cached.
35
+ # If not provided, the DEFAULT_PATH_PROC is used.
36
+ def initialize(app, cache, &block)
37
+ @app = app
38
+ @cache = cache
39
+ @path_proc = block || DEFAULT_PATH_PROC
40
+ end
41
+
42
+ # Call the next middleware with the environment. If the request was successful (response status 200),
43
+ # was a GET request, and had an empty query string, call the block set up in initialize to get the path.
44
+ # If the cache is a string, create any necessary middle directories, and cache the file in the appropriate
45
+ # subdirectory of cache. Otherwise, cache the body of the reponse as the value with the path as the key.
46
+ def call(env)
47
+ res = @app.call(env)
48
+ if env['REQUEST_METHOD'] == 'GET' and env['QUERY_STRING'] == '' and res[0] == 200 and path = @path_proc.call(env, res)
49
+ if @cache.is_a?(String)
50
+ path = File.join(@cache, path) if @cache
51
+ FileUtils.mkdir_p(File.dirname(path))
52
+ File.open(path, 'wb'){|f| res[2].each{|c| f.write(c)}}
53
+ else
54
+ @cache[path] = res[2]
55
+ end
56
+ end
57
+ res
58
+ end
59
+ end
@@ -4,45 +4,46 @@ module Rack
4
4
  [Exception, '/error/internal']
5
5
  ]
6
6
 
7
- ROUTE_EXCEPTIONS_PATH_INFO = 'rack.route_exceptions.path_info'.freeze
8
- ROUTE_EXCEPTIONS_EXCEPTION = 'rack.route_exceptions.exception'.freeze
9
- ROUTE_EXCEPTIONS_RESPONSE = 'rack.route_exceptions.response'.freeze
7
+ PATH_INFO = 'rack.route_exceptions.path_info'.freeze
8
+ EXCEPTION = 'rack.route_exceptions.exception'.freeze
9
+ RETURNED = 'rack.route_exceptions.returned'.freeze
10
+
11
+ class << self
12
+ def route(exception, to)
13
+ ROUTES.delete_if{|k,v| k == exception }
14
+ ROUTES << [exception, to]
15
+ end
16
+
17
+ alias []= route
18
+ end
10
19
 
11
20
  def initialize(app)
12
21
  @app = app
13
22
  end
14
23
 
15
24
  def call(env, try_again = true)
16
- status, header, body = response = @app.call(env)
17
-
18
- response
25
+ returned = @app.call(env)
19
26
  rescue Exception => exception
20
27
  raise(exception) unless try_again
21
28
 
22
29
  ROUTES.each do |klass, to|
23
30
  next unless klass === exception
24
- return route(to, env, response, exception)
31
+ return route(to, env, returned, exception)
25
32
  end
26
33
 
27
34
  raise(exception)
28
35
  end
29
36
 
30
- def route(to, env, response, exception)
31
- hash = {
32
- ROUTE_EXCEPTIONS_PATH_INFO => env['PATH_INFO'],
33
- ROUTE_EXCEPTIONS_EXCEPTION => exception,
34
- ROUTE_EXCEPTIONS_RESPONSE => response
35
- }
36
- env.merge!(hash)
37
+ def route(to, env, returned, exception)
38
+ env.merge!(
39
+ PATH_INFO => env['PATH_INFO'],
40
+ EXCEPTION => exception,
41
+ RETURNED => returned
42
+ )
37
43
 
38
44
  env['PATH_INFO'] = to
39
45
 
40
46
  call(env, try_again = false)
41
47
  end
42
-
43
- def self.route(exception, to)
44
- ROUTES.delete_if{|k,v| k == exception }
45
- ROUTES << [exception, to]
46
- end
47
48
  end
48
49
  end
@@ -0,0 +1,63 @@
1
+ module Rack
2
+ # Installs signal handlers that are safely processed after a request
3
+ #
4
+ # NOTE: This middleware should not be used in a threaded environment
5
+ #
6
+ # use Rack::Signals.new do
7
+ # trap 'INT', lambda {
8
+ # puts "Exiting now"
9
+ # exit
10
+ # }
11
+ #
12
+ # trap_when_ready 'USR1', lambda {
13
+ # puts "Exiting when ready"
14
+ # exit
15
+ # }
16
+ # end
17
+ class Signals
18
+ class BodyWithCallback
19
+ def initialize(body, callback)
20
+ @body, @callback = body, callback
21
+ end
22
+
23
+ def each(&block)
24
+ @body.each(&block)
25
+ @callback.call
26
+ end
27
+ end
28
+
29
+ def initialize(app, &block)
30
+ @app = app
31
+ @processing = false
32
+ @when_ready = nil
33
+ instance_eval(&block)
34
+ end
35
+
36
+ def call(env)
37
+ begin
38
+ @processing, @when_ready = true, nil
39
+ status, headers, body = @app.call(env)
40
+
41
+ if handler = @when_ready
42
+ body = BodyWithCallback.new(body, handler)
43
+ @when_ready = nil
44
+ end
45
+ ensure
46
+ @processing = false
47
+ end
48
+
49
+ [status, headers, body]
50
+ end
51
+
52
+ def trap_when_ready(signal, handler)
53
+ when_ready_handler = lambda { |signal|
54
+ if @processing
55
+ @when_ready = lambda { handler.call(signal) }
56
+ else
57
+ handler.call(signal)
58
+ end
59
+ }
60
+ trap(signal, when_ready_handler)
61
+ end
62
+ end
63
+ end
@@ -3,8 +3,8 @@ Gem::Specification.new do |s|
3
3
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
4
 
5
5
  s.name = 'rack-contrib'
6
- s.version = '0.9.0'
7
- s.date = '2009-01-23'
6
+ s.version = '0.9.2'
7
+ s.date = '2009-03-07'
8
8
 
9
9
  s.description = "Contributed Rack Middleware and Utilities"
10
10
  s.summary = "Contributed Rack Middleware and Utilities"
@@ -18,8 +18,13 @@ Gem::Specification.new do |s|
18
18
  README.rdoc
19
19
  Rakefile
20
20
  lib/rack/contrib.rb
21
+ lib/rack/contrib/accept_format.rb
22
+ lib/rack/contrib/backstage.rb
21
23
  lib/rack/contrib/bounce_favicon.rb
22
24
  lib/rack/contrib/callbacks.rb
25
+ lib/rack/contrib/config.rb
26
+ lib/rack/contrib/csshttprequest.rb
27
+ lib/rack/contrib/deflect.rb
23
28
  lib/rack/contrib/etag.rb
24
29
  lib/rack/contrib/evil.rb
25
30
  lib/rack/contrib/garbagecollector.rb
@@ -28,16 +33,27 @@ Gem::Specification.new do |s|
28
33
  lib/rack/contrib/locale.rb
29
34
  lib/rack/contrib/mailexceptions.rb
30
35
  lib/rack/contrib/nested_params.rb
36
+ lib/rack/contrib/not_found.rb
31
37
  lib/rack/contrib/post_body_content_type_parser.rb
32
38
  lib/rack/contrib/proctitle.rb
33
39
  lib/rack/contrib/profiler.rb
40
+ lib/rack/contrib/relative_redirect.rb
41
+ lib/rack/contrib/response_cache.rb
34
42
  lib/rack/contrib/route_exceptions.rb
35
43
  lib/rack/contrib/sendfile.rb
44
+ lib/rack/contrib/signals.rb
36
45
  lib/rack/contrib/time_zone.rb
37
46
  rack-contrib.gemspec
47
+ test/404.html
48
+ test/Maintenance.html
38
49
  test/mail_settings.rb
50
+ test/spec_rack_accept_format.rb
51
+ test/spec_rack_backstage.rb
39
52
  test/spec_rack_callbacks.rb
53
+ test/spec_rack_config.rb
40
54
  test/spec_rack_contrib.rb
55
+ test/spec_rack_csshttprequest.rb
56
+ test/spec_rack_deflect.rb
41
57
  test/spec_rack_etag.rb
42
58
  test/spec_rack_evil.rb
43
59
  test/spec_rack_garbagecollector.rb
@@ -45,9 +61,12 @@ Gem::Specification.new do |s|
45
61
  test/spec_rack_lighttpd_script_name_fix.rb
46
62
  test/spec_rack_mailexceptions.rb
47
63
  test/spec_rack_nested_params.rb
64
+ test/spec_rack_not_found.rb
48
65
  test/spec_rack_post_body_content_type_parser.rb
49
66
  test/spec_rack_proctitle.rb
50
67
  test/spec_rack_profiler.rb
68
+ test/spec_rack_relative_redirect.rb
69
+ test/spec_rack_response_cache.rb
51
70
  test/spec_rack_sendfile.rb
52
71
  ]
53
72
  # = MANIFEST =
@@ -55,7 +74,7 @@ Gem::Specification.new do |s|
55
74
  s.test_files = s.files.select {|path| path =~ /^test\/spec_.*\.rb/}
56
75
 
57
76
  s.extra_rdoc_files = %w[README.rdoc COPYING]
58
- s.add_dependency 'rack', '~> 0.9.1'
77
+ s.add_dependency 'rack', '>= 0.9.1'
59
78
  s.add_dependency 'test-spec', '~> 0.9.0'
60
79
  s.add_development_dependency 'tmail', '>= 1.2'
61
80
  s.add_development_dependency 'json', '>= 1.1'
@@ -0,0 +1 @@
1
+ Not Found
@@ -0,0 +1 @@
1
+ Under maintenance.
@@ -0,0 +1,72 @@
1
+ require 'test/spec'
2
+ require 'rack/mock'
3
+ require 'rack/contrib/accept_format'
4
+ require 'rack/mime'
5
+
6
+ context "Rack::AcceptFormat" do
7
+ app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, env['PATH_INFO']] }
8
+
9
+ specify "should do nothing when a format extension is already provided" do
10
+ request = Rack::MockRequest.env_for("/resource.json")
11
+ body = Rack::AcceptFormat.new(app).call(request).last
12
+ body.should == "/resource.json"
13
+ end
14
+
15
+ context "default extention" do
16
+ specify "should allow custom default" do
17
+ request = Rack::MockRequest.env_for("/resource")
18
+ body = Rack::AcceptFormat.new(app, '.xml').call(request).last
19
+ body.should == "/resource.xml"
20
+ end
21
+
22
+ specify "should default to html" do
23
+ request = Rack::MockRequest.env_for("/resource")
24
+ body = Rack::AcceptFormat.new(app).call(request).last
25
+ body.should == "/resource.html"
26
+ end
27
+
28
+ specify "should notmalize custom extention" do
29
+ request = Rack::MockRequest.env_for("/resource")
30
+
31
+ body = Rack::AcceptFormat.new(app,'xml').call(request).last #no dot prefix
32
+ body.should == "/resource.xml"
33
+
34
+ body = Rack::AcceptFormat.new(app, :xml).call(request).last
35
+ body.should == "/resource.xml"
36
+ end
37
+ end
38
+
39
+ context "there is no format extension" do
40
+ Rack::Mime::MIME_TYPES.clear
41
+
42
+ def mime(ext, type)
43
+ ext = ".#{ext}" unless ext.to_s[0] == ?.
44
+ Rack::Mime::MIME_TYPES[ext.to_s] = type
45
+ end
46
+
47
+ specify "should add the default extension if no Accept header" do
48
+ request = Rack::MockRequest.env_for("/resource")
49
+ body = Rack::AcceptFormat.new(app).call(request).last
50
+ body.should == "/resource.html"
51
+ end
52
+
53
+ specify "should add the default extension if the Accept header is not registered in the Mime::Types" do
54
+ request = Rack::MockRequest.env_for("/resource", 'HTTP_ACCEPT' => 'application/json;q=1.0, text/html;q=0.8, */*;q=0.1')
55
+ body = Rack::AcceptFormat.new(app).call(request).last
56
+ body.should == "/resource.html"
57
+ end
58
+
59
+ specify "should add the correct extension if the Accept header is registered in the Mime::Types" do
60
+ mime :json, 'application/json'
61
+ request = Rack::MockRequest.env_for("/resource", 'HTTP_ACCEPT' => 'application/json;q=1.0, text/html;q=0.8, */*;q=0.1')
62
+ body = Rack::AcceptFormat.new(app).call(request).last
63
+ body.should == "/resource.json"
64
+ end
65
+ end
66
+
67
+ specify "shouldn't confuse extention when there are dots in path" do
68
+ request = Rack::MockRequest.env_for("/parent.resource/resource")
69
+ body = Rack::AcceptFormat.new(app, '.html').call(request).last
70
+ body.should == "/parent.resource/resource.html"
71
+ end
72
+ end
@@ -0,0 +1,26 @@
1
+ require 'test/spec'
2
+ require 'rack/builder'
3
+ require 'rack/mock'
4
+ require 'rack/contrib/backstage'
5
+
6
+ context "Rack::Backstage" do
7
+ specify "shows maintenances page if present" do
8
+ app = Rack::Builder.new do
9
+ use Rack::Backstage, 'test/Maintenance.html'
10
+ run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
11
+ end
12
+ response = Rack::MockRequest.new(app).get('/')
13
+ response.body.should.equal('Under maintenance.')
14
+ response.status.should.equal(503)
15
+ end
16
+
17
+ specify "passes on request if page is not present" do
18
+ app = Rack::Builder.new do
19
+ use Rack::Backstage, 'test/Nonsense.html'
20
+ run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
21
+ end
22
+ response = Rack::MockRequest.new(app).get('/')
23
+ response.body.should.equal('Hello, World!')
24
+ response.status.should.equal(200)
25
+ end
26
+ end
@@ -43,7 +43,7 @@ context "Rack::Callbacks" do
43
43
  before Flame
44
44
  before Pacify, "with love"
45
45
 
46
- run lambda {|env| [200, {}, env['flame'] + env['peace']] }
46
+ run lambda {|env| [200, {}, [env['flame'], env['peace']]] }
47
47
 
48
48
  after Finale
49
49
  after TheEnd
@@ -62,4 +62,4 @@ context "Rack::Callbacks" do
62
62
 
63
63
  response.headers['last'].should.equal 'TheEnd'
64
64
  end
65
- end
65
+ end
@@ -0,0 +1,22 @@
1
+ require 'test/spec'
2
+ require 'rack/mock'
3
+ require 'rack/contrib/config'
4
+
5
+ context "Rack::Config" do
6
+
7
+ specify "should accept a block that modifies the environment" do
8
+ app = Rack::Builder.new do
9
+ use Rack::Lint
10
+ use Rack::ContentLength
11
+ use Rack::Config do |env|
12
+ env['greeting'] = 'hello'
13
+ end
14
+ run lambda { |env|
15
+ [200, {'Content-Type' => 'text/plain'}, [env['greeting'] || '']]
16
+ }
17
+ end
18
+ response = Rack::MockRequest.new(app).get('/')
19
+ response.body.should.equal('hello')
20
+ end
21
+
22
+ end