sinatra 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sinatra might be problematic. Click here for more details.

Files changed (109) hide show
  1. data/ChangeLog +64 -0
  2. data/LICENSE +1 -1
  3. data/README.rdoc +244 -141
  4. data/Rakefile +111 -0
  5. data/lib/sinatra.rb +942 -627
  6. data/{vendor/rack/lib → lib/sinatra}/rack/handler/mongrel.rb +10 -3
  7. data/lib/sinatra/test/methods.rb +56 -36
  8. data/lib/sinatra/test/rspec.rb +10 -0
  9. data/lib/sinatra/test/spec.rb +2 -2
  10. data/lib/sinatra/test/unit.rb +1 -1
  11. data/sinatra.gemspec +71 -43
  12. data/test/app_test.rb +181 -32
  13. data/test/application_test.rb +179 -36
  14. data/test/custom_error_test.rb +17 -22
  15. data/test/erb_test.rb +42 -22
  16. data/test/event_context_test.rb +3 -3
  17. data/test/events_test.rb +8 -11
  18. data/test/filter_test.rb +30 -0
  19. data/test/haml_test.rb +83 -31
  20. data/test/helper.rb +6 -3
  21. data/test/mapped_error_test.rb +32 -21
  22. data/test/pipeline_test.rb +66 -0
  23. data/test/sass_test.rb +7 -7
  24. data/test/sessions_test.rb +8 -9
  25. data/test/streaming_test.rb +10 -4
  26. data/test/sym_params_test.rb +1 -1
  27. data/test/template_test.rb +11 -11
  28. data/test/use_in_file_templates_test.rb +9 -10
  29. metadata +22 -101
  30. data/CHANGELOG +0 -3
  31. data/Manifest +0 -122
  32. data/index.html +0 -9
  33. data/test/diddy_test.rb +0 -41
  34. data/test/rest_test.rb +0 -16
  35. data/vendor/rack/AUTHORS +0 -7
  36. data/vendor/rack/COPYING +0 -18
  37. data/vendor/rack/KNOWN-ISSUES +0 -18
  38. data/vendor/rack/README +0 -242
  39. data/vendor/rack/Rakefile +0 -174
  40. data/vendor/rack/bin/rackup +0 -153
  41. data/vendor/rack/contrib/rack_logo.svg +0 -111
  42. data/vendor/rack/example/lobster.ru +0 -4
  43. data/vendor/rack/example/protectedlobster.rb +0 -14
  44. data/vendor/rack/example/protectedlobster.ru +0 -8
  45. data/vendor/rack/lib/rack.rb +0 -92
  46. data/vendor/rack/lib/rack/adapter/camping.rb +0 -22
  47. data/vendor/rack/lib/rack/auth/abstract/handler.rb +0 -28
  48. data/vendor/rack/lib/rack/auth/abstract/request.rb +0 -37
  49. data/vendor/rack/lib/rack/auth/basic.rb +0 -58
  50. data/vendor/rack/lib/rack/auth/digest/md5.rb +0 -124
  51. data/vendor/rack/lib/rack/auth/digest/nonce.rb +0 -51
  52. data/vendor/rack/lib/rack/auth/digest/params.rb +0 -55
  53. data/vendor/rack/lib/rack/auth/digest/request.rb +0 -40
  54. data/vendor/rack/lib/rack/auth/openid.rb +0 -116
  55. data/vendor/rack/lib/rack/builder.rb +0 -56
  56. data/vendor/rack/lib/rack/cascade.rb +0 -36
  57. data/vendor/rack/lib/rack/commonlogger.rb +0 -56
  58. data/vendor/rack/lib/rack/file.rb +0 -112
  59. data/vendor/rack/lib/rack/handler/cgi.rb +0 -57
  60. data/vendor/rack/lib/rack/handler/fastcgi.rb +0 -83
  61. data/vendor/rack/lib/rack/handler/lsws.rb +0 -52
  62. data/vendor/rack/lib/rack/handler/scgi.rb +0 -57
  63. data/vendor/rack/lib/rack/handler/webrick.rb +0 -57
  64. data/vendor/rack/lib/rack/lint.rb +0 -394
  65. data/vendor/rack/lib/rack/lobster.rb +0 -65
  66. data/vendor/rack/lib/rack/mock.rb +0 -160
  67. data/vendor/rack/lib/rack/recursive.rb +0 -57
  68. data/vendor/rack/lib/rack/reloader.rb +0 -64
  69. data/vendor/rack/lib/rack/request.rb +0 -197
  70. data/vendor/rack/lib/rack/response.rb +0 -166
  71. data/vendor/rack/lib/rack/session/abstract/id.rb +0 -126
  72. data/vendor/rack/lib/rack/session/cookie.rb +0 -71
  73. data/vendor/rack/lib/rack/session/memcache.rb +0 -83
  74. data/vendor/rack/lib/rack/session/pool.rb +0 -67
  75. data/vendor/rack/lib/rack/showexceptions.rb +0 -344
  76. data/vendor/rack/lib/rack/showstatus.rb +0 -103
  77. data/vendor/rack/lib/rack/static.rb +0 -38
  78. data/vendor/rack/lib/rack/urlmap.rb +0 -48
  79. data/vendor/rack/lib/rack/utils.rb +0 -240
  80. data/vendor/rack/test/cgi/lighttpd.conf +0 -20
  81. data/vendor/rack/test/cgi/test +0 -9
  82. data/vendor/rack/test/cgi/test.fcgi +0 -7
  83. data/vendor/rack/test/cgi/test.ru +0 -7
  84. data/vendor/rack/test/spec_rack_auth_basic.rb +0 -69
  85. data/vendor/rack/test/spec_rack_auth_digest.rb +0 -169
  86. data/vendor/rack/test/spec_rack_builder.rb +0 -50
  87. data/vendor/rack/test/spec_rack_camping.rb +0 -47
  88. data/vendor/rack/test/spec_rack_cascade.rb +0 -50
  89. data/vendor/rack/test/spec_rack_cgi.rb +0 -91
  90. data/vendor/rack/test/spec_rack_commonlogger.rb +0 -32
  91. data/vendor/rack/test/spec_rack_fastcgi.rb +0 -91
  92. data/vendor/rack/test/spec_rack_file.rb +0 -40
  93. data/vendor/rack/test/spec_rack_lint.rb +0 -317
  94. data/vendor/rack/test/spec_rack_lobster.rb +0 -45
  95. data/vendor/rack/test/spec_rack_mock.rb +0 -152
  96. data/vendor/rack/test/spec_rack_mongrel.rb +0 -165
  97. data/vendor/rack/test/spec_rack_recursive.rb +0 -77
  98. data/vendor/rack/test/spec_rack_request.rb +0 -384
  99. data/vendor/rack/test/spec_rack_response.rb +0 -167
  100. data/vendor/rack/test/spec_rack_session_cookie.rb +0 -49
  101. data/vendor/rack/test/spec_rack_session_memcache.rb +0 -100
  102. data/vendor/rack/test/spec_rack_session_pool.rb +0 -84
  103. data/vendor/rack/test/spec_rack_showexceptions.rb +0 -21
  104. data/vendor/rack/test/spec_rack_showstatus.rb +0 -71
  105. data/vendor/rack/test/spec_rack_static.rb +0 -37
  106. data/vendor/rack/test/spec_rack_urlmap.rb +0 -175
  107. data/vendor/rack/test/spec_rack_utils.rb +0 -57
  108. data/vendor/rack/test/spec_rack_webrick.rb +0 -106
  109. data/vendor/rack/test/testrequest.rb +0 -43
@@ -0,0 +1,111 @@
1
+ require 'rake/clean'
2
+
3
+ task :default => :test
4
+
5
+ # SPECS ===============================================================
6
+
7
+ desc 'Run specs with story style output'
8
+ task :spec do
9
+ sh 'specrb --specdox -Ilib:test test/*_test.rb'
10
+ end
11
+
12
+ desc 'Run specs with unit test style output'
13
+ task :test => FileList['test/*_test.rb'] do |t|
14
+ suite = t.prerequisites.map{|f| "-r#{f.chomp('.rb')}"}.join(' ')
15
+ sh "ruby -Ilib:test #{suite} -e ''", :verbose => false
16
+ end
17
+
18
+ # PACKAGING ============================================================
19
+
20
+ # Load the gemspec using the same limitations as github
21
+ def spec
22
+ @spec ||=
23
+ begin
24
+ require 'rubygems/specification'
25
+ data = File.read('sinatra.gemspec')
26
+ spec = nil
27
+ Thread.new { spec = eval("$SAFE = 3\n#{data}") }.join
28
+ spec
29
+ end
30
+ end
31
+
32
+ def package(ext='')
33
+ "dist/sinatra-#{spec.version}" + ext
34
+ end
35
+
36
+ desc 'Build packages'
37
+ task :package => %w[.gem .tar.gz].map {|e| package(e)}
38
+
39
+ desc 'Build and install as local gem'
40
+ task :install => package('.gem') do
41
+ sh "gem install #{package('.gem')}"
42
+ end
43
+
44
+ directory 'dist/'
45
+
46
+ file package('.gem') => %w[dist/ sinatra.gemspec] + spec.files do |f|
47
+ sh "gem build sinatra.gemspec"
48
+ mv File.basename(f.name), f.name
49
+ end
50
+
51
+ file package('.tar.gz') => %w[dist/] + spec.files do |f|
52
+ sh "git archive --format=tar HEAD | gzip > #{f.name}"
53
+ end
54
+
55
+ # Rubyforge Release / Publish Tasks ==================================
56
+
57
+ desc 'Publish API docs to rubyforge'
58
+ task 'publish:doc' => 'doc/api/index.html' do
59
+ sh 'scp -rp doc/* rubyforge.org:/var/www/gforge-projects/sinatra/'
60
+ end
61
+
62
+ task 'publish:gem' => [package('.gem'), package('.tar.gz')] do |t|
63
+ sh <<-end
64
+ rubyforge add_release sinatra sinatra #{spec.version} #{package('.gem')} &&
65
+ rubyforge add_file sinatra sinatra #{spec.version} #{package('.tar.gz')}
66
+ end
67
+ end
68
+
69
+ # Gemspec Helpers ====================================================
70
+
71
+ file 'sinatra.gemspec' => FileList['{lib,test,images}/**','Rakefile'] do |f|
72
+ # read spec file and split out manifest section
73
+ spec = File.read(f.name)
74
+ parts = spec.split(" # = MANIFEST =\n")
75
+ fail 'bad spec' if parts.length != 3
76
+ # determine file list from git ls-files
77
+ files = `git ls-files`.
78
+ split("\n").
79
+ sort.
80
+ reject{ |file| file =~ /^\./ }.
81
+ map{ |file| " #{file}" }.
82
+ join("\n")
83
+ # piece file back together and write...
84
+ parts[1] = " s.files = %w[\n#{files}\n ]\n"
85
+ spec = parts.join(" # = MANIFEST =\n")
86
+ File.open(f.name, 'w') { |io| io.write(spec) }
87
+ puts "updated #{f.name}"
88
+ end
89
+
90
+ # Hanna RDoc =========================================================
91
+ #
92
+ # Building docs requires the hanna gem:
93
+ # gem install mislav-hanna --source=http://gems.github.com
94
+
95
+ desc 'Generate Hanna RDoc under doc/api'
96
+ task :doc => ['doc/api/index.html']
97
+
98
+ file 'doc/api/index.html' => FileList['lib/**/*.rb','README.rdoc'] do |f|
99
+ rb_files = f.prerequisites
100
+ sh((<<-end).gsub(/\s+/, ' '))
101
+ hanna --charset utf8 \
102
+ --fmt html \
103
+ --inline-source \
104
+ --line-numbers \
105
+ --main README.rdoc \
106
+ --op doc/api \
107
+ --title 'Sinatra API Documentation' \
108
+ #{rb_files.join(' ')}
109
+ end
110
+ end
111
+ CLEAN.include 'doc/api'
@@ -1,48 +1,30 @@
1
- Dir[File.dirname(__FILE__) + "/../vendor/*"].each do |l|
2
- $:.unshift "#{File.expand_path(l)}/lib"
3
- end
4
-
5
- require 'rack'
6
-
7
- require 'rubygems'
8
- require 'uri'
9
1
  require 'time'
