corntrace-rack-contrib 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/AUTHORS +26 -0
  2. data/COPYING +18 -0
  3. data/README.rdoc +87 -0
  4. data/Rakefile +89 -0
  5. data/lib/rack/contrib/accept_format.rb +46 -0
  6. data/lib/rack/contrib/access.rb +85 -0
  7. data/lib/rack/contrib/backstage.rb +20 -0
  8. data/lib/rack/contrib/bounce_favicon.rb +16 -0
  9. data/lib/rack/contrib/callbacks.rb +37 -0
  10. data/lib/rack/contrib/config.rb +16 -0
  11. data/lib/rack/contrib/cookies.rb +50 -0
  12. data/lib/rack/contrib/csshttprequest.rb +39 -0
  13. data/lib/rack/contrib/deflect.rb +137 -0
  14. data/lib/rack/contrib/evil.rb +12 -0
  15. data/lib/rack/contrib/expectation_cascade.rb +32 -0
  16. data/lib/rack/contrib/garbagecollector.rb +14 -0
  17. data/lib/rack/contrib/host_meta.rb +47 -0
  18. data/lib/rack/contrib/jsonp.rb +78 -0
  19. data/lib/rack/contrib/lighttpd_script_name_fix.rb +16 -0
  20. data/lib/rack/contrib/locale.rb +31 -0
  21. data/lib/rack/contrib/mailexceptions.rb +120 -0
  22. data/lib/rack/contrib/nested_params.rb +143 -0
  23. data/lib/rack/contrib/not_found.rb +18 -0
  24. data/lib/rack/contrib/post_body_content_type_parser.rb +40 -0
  25. data/lib/rack/contrib/proctitle.rb +30 -0
  26. data/lib/rack/contrib/profiler.rb +108 -0
  27. data/lib/rack/contrib/relative_redirect.rb +44 -0
  28. data/lib/rack/contrib/response_cache.rb +59 -0
  29. data/lib/rack/contrib/response_headers.rb +24 -0
  30. data/lib/rack/contrib/route_exceptions.rb +49 -0
  31. data/lib/rack/contrib/runtime.rb +31 -0
  32. data/lib/rack/contrib/sendfile.rb +142 -0
  33. data/lib/rack/contrib/signals.rb +63 -0
  34. data/lib/rack/contrib/simple_endpoint.rb +81 -0
  35. data/lib/rack/contrib/static_cache.rb +93 -0
  36. data/lib/rack/contrib/time_zone.rb +25 -0
  37. data/lib/rack/contrib.rb +39 -0
  38. data/rack-contrib.gemspec +104 -0
  39. data/test/404.html +1 -0
  40. data/test/Maintenance.html +1 -0
  41. data/test/documents/test +1 -0
  42. data/test/mail_settings.rb +12 -0
  43. data/test/spec_rack_accept_format.rb +72 -0
  44. data/test/spec_rack_access.rb +154 -0
  45. data/test/spec_rack_backstage.rb +26 -0
  46. data/test/spec_rack_callbacks.rb +65 -0
  47. data/test/spec_rack_config.rb +22 -0
  48. data/test/spec_rack_contrib.rb +8 -0
  49. data/test/spec_rack_cookies.rb +56 -0
  50. data/test/spec_rack_csshttprequest.rb +66 -0
  51. data/test/spec_rack_deflect.rb +107 -0
  52. data/test/spec_rack_evil.rb +19 -0
  53. data/test/spec_rack_expectation_cascade.rb +72 -0
  54. data/test/spec_rack_garbagecollector.rb +13 -0
  55. data/test/spec_rack_host_meta.rb +50 -0
  56. data/test/spec_rack_jsonp.rb +83 -0
  57. data/test/spec_rack_lighttpd_script_name_fix.rb +16 -0
  58. data/test/spec_rack_mailexceptions.rb +97 -0
  59. data/test/spec_rack_nested_params.rb +46 -0
  60. data/test/spec_rack_not_found.rb +17 -0
  61. data/test/spec_rack_post_body_content_type_parser.rb +32 -0
  62. data/test/spec_rack_proctitle.rb +26 -0
  63. data/test/spec_rack_profiler.rb +37 -0
  64. data/test/spec_rack_relative_redirect.rb +78 -0
  65. data/test/spec_rack_response_cache.rb +137 -0
  66. data/test/spec_rack_response_headers.rb +35 -0
  67. data/test/spec_rack_runtime.rb +35 -0
  68. data/test/spec_rack_sendfile.rb +86 -0
  69. data/test/spec_rack_simple_endpoint.rb +95 -0
  70. data/test/spec_rack_static_cache.rb +91 -0
  71. data/test/statics/test +1 -0
  72. metadata +234 -0
