corntrace-rack-contrib 1.0.2

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.
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