10
2
  require 'ostruct'
3
+ require 'uri'
4
+ require 'rack'
11
5
 
12
6
  if ENV['SWIFT']
13
7
  require 'swiftcore/swiftiplied_mongrel'
14
8
  puts "Using Swiftiplied Mongrel"
15
9
  elsif ENV['EVENT']
16
- require 'swiftcore/evented_mongrel'
10
+ require 'swiftcore/evented_mongrel'
17
11
  puts "Using Evented Mongrel"
18
12
  end
19
13
 
20
- class Class
21
- def dslify_writer(*syms)
22
- syms.each do |sym|
23
- class_eval <<-end_eval
24
- def #{sym}(v=nil)
25
- self.send "#{sym}=", v if v
26
- v
27
- end
28
- end_eval
29
- end
30
- end
31
- end
32
-
33
14
  module Rack #:nodoc:
34
-
15
+
35
16
  class Request #:nodoc:
36
17
 
37
- # Set of request method names allowed via the _method parameter hack. By default,
38
- # all request methods defined in RFC2616 are included, with the exception of
39
- # TRACE and CONNECT.
18
+ # Set of request method names allowed via the _method parameter hack. By
19
+ # default, all request methods defined in RFC2616 are included, with the
20
+ # exception of TRACE and CONNECT.
40
21
  POST_TUNNEL_METHODS_ALLOWED = %w( PUT DELETE OPTIONS HEAD )
41
22
 
42
- # Return the HTTP request method with support for method tunneling using the POST
43
- # _method parameter hack. If the real request method is POST and a _method param is
44
- # given and the value is one defined in +POST_TUNNEL_METHODS_ALLOWED+, return the value
45
- # of the _method param instead.
23
+ # Return the HTTP request method with support for method tunneling using
24
+ # the POST _method parameter hack. If the real request method is POST and
25
+ # a _method param is given and the value is one defined in
26
+ # +POST_TUNNEL_METHODS_ALLOWED+, return the value of the _method param
27
+ # instead.
46
28
  def request_method
47
29
  if post_tunnel_method_hack?
48
30
  params['_method'].upcase
@@ -52,84 +34,87 @@ module Rack #:nodoc:
52
34
  end
53
35
 
54
36
  def user_agent
55
- env['HTTP_USER_AGENT']
37
+ @env['HTTP_USER_AGENT']
56
38
  end
57
39
 
58
- private
59
-
60
- # Return truthfully if and only if the following conditions are met: 1.) the
61
- # *actual* request method is POST, 2.) the request content-type is one of
62
- # 'application/x-www-form-urlencoded' or 'multipart/form-data', 3.) there is a
63
- # "_method" parameter in the POST body (not in the query string), and 4.) the
64
- # method parameter is one of the verbs listed in the POST_TUNNEL_METHODS_ALLOWED
65
- # list.
66
- def post_tunnel_method_hack?
67
- @env['REQUEST_METHOD'] == 'POST' &&
68
- POST_TUNNEL_METHODS_ALLOWED.include?(self.POST.fetch('_method', '').upcase)
69
- end
40
+ private
70
41
 
42
+ # Return truthfully if the request is a valid verb-over-post hack.
43
+ def post_tunnel_method_hack?
44
+ @env['REQUEST_METHOD'] == 'POST' &&
45
+ POST_TUNNEL_METHODS_ALLOWED.include?(self.POST.fetch('_method', '').upcase)
46
+ end
71
47
  end
72
-
48
+
73
49
  module Utils
74
50
  extend self
75
51
  end
76
52
 
53
+ module Handler
54
+ autoload :Mongrel, ::File.dirname(__FILE__) + "/sinatra/rack/handler/mongrel"
55
+ end
56
+
77
57
  end
78
58
 
59
+
79
60
  module Sinatra
80
61
  extend self
81
62
 
82
- module Version
83
- MAJOR = '0'
84
- MINOR = '2'
85
- REVISION = '2'
86
- def self.combined
87
- [MAJOR, MINOR, REVISION].join('.')
88
- end
89
- end
63
+ VERSION = '0.3.0'
90
64
 
91
- class NotFound < RuntimeError; end
92
- class ServerError < RuntimeError; end
65
+ class NotFound < RuntimeError
66
+ def self.code ; 404 ; end
67
+ end
68
+ class ServerError < RuntimeError
69
+ def self.code ; 500 ; end
70
+ end
93
71
 
94
72
  Result = Struct.new(:block, :params, :status) unless defined?(Result)
95
73
 
96
74
  def options
97
75
  application.options
98
76
  end
99
-
77
+
100
78
  def application
101
- unless @app
102
- @app = Application.new
103
- Sinatra::Environment.setup!
104
- end
105
- @app
79
+ @app ||= Application.new
106
80
  end
107
-
81
+
108
82
  def application=(app)
109
83
  @app = app
110
84
  end
111
-
85
+
112
86
  def port
113
87
  application.options.port
114
88
  end
115
-
89
+
90
+ def host
91
+ application.options.host
92
+ end
93
+
116
94
  def env
117
95
  application.options.env
118
96
  end
119
-
120
- def build_application
121
- app = application
122
- app = Rack::Session::Cookie.new(app) if Sinatra.options.sessions == true
123
- app = Rack::CommonLogger.new(app) if Sinatra.options.logging == true
124
- app
97
+
98
+ # Deprecated: use application instead of build_application.
99
+ alias :build_application :application
100
+
101
+ def server
102
+ options.server ||= defined?(Rack::Handler::Thin) ? "thin" : "mongrel"
103
+
104
+ # Convert the server into the actual handler name
105
+ handler = options.server.capitalize
106
+
107
+ # If the convenience conversion didn't get us anything,
108
+ # fall back to what the user actually set.
109
+ handler = options.server unless Rack::Handler.const_defined?(handler)
110
+
111
+ @server ||= eval("Rack::Handler::#{handler}")
125
112
  end
126
-
113
+
127
114
  def run
128
-
129
115
  begin
130
- puts "== Sinatra has taken the stage on port #{port} for #{env}"
131
- require 'pp'
132
- Rack::Handler::Mongrel.run(build_application, :Port => port) do |server|
116
+ puts "== Sinatra/#{Sinatra::VERSION} has taken the stage on port #{port} for #{env} with backup by #{server.name}"
117
+ server.run(application, {:Port => port, :Host => host}) do |server|
133
118
  trap(:INT) do
134
119
  server.stop
135
120
  puts "\n== Sinatra has ended his set (crowd applauds)"
@@ -138,103 +123,124 @@ module Sinatra
138
123
  rescue Errno::EADDRINUSE => e
139
124
  puts "== Someone is already performing on port #{port}!"
140
125
  end
141
-
142
126
  end
143
-
127
+
144
128
  class Event
129
+ include Rack::Utils
145
130
 
146
131
  URI_CHAR = '[^/?:,&#\.]'.freeze unless defined?(URI_CHAR)