@@ -0,0 +1,32 @@
1
+ module Rack
2
+ class ExpectationCascade
3
+ Expect = "Expect".freeze
4
+ ContinueExpectation = "100-continue".freeze
5
+
6
+ ExpectationFailed = [417, {"Content-Type" => "text/html"}, []].freeze
7
+ NotFound = [404, {"Content-Type" => "text/html"}, []].freeze
8
+
9
+ attr_reader :apps
10
+
11
+ def initialize
12
+ @apps = []
13
+ yield self if block_given?
14
+ end
15
+
16
+ def call(env)
17
+ set_expectation = env[Expect] != ContinueExpectation
18
+ env[Expect] = ContinueExpectation if set_expectation
19
+ @apps.each do |app|
20
+ result = app.call(env)
21
+ return result unless result[0].to_i == 417
22
+ end
23
+ set_expectation ? NotFound : ExpectationFailed
24
+ ensure
25
+ env.delete(Expect) if set_expectation
26
+ end
27
+
28
+ def <<(app)
29
+ @apps << app
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,14 @@
1
+ module Rack
2
+ # Forces garbage collection after each request.
3
+ class GarbageCollector
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ @app.call(env)
10
+ ensure
11
+ GC.start
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,47 @@
1
+ module Rack
2
+
3
+ # Rack middleware implementing the IETF draft: "Host Metadata for the Web"
4
+ # including support for Link-Pattern elements as described in the IETF draft:
5
+ # "Link-based Resource Descriptor Discovery."
6
+ #
7
+ # Usage:
8
+ # use Rack::HostMeta do
9
+ # link :uri => '/robots.txt', :rel => 'robots'
10
+ # link :uri => '/w3c/p3p.xml', :rel => 'privacy', :type => 'application/p3p.xml'
11
+ # link :pattern => '{uri};json_schema', :rel => 'describedby', :type => 'application/x-schema+json'
12
+ # end
13
+ #
14
+ # See also:
15
+ # http://tools.ietf.org/html/draft-nottingham-site-meta
16
+ # http://tools.ietf.org/html/draft-hammer-discovery
17
+ #
18
+ # TODO:
19
+ # Accept POST operations allowing downstream services to register themselves
20
+ #
21
+ class HostMeta
22
+ def initialize(app, &block)
23
+ @app = app
24
+ @lines = []
25
+ instance_eval(&block)
26
+ @response = @lines.join("\n")
27
+ end
28
+
29
+ def call(env)
30
+ if env['PATH_INFO'] == '/host-meta'
31
+ [200, {'Content-Type' => 'application/host-meta'}, [@response]]
32
+ else
33
+ @app.call(env)
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def link(config)
40
+ line = config[:uri] ? "Link: <#{config[:uri]}>;" : "Link-Pattern: <#{config[:pattern]}>;"
41
+ fragments = []
42
+ fragments << "rel=\"#{config[:rel]}\"" if config[:rel]
43
+ fragments << "type=\"#{config[:type]}\"" if config[:type]
44
+ @lines << "#{line} #{fragments.join("; ")}"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,78 @@
1
+ module Rack
2
+
3
+ # A Rack middleware for providing JSON-P support.
4
+ #
5
+ # Full credit to Flinn Mueller (http://actsasflinn.com/) for this contribution.
6
+ #
7
+ class JSONP
8
+ include Rack::Utils
9
+
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ # Proxies the request to the application, stripping out the JSON-P callback
15
+ # method and padding the response with the appropriate callback format if
16
+ # the returned body is application/json
17
+ #
18
+ # Changes nothing if no <tt>callback</tt> param is specified.
19
+ #
20
+ def call(env)
21
+ status, headers, response = @app.call(env)
22
+
23
+ headers = HeaderHash.new(headers)
24
+ request = Rack::Request.new(env)
25
+
26
+ if is_json?(headers) && has_callback?(request)
27
+ status = translate_error_code!(status, response)
28
+ response = pad(request.params.delete('callback'), response)
29
+
30
+ # No longer json, its javascript!
31
+ headers['Content-Type'].gsub!('json', 'javascript')
32
+
33
+ # Set new Content-Length, if it was set before we mutated the response body
34
+ if headers['Content-Length']
35
+ length = response.to_ary.inject(0) { |len, part| len + bytesize(part) }
36
+ headers['Content-Length'] = length.to_s
37
+ end
38
+ end
39
+ [status, headers, response]
40
+ end
41
+
42
+ private
43
+
44
+ def is_json?(headers)
45
+ headers['Content-Type'].include?('application/json')
46
+ end
47
+
48
+ def has_callback?(request)
49
+ request.params.include?('callback')
50
+ end
51
+
52
+ # Pads the response with the appropriate callback format according to the
53
+ # JSON-P spec/requirements.
54
+ #
55
+ # The Rack response spec indicates that it should be enumerable. The
56
+ # method of combining all of the data into a single string makes sense
57
+ # since JSON is returned as a full string.
58
+ #
59
+ def pad(callback, response, body = "")
60
+ response.each{ |s| body << s.to_s }
61
+ ["#{callback}(#{body})"]
62
+ end
63
+
64
+ # Translate status code 40x and 50x to a json string in response, and change
65
+ # status code to 200. That because the jsonp's implemetation could not handler
66
+ # http status code when error occured, which could be meaningful in webservice.
67
+ def translate_error_code!(status, response)
68
+ if status % 400 == 4 || status % 500 == 5
69
+ response.slice(1..-1) # keep the first element, delete the others
70
+ response[0] = %Q|{"errorCode":#{status}}|
71
+ status = 200
72
+ else
73
+ status
74
+ end
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,16 @@
1
+ module Rack
2
+ # Lighttpd sets the wrong SCRIPT_NAME and PATH_INFO if you mount your
3
+ # FastCGI app at "/". This middleware fixes this issue.
4
+
5
+ class LighttpdScriptNameFix
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ env["PATH_INFO"] = env["SCRIPT_NAME"].to_s + env["PATH_INFO"].to_s
12
+ env["SCRIPT_NAME"] = ""
13
+ @app.call(env)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ require 'i18n'
2
+
3
+ module Rack
4
+ class Locale
5
+ def initialize(app)
6
+ @app = app
7
+ end
8
+
9
+ def call(env)
10
+ old_locale = I18n.locale
11
+ locale = nil
12
+
13
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
14
+ if lang = env["HTTP_ACCEPT_LANGUAGE"]
15
+ lang = lang.split(",").map { |l|
16
+ l += ';q=1.0' unless l =~ /;q=\d+\.\d+$/
17
+ l.split(';q=')
18
+ }.first
19
+ locale = lang.first.split("-").first
20
+ else
21
+ locale = I18n.default_locale
22
+ end
23
+
24
+ locale = env['rack.locale'] = I18n.locale = locale.to_s
25
+ status, headers, body = @app.call(env)
26
+ headers['Content-Language'] = locale
27
+ I18n.locale = old_locale
28
+ [status, headers, body]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,120 @@
1
+ require 'net/smtp'
2
+ require 'tmail'
3
+ require 'erb'
4
+
5
+ module Rack
6
+ # Catches all exceptions raised from the app it wraps and
7
+ # sends a useful email with the exception, stacktrace, and
8
+ # contents of the environment.
9
+
10
+ class MailExceptions
11
+ attr_reader :config
12
+
13
+ def initialize(app)
14
+ @app = app
15
+ @config = {
16
+ :to => nil,
17
+ :from => ENV['USER'] || 'rack',
18
+ :subject => '[exception] %s',
19
+ :smtp => {
20
+ :server => 'localhost',
21
+ :domain => 'localhost',
22
+ :port => 25,
23
+ :authentication => :login,
24
+ :user_name => nil,
25
+ :password => nil
26
+ }
27
+ }
28
+ @template = ERB.new(TEMPLATE)
29
+ yield self if block_given?
30
+ end
31
+
32
+ def call(env)
33
+ status, headers, body =
34
+ begin
35
+ @app.call(env)
36
+ rescue => boom
37
+ # TODO don't allow exceptions from send_notification to
38
+ # propogate
39
+ send_notification boom, env
40
+ raise
41
+ end
42
+ send_notification env['mail.exception'], env if env['mail.exception']
43
+ [status, headers, body]
44
+ end
45
+
46
+ %w[to from subject].each do |meth|
47
+ define_method(meth) { |value| @config[meth.to_sym] = value }
48
+ end
49
+
50
+ def smtp(settings={})
51
+ @config[:smtp].merge! settings
52
+ end
53
+
54
+ private
55
+ def generate_mail(exception, env)
56
+ mail = TMail::Mail.new
57
+ mail.to = Array(config[:to])
58
+ mail.from = config[:from]
59
+ mail.subject = config[:subject] % [exception.to_s]
60
+ mail.date = Time.now
61
+ mail.set_content_type 'text/plain'
62
+ mail.charset = 'UTF-8'
63
+ mail.body = @template.result(binding)
64
+ mail
65
+ end
66
+
67
+ def send_notification(exception, env)
68
+ mail = generate_mail(exception, env)
69
+ smtp = config[:smtp]
70
+ env['mail.sent'] = true
71
+ return if smtp[:server] == 'example.com'
72
+
73
+ Net::SMTP.start smtp[:server], smtp[:port], smtp[:domain], smtp[:user_name], smtp[:password], smtp[:authentication] do |server|
74
+ mail.to.each do |recipient|
75
+ server.send_message mail.to_s, mail.from, recipient
76
+ end
77
+ end
78
+ end
79
+
80
+ def extract_body(env)
81
+ if io = env['rack.input']
82
+ io.rewind if io.respond_to?(:rewind)
83
+ io.read
84
+ end
85
+ end
86
+
87
+ TEMPLATE = (<<-'EMAIL').gsub(/^ {4}/, '')
88
+ A <%= exception.class.to_s %> occured: <%= exception.to_s %>
89
+ <% if body = extract_body(env) %>
90
+
91
+ ===================================================================
92
+ Request Body:
93
+ ===================================================================
94
+
95
+ <%= body.gsub(/^/, ' ') %>
96
+ <% end %>
97
+
98
+ ===================================================================
99
+ Rack Environment:
100
+ ===================================================================
101
+
102
+ PID: <%= $$ %>
103
+ PWD: <%= Dir.getwd %>
104
+
105
+ <%= env.to_a.
106
+ sort{|a,b| a.first <=> b.first}.
107
+ map{ |k,v| "%-25s%p" % [k+':', v] }.
108
+ join("\n ") %>
109
+
110
+ <% if exception.respond_to?(:backtrace) %>
111
+ ===================================================================
112
+ Backtrace:
113
+ ===================================================================
114
+
115
+ <%= exception.backtrace.join("\n ") %>
116
+ <% end %>
117
+ EMAIL
118
+
119
+ end
120
+ end
@@ -0,0 +1,143 @@
1
+ require 'cgi'
2
+ require 'strscan'
3
+
4
+ module Rack
5
+ # Rack middleware for parsing POST/PUT body data into nested parameters
6
+ class NestedParams
7
+
8
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
9
+ POST_BODY = 'rack.input'.freeze
10
+ FORM_INPUT = 'rack.request.form_input'.freeze
11
+ FORM_HASH = 'rack.request.form_hash'.freeze
12
+ FORM_VARS = 'rack.request.form_vars'.freeze
13
+
14
+ # supported content type
15
+ URL_ENCODED = 'application/x-www-form-urlencoded'.freeze
16
+
17
+ def initialize(app)
18
+ @app = app
19
+ end
20
+
21
+ def call(env)
22
+ if form_vars = env[FORM_VARS]
23
+ env[FORM_HASH] = parse_query_parameters(form_vars)
24
+ elsif env[CONTENT_TYPE] == URL_ENCODED
25
+ post_body = env[POST_BODY]
26
+ env[FORM_INPUT] = post_body
27
+ env[FORM_HASH] = parse_query_parameters(post_body.read)
28
+ post_body.rewind if post_body.respond_to?(:rewind)
29
+ end
30
+ @app.call(env)
31
+ end
32
+
33
+ ## the rest is nabbed from Rails ##
34
+
35
+ def parse_query_parameters(query_string)
36
+ return {} if query_string.nil? or query_string.empty?
37
+
38
+ pairs = query_string.split('&').collect do |chunk|
39
+ next if chunk.empty?
40
+ key, value = chunk.split('=', 2)
41
+ next if key.empty?
42
+ value = value.nil? ? nil : CGI.unescape(value)
43
+ [ CGI.unescape(key), value ]
44
+ end.compact
45
+
46
+ UrlEncodedPairParser.new(pairs).result
47
+ end
48
+
49
+ class UrlEncodedPairParser < StringScanner
50
+ attr_reader :top, :parent, :result
51
+
52
+ def initialize(pairs = [])
53
+ super('')
54
+ @result = {}
55
+ pairs.each { |key, value| parse(key, value) }
56
+ end
57
+
58
+ KEY_REGEXP = %r{([^\[\]=&]+)}
59
+ BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
60
+
61
+ # Parse the query string
62
+ def parse(key, value)
63
+ self.string = key
64
+ @top, @parent = result, nil
65
+
66
+ # First scan the bare key
67
+ key = scan(KEY_REGEXP) or return
68
+ key = post_key_check(key)
69
+
70
+ # Then scan as many nestings as present
71
+ until eos?
72
+ r = scan(BRACKETED_KEY_REGEXP) or return
73
+ key = self[1]
74
+ key = post_key_check(key)
75
+ end
76
+
77
+ bind(key, value)
78
+ end
79
+
80
+ private
81
+ # After we see a key, we must look ahead to determine our next action. Cases:
82
+ #
83
+ # [] follows the key. Then the value must be an array.
84
+ # = follows the key. (A value comes next)
85
+ # & or the end of string follows the key. Then the key is a flag.
86
+ # otherwise, a hash follows the key.
87
+ def post_key_check(key)
88
+ if scan(/\[\]/) # a[b][] indicates that b is an array
89
+ container(key, Array)
90
+ nil
91
+ elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
92
+ container(key, Hash)
93
+ nil
94
+ else # End of key? We do nothing.
95
+ key
96
+ end
97
+ end
98
+
99
+ # Add a container to the stack.
100
+ def container(key, klass)
101
+ type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
102
+ value = bind(key, klass.new)
103
+ type_conflict! klass, value unless value.is_a?(klass)
104
+ push(value)
105
+ end
106
+
107
+ # Push a value onto the 'stack', which is actually only the top 2 items.
108
+ def push(value)
109
+ @parent, @top = @top, value
110
+ end
111
+
112
+ # Bind a key (which may be nil for items in an array) to the provided value.
113
+ def bind(key, value)
114
+ if top.is_a? Array
115
+ if key
116
+ if top[-1].is_a?(Hash) && ! top[-1].key?(key)
117
+ top[-1][key] = value
118
+ else
119
+ top << {key => value}
120
+ end
121
+ push top.last
122
+ return top[key]
123
+ else
124
+ top << value
125
+ return value
126
+ end
127
+ elsif top.is_a? Hash
128
+ key = CGI.unescape(key)
129
+ parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
130
+ top[key] ||= value
131
+ return top[key]
132
+ else
133
+ raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
134
+ end
135
+ end
136
+
137
+ def type_conflict!(klass, value)
138
+ raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
139
+ end
140
+ end
141
+
142
+ end
143
+ end
@@ -0,0 +1,18 @@
1
+ module Rack
2
+ # Rack::NotFound is a default endpoint. Initialize with the path to
3
+ # your 404 page.
4
+
5
+ class NotFound
6
+ F = ::File
7
+
8
+ def initialize(path)
9
+ file = F.expand_path(path)
10
+ @content = F.read(file)
11
+ @length = @content.size.to_s
12
+ end
13
+
14
+ def call(env)
15
+ [404, {'Content-Type' => 'text/html', 'Content-Length' => @length}, [@content]]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,40 @@
1
+ begin
2
+ require 'json'
3
+ rescue LoadError => e
4
+ require 'json/pure'
5
+ end
6
+
7
+ module Rack
8
+
9
+ # A Rack middleware for parsing POST/PUT body data when Content-Type is
10
+ # not one of the standard supported types, like <tt>application/json</tt>.
11
+ #
12
+ # TODO: Find a better name.
13
+ #
14
+ class PostBodyContentTypeParser
15
+
16
+ # Constants
17
+ #
18
+ CONTENT_TYPE = 'CONTENT_TYPE'.freeze
19
+ POST_BODY = 'rack.input'.freeze
20
+ FORM_INPUT = 'rack.request.form_input'.freeze
21
+ FORM_HASH = 'rack.request.form_hash'.freeze
22
+
23
+ # Supported Content-Types
24
+ #
25
+ APPLICATION_JSON = 'application/json'.freeze
26
+
27
+ def initialize(app)
28
+ @app = app
29
+ end
30
+
31
+ def call(env)
32
+ case env[CONTENT_TYPE]
33
+ when APPLICATION_JSON
34
+ env.update(FORM_HASH => JSON.parse(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
35
+ end
36
+ @app.call(env)
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,30 @@
1
+ module Rack
2
+ # Middleware to update the process title ($0) with information about the
3
+ # current request. Based loosely on:
4
+ # - http://purefiction.net/mongrel_proctitle/
5
+ # - http://github.com/grempe/thin-proctitle/tree/master
6
+ #
7
+ # NOTE: This will not work properly in a multi-threaded environment.
8
+ class ProcTitle
9
+ F = ::File
10
+ PROGNAME = F.basename($0)
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ @appname = Dir.pwd.split('/').reverse.
15
+ find { |name| name !~ /^(\d+|current|releases)$/ } || PROGNAME
16
+ @requests = 0
17
+ $0 = "#{PROGNAME} [#{@appname}] init ..."
18
+ end
19
+
20
+ def call(env)
21
+ host, port = env['SERVER_NAME'], env['SERVER_PORT']
22
+ meth, path = env['REQUEST_METHOD'], env['PATH_INFO']
23
+ @requests += 1
24
+ $0 = "#{PROGNAME} [#{@appname}/#{port}] (#{@requests}) " \
25
+ "#{meth} #{path}"
26
+
27
+ @app.call(env)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,108 @@
1
+ require 'ruby-prof'
2
+
3
+ module Rack
4
+ # Set the profile=process_time query parameter to download a
5
+ # calltree profile of the request.
6
+ #
7
+ # Pass the :printer option to pick a different result format.
8
+ class Profiler
9
+ MODES = %w(
10
+ process_time
11
+ wall_time
12
+ cpu_time
13
+ allocations
14
+ memory
15
+ gc_runs
16
+ gc_time
17
+ )
18
+
19
+ DEFAULT_PRINTER = RubyProf::CallTreePrinter
20
+ DEFAULT_CONTENT_TYPE = 'application/octet-stream'
21
+
22
+ PRINTER_CONTENT_TYPE = {
23
+ RubyProf::FlatPrinter => 'text/plain',
24
+ RubyProf::GraphPrinter => 'text/plain',
25
+ RubyProf::GraphHtmlPrinter => 'text/html'
26
+ }
27
+
28
+ # Accepts a :printer => [:call_tree|:graph_html|:graph|:flat] option
29
+ # defaulting to :call_tree.
30
+ def initialize(app, options = {})
31
+ @app = app
32
+ @printer = parse_printer(options[:printer])
33
+ @times = (options[:times] || 1).to_i
34
+ end
35
+
36
+ def call(env)
37
+ if mode = profiling?(env)
38
+ profile(env, mode)
39
+ else
40
+ @app.call(env)
41
+ end
42
+ end
43
+
44
+ private
45
+ def profiling?(env)
46
+ unless RubyProf.running?
47
+ request = Rack::Request.new(env.clone)
48
+ if mode = request.params.delete('profile')
49
+ if RubyProf.const_defined?(mode.upcase)
50
+ mode
51
+ else
52
+ env['rack.errors'].write "Invalid RubyProf measure_mode: " +
53
+ "#{mode}. Use one of #{MODES.to_a.join(', ')}"
54
+ false
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def profile(env, mode)
61
+ RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
62
+
63
+ GC.enable_stats if GC.respond_to?(:enable_stats)
64
+ result = RubyProf.profile do
65
+ @times.times { @app.call(env) }
66
+ end
67
+ GC.disable_stats if GC.respond_to?(:disable_stats)
68
+
69
+ [200, headers(@printer, env, mode), print(@printer, result)]
70
+ end
71
+
72
+ def print(printer, result)
73
+ body = StringIO.new
74
+ printer.new(result).print(body, :min_percent => 0.01)
75
+ body.rewind
76
+ body
77
+ end
78
+
79
+ def headers(printer, env, mode)
80
+ headers = { 'Content-Type' => PRINTER_CONTENT_TYPE[printer] || DEFAULT_CONTENT_TYPE }
81
+ if printer == RubyProf::CallTreePrinter
82
+ filename = ::File.basename(env['PATH_INFO'])
83
+ headers['Content-Disposition'] =
84
+ %(attachment; filename="#{filename}.#{mode}.tree")
85
+ end
86
+ headers
87
+ end
88
+
89
+ def parse_printer(printer)
90
+ if printer.nil?
91
+ DEFAULT_PRINTER
92
+ elsif printer.is_a?(Class)
93
+ printer
94
+ else
95
+ name = "#{camel_case(printer)}Printer"
96
+ if RubyProf.const_defined?(name)
97
+ RubyProf.const_get(name)
98
+ else
99
+ DEFAULT_PRINTER
100
+ end
101
+ end
102
+ end
103
+
104
+ def camel_case(word)
105
+ word.to_s.gsub(/(?:^|_)(.)/) { $1.upcase }
106
+ end
107
+ end
108
+ end