147
- PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
132
+ PARAM = /(:(#{URI_CHAR}+)|\*)/.freeze unless defined?(PARAM)
148
133
  SPLAT = /(.*?)/
149
134
  attr_reader :path, :block, :param_keys, :pattern, :options
150
-
135
+
151
136
  def initialize(path, options = {}, &b)
152
137
  @path = URI.encode(path)
153
138
  @block = b
154
139
  @param_keys = []
155
140
  @options = options
156
- regex = @path.to_s.gsub(PARAM) do
157
- @param_keys << $1
158
- "(#{URI_CHAR}+)"
141
+ splats = 0
142
+ regex = @path.to_s.gsub(PARAM) do |match|
143
+ if match == "*"
144
+ @param_keys << "_splat_#{splats}"
145
+ splats += 1
146
+ SPLAT.to_s
147
+ else
148
+ @param_keys << $2
149
+ "(#{URI_CHAR}+)"
150
+ end
159
151
  end
160
-
161
- regex.gsub!('*', SPLAT.to_s)
162
-
152
+
163
153
  @pattern = /^#{regex}$/
164
154
  end
165
-
155
+
166
156
  def invoke(request)
167
157
  params = {}
168
- if agent = options[:agent]
158
+ if agent = options[:agent]
169
159
  return unless request.user_agent =~ agent
170
160
  params[:agent] = $~[1..-1]
171
161
  end
172
- if host = options[:host]
162
+ if host = options[:host]
173
163
  return unless host === request.host
174
164
  end
175
165
  return unless pattern =~ request.path_info.squeeze('/')
176
- params.merge!(param_keys.zip($~.captures.map(&:from_param)).to_hash)
166
+ path_params = param_keys.zip($~.captures.map{|s| unescape(s)}).to_hash
167
+ params.merge!(path_params)
168
+ splats = params.select { |k, v| k =~ /^_splat_\d+$/ }.sort.map(&:last)
169
+ unless splats.empty?
170
+ params.delete_if { |k, v| k =~ /^_splat_\d+$/ }
171
+ params["splat"] = splats
172
+ end
177
173
  Result.new(block, params, 200)
178
174
  end
179
-
175
+
180
176
  end
181
-
177
+
182
178
  class Error
183
-
184
- attr_reader :code, :block
185
-
186
- def initialize(code, &b)
187
- @code, @block = code, b
179
+
180
+ attr_reader :type, :block, :options
181
+
182
+ def initialize(type, options={}, &block)
183
+ @type = type
184
+ @block = block
185
+ @options = options
188
186
  end
189
-
187
+
190
188
  def invoke(request)
191
- Result.new(block, {}, 404)
189
+ Result.new(block, options, code)
192
190
  end
193
-
191
+
192
+ def code
193
+ if type.respond_to?(:code)
194
+ type.code
195
+ else
196
+ 500
197
+ end
198
+ end
199
+
194
200
  end
195
-
201
+
196
202
  class Static
197
-
203
+ include Rack::Utils
204
+
198
205
  def invoke(request)
199
206
  return unless File.file?(
200
- Sinatra.application.options.public + request.path_info
207
+ Sinatra.application.options.public + unescape(request.path_info)
201
208
  )
202
209
  Result.new(block, {}, 200)
203
210
  end
204
-
211
+
205
212
  def block
206
213
  Proc.new do
207
- send_file Sinatra.application.options.public + request.path_info,
208
- :disposition => nil
214
+ send_file Sinatra.application.options.public +
215
+ unescape(request.path_info), :disposition => nil
209
216
  end
210
217
  end
211
-
218
+
212
219
  end
213
-
214
- # Adapted from actionpack
220
+
215
221
  # Methods for sending files and streams to the browser instead of rendering.
216
222
  module Streaming
217
223
  DEFAULT_SEND_FILE_OPTIONS = {
218
224
  :type => 'application/octet-stream'.freeze,
219
225
  :disposition => 'attachment'.freeze,
220
- :stream => true,
226
+ :stream => true,
221
227
  :buffer_size => 4096
222
228
  }.freeze
223
229
 
224
230
  class MissingFile < RuntimeError; end
225
231
 
226
232
  class FileStreamer
227
-
233
+
228
234
  attr_reader :path, :options
229
-
235
+
230
236
  def initialize(path, options)
231
237
  @path, @options = path, options
232
238
  end
233
-
239
+
234
240
  def to_result(cx, *args)
235
241
  self
236
242
  end
237
-
243
+
238
244
  def each
239
245
  File.open(path, 'rb') do |file|
240
246
  while buf = file.read(options[:buffer_size])
@@ -242,161 +248,281 @@ module Sinatra
242
248
  end
243
249
  end
244
250
  end
245
-
246
- end
247
-
248
- protected
249
- # Sends the file by streaming it 4096 bytes at a time. This way the
250
- # whole file doesn't need to be read into memory at once. This makes
251
- # it feasible to send even large files.
252
- #
253
- # Be careful to sanitize the path parameter if it coming from a web
254
- # page. send_file(params[:path]) allows a malicious user to
255
- # download any file on your server.
256
- #
257
- # Options:
258
- # * <tt>:filename</tt> - suggests a filename for the browser to use.
259
- # Defaults to File.basename(path).
260
- # * <tt>:type</tt> - specifies an HTTP content type.
261
- # Defaults to 'application/octet-stream'.
262
- # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
263
- # Valid values are 'inline' and 'attachment' (default). When set to nil, the
264
- # Content-Disposition and Content-Transfer-Encoding headers are omitted entirely.
265
- # * <tt>:stream</tt> - whether to send the file to the user agent as it is read (true)
266
- # or to read the entire file before sending (false). Defaults to true.
267
- # * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
268
- # Defaults to 4096.
269
- # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
270
- # * <tt>:last_modified</tt> - an optional RFC 2616 formatted date value (See Time#httpdate)
271
- # indicating the last modified time of the file. If the request includes an
272
- # If-Modified-Since header that matches this value exactly, a 304 Not Modified response
273
- # is sent instead of the file. Defaults to the file's last modified
274
- # time.
275
- #
276
- # The default Content-Type and Content-Disposition headers are
277
- # set to download arbitrary binary files in as many browsers as
278
- # possible. IE versions 4, 5, 5.5, and 6 are all known to have
279
- # a variety of quirks (especially when downloading over SSL).
280
- #
281
- # Simple download:
282
- # send_file '/path/to.zip'
283
- #
284
- # Show a JPEG in the browser:
285
- # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
286
- #
287
- # Show a 404 page in the browser:
288
- # send_file '/path/to/404.html, :type => 'text/html; charset=utf-8', :status => 404
289
- #
290
- # Read about the other Content-* HTTP headers if you'd like to
291
- # provide the user with more information (such as Content-Description).
292
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
293
- #
294
- # Also be aware that the document may be cached by proxies and browsers.
295
- # The Pragma and Cache-Control headers declare how the file may be cached
296
- # by intermediaries. They default to require clients to validate with
297
- # the server before releasing cached responses. See
298
- # http://www.mnot.net/cache_docs/ for an overview of web caching and
299
- # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
300
- # for the Cache-Control header spec.
301
- def send_file(path, options = {}) #:doc:
302
- raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
303
-
304
- options[:length] ||= File.size(path)
305
- options[:filename] ||= File.basename(path)
306
- options[:type] ||= Rack::File::MIME_TYPES[File.extname(options[:filename])[1..-1]] || 'text/plain'
307
- options[:last_modified] ||= File.mtime(path).httpdate
308
- send_file_headers! options
309
-
310
- if options[:stream]
311
- throw :halt, [options[:status] || 200, FileStreamer.new(path, options)]
312
- else
313
- File.open(path, 'rb') { |file| throw :halt, [options[:status] || 200, file.read] }
314
- end
315
- end
316
251
 
317
- # Send binary data to the user as a file download. May set content type, apparent file name,
318
- # and specify whether to show data inline or download as an attachment.
319
- #
320
- # Options:
321
- # * <tt>:filename</tt> - Suggests a filename for the browser to use.
322
- # * <tt>:type</tt> - specifies an HTTP content type.
323
- # Defaults to 'application/octet-stream'.
324
- # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
325
- # Valid values are 'inline' and 'attachment' (default).
326
- # * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
327
- # * <tt>:last_modified</tt> - an optional RFC 2616 formatted date value (See Time#httpdate)
328
- # indicating the last modified time of the response entity. If the request includes an
329
- # If-Modified-Since header that matches this value exactly, a 304 Not Modified response
330
- # is sent instead of the data.
331
- #
332
- # Generic data download:
333
- # send_data buffer
334
- #
335
- # Download a dynamically-generated tarball:
336
- # send_data generate_tgz('dir'), :filename => 'dir.tgz'
337
- #
338
- # Display an image Active Record in the browser:
339
- # send_data image.data, :type => image.content_type, :disposition => 'inline'
340
- #
341
- # See +send_file+ for more information on HTTP Content-* headers and caching.
342
- def send_data(data, options = {}) #:doc:
343
- send_file_headers! options.merge(:length => data.size)
344
- throw :halt, [options[:status] || 200, data]
252
+ end
253
+
254
+ protected
255
+
256
+ # Sends the file by streaming it 4096 bytes at a time. This way the
257
+ # whole file doesn't need to be read into memory at once. This makes
258
+ # it feasible to send even large files.
259
+ #
260
+ # Be careful to sanitize the path parameter if it coming from a web
261
+ # page. send_file(params[:path]) allows a malicious user to
262
+ # download any file on your server.
263
+ #
264
+ # Options:
265
+ # * <tt>:filename</tt> - suggests a filename for the browser to use.
266
+ # Defaults to File.basename(path).
267
+ # * <tt>:type</tt> - specifies an HTTP content type.
268
+ # Defaults to 'application/octet-stream'.
269
+ # * <tt>:disposition</tt> - specifies whether the file will be shown
270
+ # inline or downloaded. Valid values are 'inline' and 'attachment'
271
+ # (default). When set to nil, the Content-Disposition and
272
+ # Content-Transfer-Encoding headers are omitted entirely.
273
+ # * <tt>:stream</tt> - whether to send the file to the user agent as it
274
+ # is read (true) or to read the entire file before sending (false).
275
+ # Defaults to true.
276
+ # * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used
277
+ # to stream the file. Defaults to 4096.
278
+ # * <tt>:status</tt> - specifies the status code to send with the
279
+ # response. Defaults to '200 OK'.
280
+ # * <tt>:last_modified</tt> - an optional RFC 2616 formatted date value
281
+ # (See Time#httpdate) indicating the last modified time of the file.
282
+ # If the request includes an If-Modified-Since header that matches this
283
+ # value exactly, a 304 Not Modified response is sent instead of the file.
284
+ # Defaults to the file's last modified time.
285
+ #
286
+ # The default Content-Type and Content-Disposition headers are
287
+ # set to download arbitrary binary files in as many browsers as
288
+ # possible. IE versions 4, 5, 5.5, and 6 are all known to have
289
+ # a variety of quirks (especially when downloading over SSL).
290
+ #
291
+ # Simple download:
292
+ # send_file '/path/to.zip'
293
+ #
294
+ # Show a JPEG in the browser:
295
+ # send_file '/path/to.jpeg',
296
+ # :type => 'image/jpeg',
297
+ # :disposition => 'inline'
298
+ #
299
+ # Show a 404 page in the browser:
300
+ # send_file '/path/to/404.html,
301
+ # :type => 'text/html; charset=utf-8',
302
+ # :status => 404
303
+ #
304
+ # Read about the other Content-* HTTP headers if you'd like to
305
+ # provide the user with more information (such as Content-Description).
306
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
307
+ #
308
+ # Also be aware that the document may be cached by proxies and browsers.
309
+ # The Pragma and Cache-Control headers declare how the file may be cached
310
+ # by intermediaries. They default to require clients to validate with
311
+ # the server before releasing cached responses. See
312
+ # http://www.mnot.net/cache_docs/ for an overview of web caching and
313
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
314
+ # for the Cache-Control header spec.
315
+ def send_file(path, options = {}) #:doc:
316
+ raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
317
+
318
+ options[:length] ||= File.size(path)
319
+ options[:filename] ||= File.basename(path)
320
+ options[:type] ||= Rack::File::MIME_TYPES[File.extname(options[:filename])[1..-1]] || 'text/plain'
321
+ options[:last_modified] ||= File.mtime(path).httpdate
322
+ send_file_headers! options
323
+
324
+ if options[:stream]
325
+ throw :halt, [options[:status] || 200, FileStreamer.new(path, options)]
326
+ else
327
+ File.open(path, 'rb') { |file| throw :halt, [options[:status] || 200, file.read] }
345
328
  end
329
+ end
346
330
 
347
- private
348
- def send_file_headers!(options)
349
- options = DEFAULT_SEND_FILE_OPTIONS.merge(options)
350
- [:length, :type, :disposition].each do |arg|
351
- raise ArgumentError, ":#{arg} option required" unless options.key?(arg)
352
- end
331
+ # Send binary data to the user as a file download. May set content type,
332
+ # apparent file name, and specify whether to show data inline or download
333
+ # as an attachment.
334
+ #
335
+ # Options:
336
+ # * <tt>:filename</tt> - Suggests a filename for the browser to use.
337
+ # * <tt>:type</tt> - specifies an HTTP content type.
338
+ # Defaults to 'application/octet-stream'.
339
+ # * <tt>:disposition</tt> - specifies whether the file will be shown inline
340
+ # or downloaded. Valid values are 'inline' and 'attachment' (default).
341
+ # * <tt>:status</tt> - specifies the status code to send with the response.
342
+ # Defaults to '200 OK'.
343
+ # * <tt>:last_modified</tt> - an optional RFC 2616 formatted date value (See
344
+ # Time#httpdate) indicating the last modified time of the response entity.
345
+ # If the request includes an If-Modified-Since header that matches this
346
+ # value exactly, a 304 Not Modified response is sent instead of the data.
347
+ #
348
+ # Generic data download:
349
+ # send_data buffer
350
+ #
351
+ # Download a dynamically-generated tarball:
352
+ # send_data generate_tgz('dir'), :filename => 'dir.tgz'
353
+ #
354
+ # Display an image Active Record in the browser:
355
+ # send_data image.data,
356
+ # :type => image.content_type,
357
+ # :disposition => 'inline'
358
+ #
359
+ # See +send_file+ for more information on HTTP Content-* headers and caching.
360
+ def send_data(data, options = {}) #:doc:
361
+ send_file_headers! options.merge(:length => data.size)
362
+ throw :halt, [options[:status] || 200, data]
363
+ end
353
364
 
354
- # Send a "304 Not Modified" if the last_modified option is provided and matches
355
- # the If-Modified-Since request header value.
356
- if last_modified = options[:last_modified]
357
- header 'Last-Modified' => last_modified
358
- throw :halt, [ 304, '' ] if last_modified == request.env['HTTP_IF_MODIFIED_SINCE']
359
- end
365
+ private
360
366
 
361
- headers(
362
- 'Content-Length' => options[:length].to_s,
363
- 'Content-Type' => options[:type].strip # fixes a problem with extra '\r' with some browsers
364
- )
365
-
366
- # Omit Content-Disposition and Content-Transfer-Encoding headers if
367
- # the :disposition option set to nil.
368
- if !options[:disposition].nil?
369
- disposition = options[:disposition].dup || 'attachment'
370
- disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
371
- headers 'Content-Disposition' => disposition, 'Content-Transfer-Encoding' => 'binary'
372
- end
367
+ def send_file_headers!(options)
368
+ options = DEFAULT_SEND_FILE_OPTIONS.merge(options)
369
+ [:length, :type, :disposition].each do |arg|
370
+ raise ArgumentError, ":#{arg} option required" unless options.key?(arg)
371
+ end
373
372
 
374
- # Fix a problem with IE 6.0 on opening downloaded files:
375
- # If Cache-Control: no-cache is set (which Rails does by default),
376
- # IE removes the file it just downloaded from its cache immediately
377
- # after it displays the "open/save" dialog, which means that if you
378
- # hit "open" the file isn't there anymore when the application that
379
- # is called for handling the download is run, so let's workaround that
380
- header('Cache-Control' => 'private') if headers['Cache-Control'] == 'no-cache'
373
+ # Send a "304 Not Modified" if the last_modified option is provided and
374
+ # matches the If-Modified-Since request header value.
375
+ if last_modified = options[:last_modified]
376
+ header 'Last-Modified' => last_modified
377
+ throw :halt, [ 304, '' ] if last_modified == request.env['HTTP_IF_MODIFIED_SINCE']
381
378
  end
379
+
380
+ headers(
381
+ 'Content-Length' => options[:length].to_s,
382
+ 'Content-Type' => options[:type].strip # fixes a problem with extra '\r' with some browsers
383
+ )
384
+
385
+ # Omit Content-Disposition and Content-Transfer-Encoding headers if
386
+ # the :disposition option set to nil.
387
+ if !options[:disposition].nil?
388
+ disposition = options[:disposition].dup || 'attachment'
389
+ disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
390
+ headers 'Content-Disposition' => disposition, 'Content-Transfer-Encoding' => 'binary'
391
+ end
392
+
393
+ # Fix a problem with IE 6.0 on opening downloaded files:
394
+ # If Cache-Control: no-cache is set (which Rails does by default),
395
+ # IE removes the file it just downloaded from its cache immediately
396
+ # after it displays the "open/save" dialog, which means that if you
397
+ # hit "open" the file isn't there anymore when the application that
398
+ # is called for handling the download is run, so let's workaround that
399
+ header('Cache-Control' => 'private') if headers['Cache-Control'] == 'no-cache'
400
+ end
382
401
  end
383
-
402
+
403
+
404
+ # Helper methods for building various aspects of the HTTP response.
384
405
  module ResponseHelpers
385
406
 
407
+ # Immediately halt response execution by redirecting to the resource
408
+ # specified. The +path+ argument may be an absolute URL or a path
409
+ # relative to the site root. Additional arguments are passed to the
410
+ # halt.
411
+ #
412
+ # With no integer status code, a '302 Temporary Redirect' response is
413
+ # sent. To send a permanent redirect, pass an explicit status code of
414
+ # 301:
415
+ #
416
+ # redirect '/somewhere/else', 301
417
+ #
418
+ # NOTE: No attempt is made to rewrite the path based on application
419
+ # context. The 'Location' response header is set verbatim to the value
420
+ # provided.
386
421
  def redirect(path, *args)
387
422
  status(302)
388
- headers 'Location' => path
423
+ header 'Location' => path
389
424
  throw :halt, *args
390
425
  end
391
-
426
+
427
+ # Access or modify response headers. With no argument, return the
428
+ # underlying headers Hash. With a Hash argument, add or overwrite
429
+ # existing response headers with the values provided:
430
+ #
431
+ # headers 'Content-Type' => "text/html;charset=utf-8",
432
+ # 'Last-Modified' => Time.now.httpdate,
433
+ # 'X-UA-Compatible' => 'IE=edge'
434
+ #
435
+ # This method also available in singular form (#header).
392
436
  def headers(header = nil)
393
437
  @response.headers.merge!(header) if header
394
438
  @response.headers
395
439
  end
396
440
  alias :header :headers
397
441
 
442
+ # Set the content type of the response body (HTTP 'Content-Type' header).
443
+ #
444
+ # The +type+ argument may be an internet media type (e.g., 'text/html',
445
+ # 'application/xml+atom', 'image/png') or a Symbol key into the
446
+ # Rack::File::MIME_TYPES table.
447
+ #
448
+ # Media type parameters, such as "charset", may also be specified using the
449
+ # optional hash argument:
450
+ #
451
+ # get '/foo.html' do
452
+ # content_type 'text/html', :charset => 'utf-8'
453
+ # "<h1>Hello World</h1>"
454
+ # end
455
+ #
456
+ def content_type(type, params={})
457
+ type = Rack::File::MIME_TYPES[type.to_s] if type.kind_of?(Symbol)
458
+ fail "Invalid or undefined media_type: #{type}" if type.nil?
459
+ if params.any?
460
+ params = params.collect { |kv| "%s=%s" % kv }.join(', ')
461
+ type = [ type, params ].join(";")
462
+ end
463
+ response.header['Content-Type'] = type
464
+ end
465
+
466
+ # Set the last modified time of the resource (HTTP 'Last-Modified' header)
467
+ # and halt if conditional GET matches. The +time+ argument is a Time,
468
+ # DateTime, or other object that responds to +to_time+.
469
+ #
470
+ # When the current request includes an 'If-Modified-Since' header that
471
+ # matches the time specified, execution is immediately halted with a
472
+ # '304 Not Modified' response.
473
+ #
474
+ # Calling this method before perfoming heavy processing (e.g., lengthy
475
+ # database queries, template rendering, complex logic) can dramatically
476
+ # increase overall throughput with caching clients.
477
+ def last_modified(time)
478
+ time = time.to_time if time.respond_to?(:to_time)
479
+ time = time.httpdate if time.respond_to?(:httpdate)
480
+ response.header['Last-Modified'] = time
481
+ throw :halt, 304 if time == request.env['HTTP_IF_MODIFIED_SINCE']
482
+ time
483
+ end
484
+
485
+ # Set the response entity tag (HTTP 'ETag' header) and halt if conditional
486
+ # GET matches. The +value+ argument is an identifier that uniquely
487
+ # identifies the current version of the resource. The +strength+ argument
488
+ # indicates whether the etag should be used as a :strong (default) or :weak
489
+ # cache validator.
490
+ #
491
+ # When the current request includes an 'If-None-Match' header with a
492
+ # matching etag, execution is immediately halted. If the request method is
493
+ # GET or HEAD, a '304 Not Modified' response is sent. For all other request
494
+ # methods, a '412 Precondition Failed' response is sent.
495
+ #
496
+ # Calling this method before perfoming heavy processing (e.g., lengthy
497
+ # database queries, template rendering, complex logic) can dramatically
498
+ # increase overall throughput with caching clients.
499
+ #
500
+ # ==== See Also
501
+ # {RFC2616: ETag}[http://w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19],
502
+ # ResponseHelpers#last_modified
503
+ def entity_tag(value, strength=:strong)
504
+ value =
505
+ case strength
506
+ when :strong then '"%s"' % value
507
+ when :weak then 'W/"%s"' % value
508
+ else raise TypeError, "strength must be one of :strong or :weak"
509
+ end
510
+ response.header['ETag'] = value
511
+
512
+ # Check for If-None-Match request header and halt if match is found.
513
+ etags = (request.env['HTTP_IF_NONE_MATCH'] || '').split(/\s*,\s*/)
514
+ if etags.include?(value) || etags.include?('*')
515
+ # GET/HEAD requests: send Not Modified response
516
+ throw :halt, 304 if request.get? || request.head?
517
+ # Other requests: send Precondition Failed response
518
+ throw :halt, 412
519
+ end
520
+ end
521
+
522
+ alias :etag :entity_tag
523
+
398
524
  end
399
-
525
+
400
526
  module RenderingHelpers
401
527
 
402
528
  def render(renderer, template, options={})
@@ -407,90 +533,104 @@ module Sinatra
407
533
  end
408
534
  result
409
535
  end
410
-
536
+
411
537
  def determine_layout(renderer, template, options)
412
538
  return if options[:layout] == false
413
539
  layout_from_options = options[:layout] || :layout
414
540
  resolve_template(renderer, layout_from_options, options, false)
415
541
  end
416
542
 
417
- private
418
-
419
- def resolve_template(renderer, template, options, scream = true)
420
- case template
421
- when String
422
- template
423
- when Proc
424
- template.call
425
- when Symbol
426
- if proc = templates[template]
427
- resolve_template(renderer, proc, options, scream)
428
- else
429
- read_template_file(renderer, template, options, scream)
430
- end
543
+ private
544
+
545
+ def resolve_template(renderer, template, options, scream = true)
546
+ case template
547
+ when String
548
+ template
549
+ when Proc
550
+ template.call
551
+ when Symbol
552
+ if proc = templates[template]
553
+ resolve_template(renderer, proc, options, scream)
431
554
  else
432
- nil
433
- end
434
- end
435
-
436
- def read_template_file(renderer, template, options, scream = true)
437
- path = File.join(
438
- options[:views_directory] || Sinatra.application.options.views,
439
- "#{template}.#{renderer}"
440
- )
441
- unless File.exists?(path)
442
- raise Errno::ENOENT.new(path) if scream
443
- nil
444
- else
445
- File.read(path)
555
+ read_template_file(renderer, template, options, scream)
446
556
  end
557
+ else
558
+ nil
447
559
  end
448
-
449
- def templates
450
- Sinatra.application.templates
560
+ end
561
+
562
+ def read_template_file(renderer, template, options, scream = true)
563
+ path = File.join(
564
+ options[:views_directory] || Sinatra.application.options.views,
565
+ "#{template}.#{renderer}"
566
+ )
567
+ unless File.exists?(path)
568
+ raise Errno::ENOENT.new(path) if scream
569
+ nil
570
+ else
571
+ File.read(path)
451
572
  end
452
-
573
+ end
574
+
575
+ def templates
576
+ Sinatra.application.templates
577
+ end
578
+
453
579
  end
454
580
 
455
581
  module Erb
456
-
582
+
457
583
  def erb(content, options={})
458
584
  require 'erb'
459
585
  render(:erb, content, options)
460
586
  end
461
-
462
- private
463
-
464
- def render_erb(content, options = {})
465
- ::ERB.new(content).result(binding)
587
+
588
+ private
589
+
590
+ def render_erb(content, options = {})
591
+ locals_opt = options.delete(:locals) || {}
592
+
593
+ locals_code = ""
594
+ locals_hash = {}
595
+ locals_opt.each do |key, value|
596
+ locals_code << "#{key} = locals_hash[:#{key}]\n"
597
+ locals_hash[:"#{key}"] = value
466
598
  end
467
-
599
+
600
+ body = ::ERB.new(content).src
601
+ eval("#{locals_code}#{body}", binding)
602
+ end
603
+
468
604
  end
469
605
 
470
606
  module Haml
471
-
607
+
472
608
  def haml(content, options={})
473
609
  require 'haml'
474
610
  render(:haml, content, options)
475
611
  end
476
-
477
- private
478
-
479
- def render_haml(content, options = {}, &b)
480
- ::Haml::Engine.new(content).render(options[:scope] || self, options[:locals] || {}, &b)
481
- end
482
-
612
+
613
+ private
614
+
615
+ def render_haml(content, options = {}, &b)
616
+ haml_options = (options[:options] || {}).
617
+ merge(Sinatra.options.haml || {})
618
+ ::Haml::Engine.new(content, haml_options).
619
+ render(options[:scope] || self, options[:locals] || {}, &b)
620
+ end
621
+
483
622
  end
484
623
 
485
624
  # Generate valid CSS using Sass (part of Haml)
486
625
  #
487
- # Sass templates can be in external files with <tt>.sass</tt> extension or can use Sinatra's
488
- # in_file_templates. In either case, the file can be rendered by passing the name of
489
- # the template to the +sass+ method as a symbol.
626
+ # Sass templates can be in external files with <tt>.sass</tt> extension
627
+ # or can use Sinatra's in_file_templates. In either case, the file can
628
+ # be rendered by passing the name of the template to the +sass+ method
629
+ # as a symbol.
490
630
  #
491
- # Unlike Haml, Sass does not support a layout file, so the +sass+ method will ignore both
492
- # the default <tt>layout.sass</tt> file and any parameters passed in as <tt>:layout</tt> in
493
- # the options hash.
631
+ # Unlike Haml, Sass does not support a layout file, so the +sass+ method
632
+ # will ignore both the default <tt>layout.sass</tt> file and any parameters
633
+ # passed in as <tt>:layout</tt> in the options hash.
494
634
  #
495
635
  # === Sass Template Files
496
636
  #
@@ -504,7 +644,7 @@ module Sinatra
504
644
  # end
505
645
  #
506
646
  # The "views/stylesheet.sass" file might contain the following:
507
- #
647
+ #
508
648
  # body
509
649
  # #admin
510
650
  # :background-color #CCC
@@ -520,51 +660,50 @@ module Sinatra
520
660
  # background-color: #CCC; }
521
661
  # body #main {
522
662
  # background-color: #000; }
523
- #
663
+ #
524
664
  # #form {
525
665
  # border-color: #AAA;
526
666
  # border-width: 10px; }
527
- #
667
+ #
528
668
  #
529
669
  # NOTE: Haml must be installed or a LoadError will be raised the first time an
530
670
  # attempt is made to render a Sass template.
531
671
  #
532
672
  # See http://haml.hamptoncatlin.com/docs/rdoc/classes/Sass.html for comprehensive documentation on Sass.
533
-
534
-
535
673
  module Sass
536
-
674
+
537
675
  def sass(content, options = {})
538
676
  require 'sass'
539
-
677
+
540
678
  # Sass doesn't support a layout, so we override any possible layout here
541
679
  options[:layout] = false
542
-
680
+
543
681
  render(:sass, content, options)
544
682
  end
545
-
546
- private
547
-
548
- def render_sass(content, options = {})
549
- ::Sass::Engine.new(content).render
550
- end
683
+
684
+ private
685
+
686
+ def render_sass(content, options = {})
687
+ ::Sass::Engine.new(content).render
688
+ end
689
+
551
690
  end
552
691
 
553
692
  # Generating conservative XML content using Builder templates.
554
693
  #
555
- # Builder templates can be inline by passing a block to the builder method, or in
556
- # external files with +.builder+ extension by passing the name of the template
557
- # to the +builder+ method as a Symbol.
694
+ # Builder templates can be inline by passing a block to the builder method,
695
+ # or in external files with +.builder+ extension by passing the name of the
696
+ # template to the +builder+ method as a Symbol.
558
697
  #
559
698
  # === Inline Rendering
560
699
  #
561
- # If the builder method is given a block, the block is called directly with an
562
- # +XmlMarkup+ instance and the result is returned as String:
700
+ # If the builder method is given a block, the block is called directly with
701
+ # an +XmlMarkup+ instance and the result is returned as String:
563
702
  # get '/who.xml' do
564
703
  # builder do |xml|
565
704
  # xml.instruct!
566
705
  # xml.person do
567
- # xml.name "Francis Albert Sinatra",
706
+ # xml.name "Francis Albert Sinatra",
568
707
  # :aka => "Frank Sinatra"
569
708
  # xml.email 'frank@capitolrecords.com'
570
709
  # end
@@ -580,9 +719,9 @@ module Sinatra
580
719
  #
581
720
  # === Builder Template Files
582
721
  #
583
- # Builder templates can be stored in separate files with a +.builder+
584
- # extension under the view path. An +XmlMarkup+ object named +xml+ is automatically
585
- # made available to template.
722
+ # Builder templates can be stored in separate files with a +.builder+
723
+ # extension under the view path. An +XmlMarkup+ object named +xml+ is
724
+ # automatically made available to template.
586
725
  #
587
726
  # Example:
588
727
  # get '/bio.xml' do
@@ -613,10 +752,11 @@ module Sinatra
613
752
  # <died age='82' />
614
753
  # </person>
615
754
  #
616
- # NOTE: Builder must be installed or a LoadError will be raised the first time an
617
- # attempt is made to render a builder template.
755
+ # NOTE: Builder must be installed or a LoadError will be raised the first
756
+ # time an attempt is made to render a builder template.
618
757
  #
619
- # See http://builder.rubyforge.org/ for comprehensive documentation on Builder.
758
+ # See http://builder.rubyforge.org/ for comprehensive documentation on
759
+ # Builder.
620
760
  module Builder
621
761
 
622
762
  def builder(content=nil, options={}, &block)
@@ -625,24 +765,24 @@ module Sinatra
625
765
  render(:builder, content, options)
626
766
  end
627
767
 
628
- private
768
+ private
629
769
 
630
- def render_builder(content, options = {}, &b)
631
- require 'builder'
632
- xml = ::Builder::XmlMarkup.new(:indent => 2)
633
- case content
634
- when String
635
- eval(content, binding, '<BUILDER>', 1)
636
- when Proc
637
- content.call(xml)
638
- end
639
- xml.target!
770
+ def render_builder(content, options = {}, &b)
771
+ require 'builder'
772
+ xml = ::Builder::XmlMarkup.new(:indent => 2)
773
+ case content
774
+ when String
775
+ eval(content, binding, '<BUILDER>', 1)
776
+ when Proc
777
+ content.call(xml)
640
778
  end
779
+ xml.target!
780
+ end
641
781
 
642
782
  end
643
783
 
644
784
  class EventContext
645
-
785
+ include Rack::Utils
646
786
  include ResponseHelpers
647
787
  include Streaming
648
788
  include RenderingHelpers
@@ -650,119 +790,280 @@ module Sinatra
650
790
  include Haml
651
791
  include Builder
652
792
  include Sass
653
-
793
+
654
794
  attr_accessor :request, :response
655
-
656
- dslify_writer :status, :body
657
-
795
+
796
+ attr_accessor :route_params
797
+
658
798
  def initialize(request, response, route_params)
799
+ @params = nil
800
+ @data = nil
659
801
  @request = request
660
802
  @response = response
661
803
  @route_params = route_params
662
804
  @response.body = nil
663
805
  end
664
-
806
+
807
+ def status(value=nil)
808
+ response.status = value if value
809
+ response.status
810
+ end
811
+
812
+ def body(value=nil)
813
+ response.body = value if value
814
+ response.body
815
+ end
816
+
665
817
  def params
666
- @params ||= begin
667
- h = Hash.new {|h,k| h[k.to_s] if Symbol === k}
668
- h.merge(@route_params.merge(@request.params))
669
- end
818
+ @params ||=
819
+ begin
820
+ hash = Hash.new {|h,k| h[k.to_s] if Symbol === k}
821
+ hash.merge! @request.params
822
+ hash.merge! @route_params
823
+ hash
824
+ end
670
825
  end
671
826
 
672
827
  def data
673
828
  @data ||= params.keys.first
674
829
  end
675
-
830
+
676
831
  def stop(*args)
677
832
  throw :halt, args
678
833
  end
679
-
834
+
680
835
  def complete(returned)
681
836
  @response.body || returned
682
837
  end
683
-
838
+
684
839
  def session
685
- @request.env['rack.session'] || {}
840
+ request.env['rack.session'] ||= {}
841
+ end
842
+
843
+ def reset!
844
+ @params = nil
845
+ @data = nil
686
846
  end
687
-
688
- private
689
847
 
690
- def method_missing(name, *args, &b)
848
+ private
849
+
850
+ def method_missing(name, *args, &b)
851
+ if @response.respond_to?(name)
691
852
  @response.send(name, *args, &b)
853
+ else
854
+ super
692
855
  end
693
-
856
+ end
857
+
694
858
  end
695
-
859
+
860
+
861
+ # The Application class represents the top-level working area of a
862
+ # Sinatra app. It provides the DSL for defining various aspects of the
863
+ # application and implements a Rack compatible interface for dispatching
864
+ # requests.
865
+ #
866
+ # Many of the instance methods defined in this class (#get, #post,
867
+ # #put, #delete, #layout, #before, #error, #not_found, etc.) are
868
+ # available at top-level scope. When invoked from top-level, the
869
+ # messages are forwarded to the "default application" (accessible
870
+ # at Sinatra::application).
696
871
  class Application
697
-
698
- attr_reader :events, :errors, :templates, :filters
699
- attr_reader :clearables, :reloading
700
-
701
- attr_writer :options
702
-
872
+
873
+ # Hash of event handlers with request method keys and
874
+ # arrays of potential handlers as values.
875
+ attr_reader :events
876
+
877
+ # Hash of error handlers with error status codes as keys and
878
+ # handlers as values.
879
+ attr_reader :errors
880
+
881
+ # Hash of template name mappings.
882
+ attr_reader :templates
883
+
884
+ # Hash of filters with event name keys (:before) and arrays of
885
+ # handlers as values.
886
+ attr_reader :filters
887
+
888
+ # Array of objects to clear during reload. The objects in this array
889
+ # must respond to :clear.
890
+ attr_reader :clearables
891
+
892
+ # Object including open attribute methods for modifying Application
893
+ # configuration.
894
+ attr_reader :options
895
+
896
+ # List of methods available from top-level scope. When invoked from
897
+ # top-level the method is forwarded to the default application
898
+ # (Sinatra::application).
899
+ FORWARD_METHODS = %w[
900
+ get put post delete head template layout before error not_found
901
+ configures configure set set_options set_option enable disable use
902
+ development? test? production?
903
+ ]
904
+
905
+ # Create a new Application with a default configuration taken
906
+ # from the default_options Hash.
907
+ #
908
+ # NOTE: A default Application is automatically created the first
909
+ # time any of Sinatra's DSL related methods is invoked so there
910
+ # is typically no need to create an instance explicitly. See
911
+ # Sinatra::application for more information.
912
+ def initialize
913
+ @reloading = false
914
+ @clearables = [
915
+ @events = Hash.new { |hash, key| hash[key] = [] },
916
+ @errors = Hash.new,
917
+ @filters = Hash.new { |hash, key| hash[key] = [] },
918
+ @templates = Hash.new,
919
+ @middleware = []
920
+ ]
921
+ @options = OpenStruct.new(self.class.default_options)
922
+ load_default_configuration!
923
+ end
924
+
925
+ # Hash of default application configuration options. When a new
926
+ # Application is created, the #options object takes its initial values
927
+ # from here.
928
+ #
929
+ # Changes to the default_options Hash effect only Application objects
930
+ # created after the changes are made. For this reason, modifications to
931
+ # the default_options Hash typically occur at the very beginning of a
932
+ # file, before any DSL related functions are invoked.
703
933
  def self.default_options
704
- @@default_options ||= {
934
+ return @default_options unless @default_options.nil?
935
+ root = File.expand_path(File.dirname($0))
936
+ @default_options = {
705
937
  :run => true,
706
938
  :port => 4567,
939
+ :host => '0.0.0.0',
707
940
  :env => :development,
708
- :root => Dir.pwd,
709
- :views => Dir.pwd + '/views',
710
- :public => Dir.pwd + '/public',
941
+ :root => root,
942
+ :views => root + '/views',
943
+ :public => root + '/public',
711
944
  :sessions => false,
712
945
  :logging => true,
946
+ :app_file => $0,
947
+ :raise_errors => false
713
948
  }
714
- end
715
-
716
- def default_options
717
- self.class.default_options
949
+ load_default_options_from_command_line!
950
+ @default_options
718
951
  end
719
952
 
720
-
721
- ##
722
- # Load all options given on the command line
953
+ # Search ARGV for command line arguments and update the
954
+ # Sinatra::default_options Hash accordingly. This method is
955
+ # invoked the first time the default_options Hash is accessed.
723
956
  # NOTE: Ignores --name so unit/spec tests can run individually
724
- def load_options!
957
+ def self.load_default_options_from_command_line! #:nodoc:
958
+ # fixes issue with: gem install --test sinatra
959
+ return if ARGV.empty? || File.basename($0) =~ /gem/
725
960
  require 'optparse'
726
961
  OptionParser.new do |op|
727
962
  op.on('-p port') { |port| default_options[:port] = port }
728
- op.on('-e env') { |env| default_options[:env] = env }
729
- op.on('-x') { |env| default_options[:mutex] = true }
963
+ op.on('-e env') { |env| default_options[:env] = env.to_sym }
964
+ op.on('-x') { default_options[:mutex] = true }
965
+ op.on('-s server') { |server| default_options[:server] = server }
730
966
  end.parse!(ARGV.dup.select { |o| o !~ /--name/ })
731
967
  end
732
968
 
733
- # Called immediately after the application is initialized or reloaded to
734
- # register default events. Events added here have dibs on requests since
735
- # they appear first in the list.
736
- def load_default_events!
737
- events[:get] << Static.new
969
+ # Determine whether the application is in the process of being
970
+ # reloaded.
971
+ def reloading?
972
+ @reloading == true
738
973
  end
739
974
 
740
- def initialize
741
- @clearables = [
742
- @events = Hash.new { |hash, key| hash[key] = [] },
743
- @errors = Hash.new,
744
- @filters = Hash.new { |hash, key| hash[key] = [] },
745
- @templates = Hash.new
746
- ]
747
- load_options!
748
- load_default_events!
975
+ # Yield to the block for configuration if the current environment
976
+ # matches any included in the +envs+ list. Always yield to the block
977
+ # when no environment is specified.
978
+ #
979
+ # NOTE: configuration blocks are not executed during reloads.
980
+ def configures(*envs, &b)
981
+ return if reloading?
982
+ yield self if envs.empty? || envs.include?(options.env)
983
+ end
984
+
985
+ alias :configure :configures
986
+
987
+ # When both +option+ and +value+ arguments are provided, set the option
988
+ # specified. With a single Hash argument, set all options specified in
989
+ # Hash. Options are available via the Application#options object.
990
+ #
991
+ # Setting individual options:
992
+ # set :port, 80
993
+ # set :env, :production
994
+ # set :views, '/path/to/views'
995
+ #
996
+ # Setting multiple options:
997
+ # set :port => 80,
998
+ # :env => :production,
999
+ # :views => '/path/to/views'
1000
+ #
1001
+ def set(option, value=self)
1002
+ if value == self && option.kind_of?(Hash)
1003
+ option.each { |key,val| set(key, val) }
1004
+ else
1005
+ options.send("#{option}=", value)
1006
+ end
749
1007
  end
750
1008
 
751
- def define_event(method, path, options = {}, &b)
752
- events[method] << event = Event.new(path, options, &b)
753
- event
1009
+ alias :set_option :set
1010
+ alias :set_options :set
1011
+
1012
+ # Enable the options specified by setting their values to true. For
1013
+ # example, to enable sessions and logging:
1014
+ # enable :sessions, :logging
1015
+ def enable(*opts)
1016
+ opts.each { |key| set(key, true) }
754
1017
  end
755
-
756
- def define_template(name=:layout, &b)
757
- templates[name] = b
1018
+
1019
+ # Disable the options specified by setting their values to false. For
1020
+ # example, to disable logging and automatic run:
1021
+ # disable :logging, :run
1022
+ def disable(*opts)
1023
+ opts.each { |key| set(key, false) }
1024
+ end
1025
+
1026
+ # Define an event handler for the given request method and path
1027
+ # spec. The block is executed when a request matches the method
1028
+ # and spec.
1029
+ #
1030
+ # NOTE: The #get, #post, #put, and #delete helper methods should
1031
+ # be used to define events when possible.
1032
+ def event(method, path, options = {}, &b)
1033
+ events[method].push(Event.new(path, options, &b)).last
1034
+ end
1035
+
1036
+ # Define an event handler for GET requests.
1037
+ def get(path, options={}, &b)
1038
+ event(:get, path, options, &b)
758
1039
  end
759
-
760
- def define_error(code, options = {}, &b)
761
- errors[code] = Error.new(code, &b)
1040
+
1041
+ # Define an event handler for POST requests.
1042
+ def post(path, options={}, &b)
1043
+ event(:post, path, options, &b)
762
1044
  end
763
-
764
- def define_filter(type, &b)
765
- filters[:before] << b
1045
+
1046
+ # Define an event handler for HEAD requests.
1047
+ def head(path, options={}, &b)
1048
+ event(:head, path, options, &b)
1049
+ end
1050
+
1051
+ # Define an event handler for PUT requests.
1052
+ #
1053
+ # NOTE: PUT events are triggered when the HTTP request method is
1054
+ # PUT and also when the request method is POST and the body includes a
1055
+ # "_method" parameter set to "PUT".
1056
+ def put(path, options={}, &b)
1057
+ event(:put, path, options, &b)
1058
+ end
1059
+
1060
+ # Define an event handler for DELETE requests.
1061
+ #
1062
+ # NOTE: DELETE events are triggered when the HTTP request method is
1063
+ # DELETE and also when the request method is POST and the body includes a
1064
+ # "_method" parameter set to "DELETE".
1065
+ def delete(path, options={}, &b)
1066
+ event(:delete, path, options, &b)
766
1067
  end
767
1068
 
768
1069
  # Visits and invokes each handler registered for the +request_method+ in
@@ -780,224 +1081,305 @@ module Sinatra
780
1081
  errors[NotFound].invoke(request)
781
1082
  end
782
1083
 
783
- def options
784
- @options ||= OpenStruct.new(default_options)
1084
+ # Define a named template. The template may be referenced from
1085
+ # event handlers by passing the name as a Symbol to rendering
1086
+ # methods. The block is executed each time the template is rendered
1087
+ # and the resulting object is passed to the template handler.
1088
+ #
1089
+ # The following example defines a HAML template named hello and
1090
+ # invokes it from an event handler:
1091
+ #
1092
+ # template :hello do
1093
+ # "h1 Hello World!"
1094
+ # end
1095
+ #
1096
+ # get '/' do
1097
+ # haml :hello
1098
+ # end
1099
+ #
1100
+ def template(name, &b)
1101
+ templates[name] = b
785
1102
  end
786
-
787
- def development?
788
- options.env == :development
1103
+
1104
+ # Define a layout template.
1105
+ def layout(name=:layout, &b)
1106
+ template(name, &b)
1107
+ end
1108
+
1109
+ # Define a custom error handler for the exception class +type+. The block
1110
+ # is invoked when the specified exception type is raised from an error
1111
+ # handler and can manipulate the response as needed:
1112
+ #
1113
+ # error MyCustomError do
1114
+ # status 500
1115
+ # 'So what happened was...' + request.env['sinatra.error'].message
1116
+ # end
1117
+ #
1118
+ # The Sinatra::ServerError handler is used by default when an exception
1119
+ # occurs and no matching error handler is found.
1120
+ def error(type=ServerError, options = {}, &b)
1121
+ errors[type] = Error.new(type, options, &b)
789
1122
  end
790
1123
 
1124
+ # Define a custom error handler for '404 Not Found' responses. This is a
1125
+ # shorthand for:
1126
+ # error NotFound do
1127
+ # ..
1128
+ # end
1129
+ def not_found(options={}, &b)
1130
+ error NotFound, options, &b
1131
+ end
1132
+
1133
+ # Define a request filter. When <tt>type</tt> is <tt>:before</tt>, execute the
1134
+ # block in the context of each request before matching event handlers.
1135
+ def filter(type, &b)
1136
+ filters[type] << b
1137
+ end
1138
+
1139
+ # Invoke the block in the context of each request before invoking
1140
+ # matching event handlers.
1141
+ def before(&b)
1142
+ filter :before, &b
1143
+ end
1144
+
1145
+ # True when environment is :development.
1146
+ def development? ; options.env == :development ; end
1147
+
1148
+ # True when environment is :test.
1149
+ def test? ; options.env == :test ; end
1150
+
1151
+ # True when environment is :production.
1152
+ def production? ; options.env == :production ; end
1153
+
1154
+ # Clear all events, templates, filters, and error handlers
1155
+ # and then reload the application source file. This occurs
1156
+ # automatically before each request is processed in development.
791
1157
  def reload!
792
- @reloading = true
793
1158
  clearables.each(&:clear)
794
- load_default_events!
795
- Kernel.load $0
1159
+ load_default_configuration!
1160
+ load_development_configuration! if development?
1161
+ @pipeline = nil
1162
+ @reloading = true
1163
+ Kernel.load options.app_file
796
1164
  @reloading = false
797
- Environment.setup!
798
1165
  end
799
-
1166
+
1167
+ # Determine whether the application is in the process of being
1168
+ # reloaded.
1169
+ def reloading?
1170
+ @reloading == true
1171
+ end
1172
+
1173
+ # Mutex instance used for thread synchronization.
800
1174
  def mutex
801
1175
  @@mutex ||= Mutex.new
802
1176
  end
803
-
1177
+
1178
+ # Yield to the block with thread synchronization
804
1179
  def run_safely
805
- if options.mutex
1180
+ if development? || options.mutex
806
1181
  mutex.synchronize { yield }
807
1182
  else
808
1183
  yield
809
1184
  end
810
1185
  end
811
-
1186
+
1187
+ # Add a piece of Rack middleware to the pipeline leading to the
1188
+ # application.
1189
+ def use(klass, *args, &block)
1190
+ fail "#{klass} must respond to 'new'" unless klass.respond_to?(:new)
1191
+ @pipeline = nil
1192
+ @middleware.push([ klass, args, block ]).last
1193
+ end
1194
+
1195
+ private
1196
+
1197
+ # Rack middleware derived from current state of application options.
1198
+ # These components are plumbed in at the very beginning of the
1199
+ # pipeline.
1200
+ def optional_middleware
1201
+ [
1202
+ ([ Rack::CommonLogger, [], nil ] if options.logging),
1203
+ ([ Rack::Session::Cookie, [], nil ] if options.sessions)
1204
+ ].compact
1205
+ end
1206
+
1207
+ # Rack middleware explicitly added to the application with #use. These
1208
+ # components are plumbed into the pipeline downstream from
1209
+ # #optional_middle.
1210
+ def explicit_middleware
1211
+ @middleware
1212
+ end
1213
+
1214
+ # All Rack middleware used to construct the pipeline.
1215
+ def middleware
1216
+ optional_middleware + explicit_middleware
1217
+ end
1218
+
1219
+ public
1220
+
1221
+ # An assembled pipeline of Rack middleware that leads eventually to
1222
+ # the Application#invoke method. The pipeline is built upon first
1223
+ # access. Defining new middleware with Application#use or manipulating
1224
+ # application options may cause the pipeline to be rebuilt.
1225
+ def pipeline
1226
+ @pipeline ||=
1227
+ middleware.inject(method(:dispatch)) do |app,(klass,args,block)|
1228
+ klass.new(app, *args, &block)
1229
+ end
1230
+ end
1231
+
1232
+ # Rack compatible request invocation interface.
812
1233
  def call(env)
813
- reload! if development?
1234
+ run_safely do
1235
+ reload! if development? && (options.reload != false)
1236
+ pipeline.call(env)
1237
+ end
1238
+ end
1239
+
1240
+ # Request invocation handler - called at the end of the Rack pipeline
1241
+ # for each request.
1242
+ #
1243
+ # 1. Create Rack::Request, Rack::Response helper objects.
1244
+ # 2. Lookup event handler based on request method and path.
1245
+ # 3. Create new EventContext to house event handler evaluation.
1246
+ # 4. Invoke each #before filter in context of EventContext object.
1247
+ # 5. Invoke event handler in context of EventContext object.
1248
+ # 6. Return response to Rack.
1249
+ #
1250
+ # See the Rack specification for detailed information on the
1251
+ # +env+ argument and return value.
1252
+ def dispatch(env)
814
1253
  request = Rack::Request.new(env)
815
- result = lookup(request)
816
- context = EventContext.new(
817
- request,
818
- Rack::Response.new,
819
- result.params
820
- )
821
- context.status(result.status)
1254
+ context = EventContext.new(request, Rack::Response.new([], 200), {})
822
1255
  begin
823
- returned = run_safely do
1256
+ returned =
824
1257
  catch(:halt) do
825
1258
  filters[:before].each { |f| context.instance_eval(&f) }
1259
+ result = lookup(context.request)
1260
+ context.route_params = result.params
1261
+ context.response.status = result.status
1262
+ context.reset!
826
1263
  [:complete, context.instance_eval(&result.block)]
827
1264
  end
828
- end
829
1265
  body = returned.to_result(context)
830
1266
  rescue => e
831
1267
  request.env['sinatra.error'] = e
832
1268
  context.status(500)
1269
+ raise if options.raise_errors && e.class != NotFound
833
1270
  result = (errors[e.class] || errors[ServerError]).invoke(request)
834
- returned = run_safely do
1271
+ returned =
835
1272
  catch(:halt) do
836
1273
  [:complete, context.instance_eval(&result.block)]
837
1274
  end
838
- end
839
1275
  body = returned.to_result(context)
840
1276
  end
841
1277
  body = '' unless body.respond_to?(:each)
842
- body = '' if request.request_method.upcase == 'HEAD'
1278
+ body = '' if request.env["REQUEST_METHOD"].upcase == 'HEAD'
843
1279
  context.body = body.kind_of?(String) ? [*body] : body
844
1280
  context.finish
845
1281
  end
846
-
847
- end
848
-
849
-
850
- module Environment
851
- extend self
852
-
853
- def setup!
1282
+
1283
+ private
1284
+
1285
+ # Called immediately after the application is initialized or reloaded to
1286
+ # register default events, templates, and error handlers.
1287
+ def load_default_configuration!
1288
+ events[:get] << Static.new
854
1289
  configure do
855
1290
  error do
856
- raise request.env['sinatra.error'] if Sinatra.options.raise_errors
857
1291
  '<h1>Internal Server Error</h1>'
858
1292
  end
859
1293
  not_found { '<h1>Not Found</h1>'}
860
1294
  end
861
-
862
- configures :development do
1295
+ end
863
1296
 
864
- get '/sinatra_custom_images/:image.png' do
865
- File.read(File.dirname(__FILE__) + "/../images/#{params[:image]}.png")
866
- end
1297
+ # Called before reloading to perform development specific configuration.
1298
+ def load_development_configuration!
1299
+ get '/sinatra_custom_images/:image.png' do
1300
+ content_type :png
1301
+ File.read(File.dirname(__FILE__) + "/../images/#{params[:image]}.png")
1302
+ end
867
1303
 
868
- not_found do
869
- %Q(
870
- <style>
871
- body {
872
- text-align: center;
873
- color: #888;
874
- font-family: Arial;
875
- font-size: 22px;
876
- margin: 20px;
877
- }
878
- #content {
879
- margin: 0 auto;
880
- width: 500px;
881
- text-align: left;
882
- }
883
- </style>
884
- <html>
885
- <body>
886
- <h2>Sinatra doesn't know this diddy.</h2>
887
- <img src='/sinatra_custom_images/404.png'></img>
888
- <div id="content">
889
- Try this:
890
- <pre>#{request.request_method.downcase} "#{request.path_info}" do
891
- .. do something ..
892
- end<pre>
893
- </div>
894
- </body>
895
- </html>
896
- )
897
- end
1304
+ not_found do
1305
+ (<<-HTML).gsub(/^ {8}/, '')
1306
+ <!DOCTYPE html>
1307
+ <html>
1308
+ <head>
1309
+ <style type="text/css">
1310
+ body {text-align:center;color:#888;font-family:arial;font-size:22px;margin:20px;}
1311
+ #content {margin:0 auto;width:500px;text-align:left}
1312
+ </style>
1313
+ </head>
1314
+ <body>
1315
+ <h2>Sinatra doesn't know this diddy.</h2>
1316
+ <img src='/sinatra_custom_images/404.png'>
1317
+ <div id="content">
1318
+ Try this:
1319
+ <pre>#{request.request_method.downcase} "#{request.path_info}" do\n .. do something ..\nend<pre>
1320
+ </div>
1321
+ </body>
1322
+ </html>
1323
+ HTML
1324
+ end
898
1325
 
899
- error do
900
- @error = request.env['sinatra.error']
901
- %Q(
902
- <html>
903
- <body>
904
- <style type="text/css" media="screen">
905
- body {
906
- font-family: Verdana;
907
- color: #333;
908
- }
909
-
910
- #content {
911
- width: 700px;
912
- margin-left: 20px;
913
- }
914
-
915
- #content h1 {
916
- width: 99%;
917
- color: #1D6B8D;
918
- font-weight: bold;
919
- }
920
-
921
- #stacktrace {
922
- margin-top: -20px;
923
- }
924
-
925
- #stacktrace pre {
926
- font-size: 12px;
927
- border-left: 2px solid #ddd;
928
- padding-left: 10px;
929
- }
930
-
931
- #stacktrace img {
932
- margin-top: 10px;
933
- }
934
- </style>
935
- <div id="content">
936
- <img src="/sinatra_custom_images/500.png" />
937
- <div class="info">
938
- Params: <pre>#{params.inspect}
939
- </div>
940
- <div id="stacktrace">
941
- <h1>#{Rack::Utils.escape_html(@error.class.name + ' - ' + @error.message)}</h1>
942
- <pre><code>#{Rack::Utils.escape_html(@error.backtrace.join("\n"))}</code></pre>
943
- </div>
944
- </body>
945
- </html>
946
- )
947
- end
1326
+ error do
1327
+ @error = request.env['sinatra.error']
1328
+ (<<-HTML).gsub(/^ {8}/, '')
1329
+ <!DOCTYPE html>
1330
+ <html>
1331
+ <head>
1332
+ <style type="text/css" media="screen">
1333
+ body {font-family:verdana;color:#333}
1334
+ #content {width:700px;margin-left:20px}
1335
+ #content h1 {width:99%;color:#1D6B8D;font-weight:bold}
1336
+ #stacktrace {margin-top:-20px}
1337
+ #stacktrace pre {font-size:12px;border-left:2px solid #ddd;padding-left:10px}
1338
+ #stacktrace img {margin-top:10px}
1339
+ </style>
1340
+ </head>
1341
+ <body>
1342
+ <div id="content">
1343
+ <img src="/sinatra_custom_images/500.png">
1344
+ <div class="info">
1345
+ Params: <pre>#{params.inspect}</pre>
1346
+ </div>
1347
+ <div id="stacktrace">
1348
+ <h1>#{escape_html(@error.class.name + ' - ' + @error.message.to_s)}</h1>
1349
+ <pre><code>#{escape_html(@error.backtrace.join("\n"))}</code></pre>
1350
+ </div>
1351
+ </div>
1352
+ </body>
1353
+ </html>
1354
+ HTML
948
1355
  end
949
1356
  end
950
- end
951
-
952
- end
953
-
954
- def get(path, options ={}, &b)
955
- Sinatra.application.define_event(:get, path, options, &b)
956
- end
957
-
958
- def post(path, options ={}, &b)
959
- Sinatra.application.define_event(:post, path, options, &b)
960
- end
961
1357
 
962
- def put(path, options ={}, &b)
963
- Sinatra.application.define_event(:put, path, options, &b)
964
- end
1358
+ end
965
1359
 
966
- def delete(path, options ={}, &b)
967
- Sinatra.application.define_event(:delete, path, options, &b)
968
1360
  end
969
1361
 
970
- def before(&b)
971
- Sinatra.application.define_filter(:before, &b)
1362
+ # Delegate DSLish methods to the currently active Sinatra::Application
1363
+ # instance.
1364
+ Sinatra::Application::FORWARD_METHODS.each do |method|
1365
+ eval(<<-EOS, binding, '(__DSL__)', 1)
1366
+ def #{method}(*args, &b)
1367
+ Sinatra.application.#{method}(*args, &b)
1368
+ end
1369
+ EOS
972
1370
  end
973
1371
 
974
1372
  def helpers(&b)
975
1373
  Sinatra::EventContext.class_eval(&b)
976
1374
  end
977
1375
 
978
- def error(type = Sinatra::ServerError, options = {}, &b)
979
- Sinatra.application.define_error(type, options, &b)
980
- end
981
-
982
- def not_found(options = {}, &b)
983
- Sinatra.application.define_error(Sinatra::NotFound, options, &b)
984
- end
985
-
986
- def layout(name = :layout, &b)
987
- Sinatra.application.define_template(name, &b)
988
- end
989
-
990
- def template(name, &b)
991
- Sinatra.application.define_template(name, &b)
992
- end
993
-
994
1376
  def use_in_file_templates!
995
1377
  require 'stringio'
996
1378
  templates = IO.read(caller.first.split(':').first).split('__FILE__').last
997
1379
  data = StringIO.new(templates)
998
1380
  current_template = nil
999
1381
  data.each do |line|
1000
- if line =~ /^##\s?(.*)/
1382
+ if line =~ /^@@\s?(.*)/
1001
1383
  current_template = $1.to_sym
1002
1384
  Sinatra.application.templates[current_template] = ''
1003
1385
  elsif current_template
@@ -1006,22 +1388,6 @@ def use_in_file_templates!
1006
1388
  end
1007
1389
  end
1008
1390
 
1009
- def configures(*envs, &b)
1010
- yield if !Sinatra.application.reloading &&
1011
- (envs.include?(Sinatra.application.options.env) ||
1012
- envs.empty?)
1013
- end
1014
- alias :configure :configures
1015
-
1016
- def set_options(opts)
1017
- Sinatra::Application.default_options.merge!(opts)
1018
- Sinatra.application.options = nil
1019
- end
1020
-
1021
- def set_option(key, value)
1022
- set_options(key => value)
1023
- end
1024
-
1025
1391
  def mime(ext, type)
1026
1392
  Rack::File::MIME_TYPES[ext.to_s] = type
1027
1393
  end
@@ -1029,85 +1395,33 @@ end
1029
1395
  ### Misc Core Extensions
1030
1396
 
1031
1397
  module Kernel
1032
-
1033
1398
  def silence_warnings
1034
1399
  old_verbose, $VERBOSE = $VERBOSE, nil
1035
1400
  yield
1036
1401
  ensure
1037
1402
  $VERBOSE = old_verbose
1038
1403
  end
1039
-
1040
- end
1041
-
1042
- class NilClass
1043
-
1044
- # Returns nil. Necessary for mappings where
1045
- # params are optional, such as: '/:blah?'
1046
- # nil.from_param # => nil
1047
- def from_param
1048
- nil
1049
- end
1050
-
1051
- end
1052
-
1053
- class String
1054
-
1055
- # Converts +self+ to an escaped URI parameter value
1056
- # 'Foo Bar'.to_param # => 'Foo%20Bar'
1057
- def to_param
1058
- URI.escape(self)
1059
- end
1060
-
1061
- # Converts +self+ from an escaped URI parameter value
1062
- # 'Foo%20Bar'.from_param # => 'Foo Bar'
1063
- def from_param
1064
- URI.unescape(self)
1065
- end
1066
-
1067
- end
1068
-
1069
- class Hash
1070
-
1071
- def to_params
1072
- map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
1073
- end
1074
-
1075
- def symbolize_keys
1076
- self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
1077
- end
1078
-
1079
- def pass(*keys)
1080
- reject { |k,v| !keys.include?(k) }
1081
- end
1082
-
1083
1404
  end
1084
1405
 
1085
1406
  class Symbol
1086
-
1087
- def to_proc
1407
+ def to_proc
1088
1408
  Proc.new { |*args| args.shift.__send__(self, *args) }
1089
1409
  end
1090
-
1091
1410
  end
1092
1411
 
1093
1412
  class Array
1094
-
1095
1413
  def to_hash
1096
1414
  self.inject({}) { |h, (k, v)| h[k] = v; h }
1097
1415
  end
1098
-
1099
1416
  def to_proc
1100
1417
  Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
1101
1418
  end
1102
-
1103
1419
  end
1104
1420
 
1105
1421
  module Enumerable
1106
-
1107
1422
  def eject(&block)
1108
1423
  find { |e| result = block[e] and break result }
1109
1424
  end
1110
-
1111
1425
  end
1112
1426
 
1113
1427
  ### Core Extension results for throw :halt
@@ -1154,9 +1468,10 @@ end
1154
1468
  at_exit do
1155
1469
  raise $! if $!
1156
1470
  if Sinatra.application.options.run
1157
- Sinatra.run
1471
+ Sinatra.run
1158
1472
  end
1159
1473
  end
1160
1474
 
1161
- mime :xml, 'application/xml'
1475
+ mime :xml, 'application/xml'
1162
1476
  mime :js, 'application/javascript'
1477
+ mime :png, 'image/png'