sinatra 1.0 → 1.1.a

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 (57) hide show
  1. data/CHANGES +108 -1
  2. data/LICENSE +1 -1
  3. data/README.de.rdoc +1024 -0
  4. data/README.es.rdoc +1047 -0
  5. data/README.fr.rdoc +1038 -0
  6. data/README.hu.rdoc +607 -0
  7. data/README.jp.rdoc +473 -15
  8. data/README.rdoc +429 -41
  9. data/Rakefile +17 -6
  10. data/lib/sinatra/base.rb +357 -158
  11. data/lib/sinatra/showexceptions.rb +9 -1
  12. data/sinatra.gemspec +52 -9
  13. data/test/builder_test.rb +25 -1
  14. data/test/coffee_test.rb +88 -0
  15. data/test/encoding_test.rb +18 -0
  16. data/test/filter_test.rb +61 -2
  17. data/test/hello.mab +1 -0
  18. data/test/helper.rb +1 -0
  19. data/test/helpers_test.rb +141 -37
  20. data/test/less_test.rb +26 -2
  21. data/test/liquid_test.rb +58 -0
  22. data/test/markaby_test.rb +58 -0
  23. data/test/markdown_test.rb +35 -0
  24. data/test/nokogiri_test.rb +69 -0
  25. data/test/radius_test.rb +59 -0
  26. data/test/rdoc_test.rb +34 -0
  27. data/test/request_test.rb +12 -0
  28. data/test/routing_test.rb +35 -1
  29. data/test/sass_test.rb +46 -16
  30. data/test/scss_test.rb +88 -0
  31. data/test/settings_test.rb +32 -0
  32. data/test/sinatra_test.rb +4 -0
  33. data/test/static_test.rb +64 -0
  34. data/test/templates_test.rb +55 -1
  35. data/test/textile_test.rb +34 -0
  36. data/test/views/ascii.haml +2 -0
  37. data/test/views/explicitly_nested.str +1 -0
  38. data/test/views/hello.coffee +1 -0
  39. data/test/views/hello.liquid +1 -0
  40. data/test/views/hello.mab +1 -0
  41. data/test/views/hello.md +1 -0
  42. data/test/views/hello.nokogiri +1 -0
  43. data/test/views/hello.radius +1 -0
  44. data/test/views/hello.rdoc +1 -0
  45. data/test/views/hello.sass +1 -1
  46. data/test/views/hello.scss +3 -0
  47. data/test/views/hello.str +1 -0
  48. data/test/views/hello.textile +1 -0
  49. data/test/views/layout2.liquid +2 -0
  50. data/test/views/layout2.mab +2 -0
  51. data/test/views/layout2.nokogiri +3 -0
  52. data/test/views/layout2.radius +2 -0
  53. data/test/views/layout2.str +2 -0
  54. data/test/views/nested.str +1 -0
  55. data/test/views/utf8.haml +2 -0
  56. metadata +240 -33
  57. data/lib/sinatra/tilt.rb +0 -746
data/Rakefile CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'rake/clean'
2
2
  require 'rake/testtask'
3
3
  require 'fileutils'
4
+ require 'date'
4
5
 
5
6
  task :default => :test
6
7
  task :spec => :test
@@ -12,9 +13,19 @@ end
12
13
 
13
14
  # SPECS ===============================================================
14
15
 
15
- Rake::TestTask.new(:test) do |t|
16
- t.test_files = FileList['test/*_test.rb']
17
- t.ruby_opts = ['-rubygems -I.'] if defined? Gem
16
+ if !ENV['NO_TEST_FIX'] and RUBY_VERSION == '1.9.2' and RUBY_PATCHLEVEL == 0
17
+ # Avoids seg fault
18
+ task(:test) do
19
+ second_run = %w[settings rdoc markaby].map { |l| "test/#{l}_test.rb" }
20
+ first_run = Dir.glob('test/*_test.rb') - second_run
21
+ [first_run, second_run].each { |f| sh "testrb #{f.join ' '}" }
22
+ end
23
+ else
24
+ Rake::TestTask.new(:test) do |t|
25
+ t.test_files = FileList['test/*_test.rb']
26
+ t.ruby_opts = ['-rubygems'] if defined? Gem
27
+ t.ruby_opts << '-I.'
28
+ end
18
29
  end
19
30
 
20
31
  # Rcov ================================================================
@@ -22,7 +33,7 @@ namespace :test do
22
33
  desc 'Mesures test coverage'
23
34
  task :coverage do
24
35
  rm_f "coverage"
25
- rcov = "rcov --text-summary --test-unit-only -Ilib"
36
+ rcov = "rcov --text-summary -Ilib"
26
37
  system("#{rcov} --no-html --no-color test/*_test.rb")
27
38
  end
28
39
  end
@@ -36,11 +47,11 @@ task 'doc' => ['doc:api']
36
47
 
37
48
  task 'doc:api' => ['doc/api/index.html']
38
49
 
39
- file 'doc/api/index.html' => FileList['lib/**/*.rb','README.rdoc'] do |f|
50
+ file 'doc/api/index.html' => FileList['lib/**/*.rb', 'README.*'] do |f|
40
51
  require 'rbconfig'
41
52
  hanna = RbConfig::CONFIG['ruby_install_name'].sub('ruby', 'hanna')
42
53
  rb_files = f.prerequisites
43
- sh((<<-end).gsub(/\s+/, ' '))
54
+ sh(<<-end.gsub(/\s+/, ' '))
44
55
  #{hanna}
45
56
  --charset utf8
46
57
  --fmt html
@@ -4,48 +4,29 @@ require 'uri'
4
4
  require 'rack'
5
5
  require 'rack/builder'
6
6
  require 'sinatra/showexceptions'
7
-
8
- # require tilt if available; fall back on bundled version.
9
- begin
10
- require 'tilt'
11
- if Tilt::VERSION < '0.8'
12
- warn "WARN: sinatra requires tilt >= 0.8; you have #{Tilt::VERSION}. " +
13
- "loading bundled version..."
14
- Object.send :remove_const, :Tilt
15
- raise LoadError
16
- end
17
- rescue LoadError
18
- require 'sinatra/tilt'
19
- end
7
+ require 'tilt'
20
8
 
21
9
  module Sinatra
22
- VERSION = '1.0'
10
+ VERSION = '1.1.a'
23
11
 
24
12
  # The request object. See Rack::Request for more info:
25
13
  # http://rack.rubyforge.org/doc/classes/Rack/Request.html
26
14
  class Request < Rack::Request
27
15
  # Returns an array of acceptable media types for the response
28
16
  def accept
29
- @env['HTTP_ACCEPT'].to_s.split(',').map { |a| a.strip }
17
+ @env['HTTP_ACCEPT'].to_s.split(',').map { |a| a.split(';')[0].strip }
30
18
  end
31
19
 
32
- def secure?
33
- (@env['HTTP_X_FORWARDED_PROTO'] || @env['rack.url_scheme']) == 'https'
34
- end
35
-
36
- # Override Rack < 1.1's Request#params implementation (see lh #72 for
37
- # more info) and add a Request#user_agent method.
38
- # XXX remove when we require rack > 1.1
39
- if Rack.release < '1.1'
40
- def params
41
- self.GET.update(self.POST)
42
- rescue EOFError, Errno::ESPIPE
43
- self.GET
44
- end
45
-
46
- def user_agent
47
- @env['HTTP_USER_AGENT']
20
+ if Rack.release <= "1.2"
21
+ # Whether or not the web server (or a reverse proxy in front of it) is
22
+ # using SSL to communicate with the client.
23
+ def secure?
24
+ @env['HTTPS'] == 'on' or
25
+ @env['HTTP_X_FORWARDED_PROTO'] == 'https' or
26
+ @env['rack.url_scheme'] == 'https'
48
27
  end
28
+ else
29
+ alias secure? ssl?
49
30
  end
50
31
  end
51
32
 
@@ -87,15 +68,30 @@ module Sinatra
87
68
  # evaluation is deferred until the body is read with #each.
88
69
  def body(value=nil, &block)
89
70
  if block_given?
90
- def block.each ; yield call ; end
71
+ def block.each; yield(call) end
91
72
  response.body = block
92
- else
73
+ elsif value
93
74
  response.body = value
75
+ else
76
+ response.body
94
77
  end
95
78
  end
96
79
 
97
80
  # Halt processing and redirect to the URI provided.
98
81
  def redirect(uri, *args)
82
+ if not uri =~ /^https?:\/\//
83
+ # According to RFC 2616 section 14.30, "the field value consists of a
84
+ # single absolute URI"
85
+ abs_uri = "#{request.scheme}://#{request.host}"
86
+
87
+ if request.scheme == 'https' && request.port != 443 ||
88
+ request.scheme == 'http' && request.port != 80
89
+ abs_uri << ":#{request.port}"
90
+ end
91
+
92
+ uri = (abs_uri << uri)
93
+ end
94
+
99
95
  status 302
100
96
  response['Location'] = uri
101
97
  halt(*args)
@@ -121,7 +117,7 @@ module Sinatra
121
117
 
122
118
  # Access the underlying Rack session.
123
119
  def session
124
- env['rack.session'] ||= {}
120
+ request.session
125
121
  end
126
122
 
127
123
  # Look up a media type by file extension in Rack's mime registry.
@@ -132,8 +128,9 @@ module Sinatra
132
128
  # Set the Content-Type of the response body given a media type or file
133
129
  # extension.
134
130
  def content_type(type, params={})
135
- mime_type = self.mime_type(type)
131
+ mime_type = mime_type(type)
136
132
  fail "Unknown media type: %p" % type if mime_type.nil?
133
+ params[:charset] ||= defined?(Encoding) ? Encoding.default_external.to_s.downcase : 'utf-8'
137
134
  if params.any?
138
135
  params = params.collect { |kv| "%s=%s" % kv }.join(', ')
139
136
  response['Content-Type'] = [mime_type, params].join(";")
@@ -158,19 +155,30 @@ module Sinatra
158
155
  last_modified stat.mtime
159
156
 
160
157
  content_type mime_type(opts[:type]) ||
158
+ opts[:type] ||
161
159
  mime_type(File.extname(path)) ||
162
160
  response['Content-Type'] ||
163
161
  'application/octet-stream'
164
162
 
165
- response['Content-Length'] ||= (opts[:length] || stat.size).to_s
166
-
167
163
  if opts[:disposition] == 'attachment' || opts[:filename]
168
164
  attachment opts[:filename] || path
169
165
  elsif opts[:disposition] == 'inline'
170
166
  response['Content-Disposition'] = 'inline'
171
167
  end
172
168
 
173
- halt StaticFile.open(path, 'rb')
169
+ file_length = opts[:length] || stat.size
170
+ sf = StaticFile.open(path, 'rb')
171
+ if ! sf.parse_ranges(env, file_length)
172
+ response['Content-Range'] = "bytes */#{file_length}"
173
+ halt 416
174
+ elsif r=sf.range
175
+ response['Content-Range'] = "bytes #{r.begin}-#{r.end}/#{file_length}"
176
+ response['Content-Length'] = (r.end - r.begin + 1).to_s
177
+ halt 206, sf
178
+ else
179
+ response['Content-Length'] ||= file_length.to_s
180
+ halt sf
181
+ end
174
182
  rescue Errno::ENOENT
175
183
  not_found
176
184
  end
@@ -179,10 +187,65 @@ module Sinatra
179
187
  # generated iteratively in 8K chunks.
180
188
  class StaticFile < ::File #:nodoc:
181
189
  alias_method :to_path, :path
190
+
191
+ attr_accessor :range # a Range or nil
192
+
193
+ # Checks for byte-ranges in the request and sets self.range appropriately.
194
+ # Returns false if the ranges are unsatisfiable and the request should return 416.
195
+ def parse_ranges(env, size)
196
+ #r = Rack::Utils::byte_ranges(env, size) # TODO: not available yet in released Rack
197
+ r = byte_ranges(env, size)
198
+ return false if r == [] # Unsatisfiable; report error
199
+ @range = r[0] if r && r.length == 1 # Ignore multiple-range requests for now
200
+ return true
201
+ end
202
+
203
+ # TODO: Copied from the new method Rack::Utils::byte_ranges; this method can be removed once
204
+ # a version of Rack with that method is released and Sinatra can depend on it.
205
+ def byte_ranges(env, size)
206
+ # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
207
+ http_range = env['HTTP_RANGE']
208
+ return nil unless http_range
209
+ ranges = []
210
+ http_range.split(/,\s*/).each do |range_spec|
211
+ matches = range_spec.match(/bytes=(\d*)-(\d*)/)
212
+ return nil unless matches
213
+ r0,r1 = matches[1], matches[2]
214
+ if r0.empty?
215
+ return nil if r1.empty?
216
+ # suffix-byte-range-spec, represents trailing suffix of file
217
+ r0 = [size - r1.to_i, 0].max
218
+ r1 = size - 1
219
+ else
220
+ r0 = r0.to_i
221
+ if r1.empty?
222
+ r1 = size - 1
223
+ else
224
+ r1 = r1.to_i
225
+ return nil if r1 < r0 # backwards range is syntactically invalid
226
+ r1 = size-1 if r1 >= size
227
+ end
228
+ end
229
+ ranges << (r0..r1) if r0 <= r1
230
+ end
231
+ ranges
232
+ end
233
+
234
+ CHUNK_SIZE = 8192
235
+
182
236
  def each
183
- rewind
184
- while buf = read(8192)
185
- yield buf
237
+ if @range
238
+ self.pos = @range.begin
239
+ length = @range.end - @range.begin + 1
240
+ while length > 0 && (buf = read([CHUNK_SIZE,length].min))
241
+ yield buf
242
+ length -= buf.length
243
+ end
244
+ else
245
+ rewind
246
+ while buf = read(CHUNK_SIZE)
247
+ yield buf
248
+ end
186
249
  end
187
250
  end
188
251
  end
@@ -242,16 +305,16 @@ module Sinatra
242
305
  # and halt if conditional GET matches. The +time+ argument is a Time,
243
306
  # DateTime, or other object that responds to +to_time+.
244
307
  #
245
- # When the current request includes an 'If-Modified-Since' header that
246
- # matches the time specified, execution is immediately halted with a
247
- # '304 Not Modified' response.
308
+ # When the current request includes an 'If-Modified-Since' header that is
309
+ # equal or later than the time specified, execution is immediately halted
310
+ # with a '304 Not Modified' response.
248
311
  def last_modified(time)
249
312
  return unless time
250
313
  time = time.to_time if time.respond_to?(:to_time)
251
- time = time.httpdate if time.respond_to?(:httpdate)
252
- response['Last-Modified'] = time
253
- halt 304 if time == request.env['HTTP_IF_MODIFIED_SINCE']
254
- time
314
+ time = Time.parse time.strftime('%FT%T%:z') if time.respond_to?(:strftime)
315
+ response['Last-Modified'] = time.respond_to?(:httpdate) ? time.httpdate : time.to_s
316
+ halt 304 if Time.httpdate(request.env['HTTP_IF_MODIFIED_SINCE']) >= time
317
+ rescue ArgumentError
255
318
  end
256
319
 
257
320
  # Set the response entity tag (HTTP 'ETag' header) and halt if conditional
@@ -295,15 +358,17 @@ module Sinatra
295
358
  # :locals A hash with local variables that should be available
296
359
  # in the template
297
360
  module Templates
361
+ module ContentTyped
362
+ attr_accessor :content_type
363
+ end
364
+
298
365
  include Tilt::CompileSite
299
366
 
300
367
  def erb(template, options={}, locals={})
301
- options[:outvar] = '@_out_buf'
302
368
  render :erb, template, options, locals
303
369
  end
304
370
 
305
371
  def erubis(template, options={}, locals={})
306
- options[:outvar] = '@_out_buf'
307
372
  render :erubis, template, options, locals
308
373
  end
309
374
 
@@ -312,35 +377,86 @@ module Sinatra
312
377
  end
313
378
 
314
379
  def sass(template, options={}, locals={})
315
- options[:layout] = false
380
+ options.merge! :layout => false, :default_content_type => :css
316
381
  render :sass, template, options, locals
317
382
  end
318
383
 
384
+ def scss(template, options={}, locals={})
385
+ options.merge! :layout => false, :default_content_type => :css
386
+ render :scss, template, options, locals
387
+ end
388
+
319
389
  def less(template, options={}, locals={})
320
- options[:layout] = false
390
+ options.merge! :layout => false, :default_content_type => :css
321
391
  render :less, template, options, locals
322
392
  end
323
393
 
324
394
  def builder(template=nil, options={}, locals={}, &block)
395
+ render_xml(:builder, template, options, locals, &block)
396
+ end
397
+
398
+ def liquid(template, options={}, locals={})
399
+ render :liquid, template, options, locals
400
+ end
401
+
402
+ def markdown(template, options={}, locals={})
403
+ render :markdown, template, options, locals
404
+ end
405
+
406
+ def textile(template, options={}, locals={})
407
+ render :textile, template, options, locals
408
+ end
409
+
410
+ def rdoc(template, options={}, locals={})
411
+ render :rdoc, template, options, locals
412
+ end
413
+
414
+ def radius(template, options={}, locals={})
415
+ render :radius, template, options, locals
416
+ end
417
+
418
+ def markaby(template, options={}, locals={})
419
+ render :mab, template, options, locals
420
+ end
421
+
422
+ def coffee(template, options={}, locals={})
423
+ options.merge! :layout => false, :default_content_type => :js
424
+ render :coffee, template, options, locals
425
+ end
426
+
427
+ def nokogiri(template=nil, options={}, locals={}, &block)
428
+ options[:layout] = false if Tilt::VERSION <= "1.1"
429
+ render_xml(:nokogiri, template, options, locals, &block)
430
+ end
431
+
432
+ private
433
+ # logic shared between builder and nokogiri
434
+ def render_xml(engine, template, options={}, locals={}, &block)
435
+ options[:default_content_type] = :xml
325
436
  options, template = template, nil if template.is_a?(Hash)
326
437
  template = Proc.new { block } if template.nil?
327
- render :builder, template, options, locals
438
+ render engine, template, options, locals
328
439
  end
329
440
 
330
- private
331
441
  def render(engine, data, options={}, locals={}, &block)
332
442
  # merge app-level options
333
443
  options = settings.send(engine).merge(options) if settings.respond_to?(engine)
444
+ options[:outvar] ||= '@_out_buf'
334
445
 
335
446
  # extract generic options
336
- locals = options.delete(:locals) || locals || {}
337
- views = options.delete(:views) || settings.views || "./views"
338
- layout = options.delete(:layout)
339
- layout = :layout if layout.nil? || layout == true
447
+ locals = options.delete(:locals) || locals || {}
448
+ views = options.delete(:views) || settings.views || "./views"
449
+ @default_layout = :layout if @default_layout.nil?
450
+ layout = options.delete(:layout)
451
+ layout = @default_layout if layout.nil? or layout == true
452
+ content_type = options.delete(:content_type) || options.delete(:default_content_type)
340
453
 
341
454
  # compile and render template
342
- template = compile_template(engine, data, options, views)
343
- output = template.render(self, locals, &block)
455
+ layout_was = @default_layout
456
+ @default_layout = false if layout
457
+ template = compile_template(engine, data, options, views)
458
+ output = template.render(self, locals, &block)
459
+ @default_layout = layout_was
344
460
 
345
461
  # render layout
346
462
  if layout
@@ -351,11 +467,12 @@ module Sinatra
351
467
  end
352
468
  end
353
469
 
470
+ output.extend(ContentTyped).content_type = content_type if content_type
354
471
  output
355
472
  end
356
473
 
357
474
  def compile_template(engine, data, options, views)
358
- @template_cache.fetch engine, data, options do
475
+ template_cache.fetch engine, data, options do
359
476
  template = Tilt[engine]
360
477
  raise "Template engine not found: #{engine}" if template.nil?
361
478
 
@@ -367,6 +484,11 @@ module Sinatra
367
484
  template.new(path, line.to_i, options) { body }
368
485
  else
369
486
  path = ::File.join(views, "#{data}.#{engine}")
487
+ Tilt.mappings.each do |ext, klass|
488
+ break if File.exists?(path)
489
+ next unless klass == template
490
+ path = ::File.join(views, "#{data}.#{ext}")
491
+ end
370
492
  template.new(path, 1, options)
371
493
  end
372
494
  when data.is_a?(Proc) || data.is_a?(String)
@@ -387,6 +509,7 @@ module Sinatra
387
509
  include Templates
388
510
 
389
511
  attr_accessor :app
512
+ attr_reader :template_cache
390
513
 
391
514
  def initialize(app=nil)
392
515
  @app = app
@@ -401,15 +524,24 @@ module Sinatra
401
524
 
402
525
  attr_accessor :env, :request, :response, :params
403
526
 
404
- def call!(env)
527
+ def call!(env) # :nodoc:
405
528
  @env = env
406
529
  @request = Request.new(env)
407
530
  @response = Response.new
408
531
  @params = indifferent_params(@request.params)
409
- @template_cache.clear if settings.reload_templates
532
+ template_cache.clear if settings.reload_templates
533
+ force_encoding(@params)
410
534
 
535
+ @response['Content-Type'] = nil
411
536
  invoke { dispatch! }
412
537
  invoke { error_block!(response.status) }
538
+ unless @response['Content-Type']
539
+ if body.respond_to?(:to_ary) and body.first.respond_to? :content_type
540
+ content_type body.first.content_type
541
+ else
542
+ content_type :html
543
+ end
544
+ end
413
545
 
414
546
  status, header, body = @response.finish
415
547
 
@@ -424,11 +556,20 @@ module Sinatra
424
556
  [status, header, body]
425
557
  end
426
558
 
559
+ # Access settings defined with Base.set.
560
+ def self.settings
561
+ self
562
+ end
563
+
427
564
  # Access settings defined with Base.set.
428
565
  def settings
429
- self.class
566
+ self.class.settings
430
567
  end
568
+
431
569
  alias_method :options, :settings
570
+ class << self
571
+ alias_method :options, :settings
572
+ end
432
573
 
433
574
  # Exit the current block, halts any further processing
434
575
  # of the request, and returns the specified response.
@@ -455,64 +596,28 @@ module Sinatra
455
596
  end
456
597
 
457
598
  private
458
- # Run before filters defined on the class and all superclasses.
459
- def before_filter!(base=self.class)
460
- before_filter!(base.superclass) if base.superclass.respond_to?(:before_filters)
461
- base.before_filters.each { |block| instance_eval(&block) }
462
- end
463
-
464
- # Run after filters defined on the class and all superclasses.
465
- def after_filter!(base=self.class)
466
- after_filter!(base.superclass) if base.superclass.respond_to?(:after_filters)
467
- base.after_filters.each { |block| instance_eval(&block) }
599
+ # Run filters defined on the class and all superclasses.
600
+ def filter!(type, base = self.class)
601
+ filter! type, base.superclass if base.superclass.respond_to?(:filters)
602
+ base.filters[type].each { |block| instance_eval(&block) }
468
603
  end
469
604
 
470
605
  # Run routes defined on the class and all superclasses.
471
606
  def route!(base=self.class, pass_block=nil)
472
607
  if routes = base.routes[@request.request_method]
473
- original_params = @params
474
- path = unescape(@request.path_info)
475
-
476
608
  routes.each do |pattern, keys, conditions, block|
477
- if match = pattern.match(path)
478
- values = match.captures.to_a
479
- params =
480
- if keys.any?
481
- keys.zip(values).inject({}) do |hash,(k,v)|
482
- if k == 'splat'
483
- (hash[k] ||= []) << v
484
- else
485
- hash[k] = v
486
- end
487
- hash
488
- end
489
- elsif values.any?
490
- {'captures' => values}
491
- else
492
- {}
493
- end
494
- @params = original_params.merge(params)
495
- @block_params = values
496
-
497
- pass_block = catch(:pass) do
498
- conditions.each { |cond|
499
- throw :pass if instance_eval(&cond) == false }
500
- route_eval(&block)
501
- end
609
+ pass_block = process_route(pattern, keys, conditions) do
610
+ route_eval(&block)
502
611
  end
503
612
  end
504
-
505
- @params = original_params
506
613
  end
507
614
 
508
615
  # Run routes defined in superclass.
509
616
  if base.superclass.respond_to?(:routes)
510
- route! base.superclass, pass_block
511
- return
617
+ return route!(base.superclass, pass_block)
512
618
  end
513
619
 
514
620
  route_eval(&pass_block) if pass_block
515
-
516
621
  route_missing
517
622
  end
518
623
 
@@ -521,6 +626,46 @@ module Sinatra
521
626
  throw :halt, instance_eval(&block)
522
627
  end
523
628
 
629
+ # If the current request matches pattern and conditions, fill params
630
+ # with keys and call the given block.
631
+ # Revert params afterwards.
632
+ #
633
+ # Returns pass block.
634
+ def process_route(pattern, keys, conditions)
635
+ @original_params ||= @params
636
+ @path ||= begin
637
+ path = unescape(@request.path_info)
638
+ path.empty? ? "/" : path
639
+ end
640
+ if match = pattern.match(@path)
641
+ values = match.captures.to_a
642
+ params =
643
+ if keys.any?
644
+ keys.zip(values).inject({}) do |hash,(k,v)|
645
+ if k == 'splat'
646
+ (hash[k] ||= []) << v
647
+ else
648
+ hash[k] = v
649
+ end
650
+ hash
651
+ end
652
+ elsif values.any?
653
+ {'captures' => values}
654
+ else
655
+ {}
656
+ end
657
+ @params = @original_params.merge(params)
658
+ @block_params = values
659
+ catch(:pass) do
660
+ conditions.each { |cond|
661
+ throw :pass if instance_eval(&cond) == false }
662
+ yield
663
+ end
664
+ end
665
+ ensure
666
+ @params = @original_params
667
+ end
668
+
524
669
  # No matching route was found or all routes passed. The default
525
670
  # implementation is to forward the request downstream when running
526
671
  # as middleware (@app is non-nil); when no downstream app is set, raise
@@ -557,6 +702,7 @@ module Sinatra
557
702
  end
558
703
  end
559
704
 
705
+ # Creates a Hash with indifferent access.
560
706
  def indifferent_hash
561
707
  Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
562
708
  end
@@ -587,7 +733,7 @@ module Sinatra
587
733
  end
588
734
  when res.respond_to?(:each)
589
735
  @response.body = res
590
- when (100...599) === res
736
+ when (100..599) === res
591
737
  @response.status = res
592
738
  end
593
739
 
@@ -597,16 +743,17 @@ module Sinatra
597
743
  # Dispatch a request with error handling.
598
744
  def dispatch!
599
745
  static! if settings.static? && (request.get? || request.head?)
600
- before_filter!
746
+ filter! :before
601
747
  route!
602
748
  rescue NotFound => boom
603
749
  handle_not_found!(boom)
604
750
  rescue ::Exception => boom
605
751
  handle_exception!(boom)
606
752
  ensure
607
- after_filter! unless env['sinatra.static_file']
753
+ filter! :after unless env['sinatra.static_file']
608
754
  end
609
755
 
756
+ # Special treatment for 404s in order to play nice with cascades.
610
757
  def handle_not_found!(boom)
611
758
  @env['sinatra.error'] = boom
612
759
  @response.status = 404
@@ -615,11 +762,12 @@ module Sinatra
615
762
  error_block! boom.class, NotFound
616
763
  end
617
764
 
765
+ # Error handling during requests.
618
766
  def handle_exception!(boom)
619
767
  @env['sinatra.error'] = boom
620
768
 
621
769
  dump_errors!(boom) if settings.dump_errors?
622
- raise boom if settings.show_exceptions?
770
+ raise boom if settings.show_exceptions? and settings.show_exceptions != :after_handler
623
771
 
624
772
  @response.status = 500
625
773
  if res = error_block!(boom.class)
@@ -644,6 +792,7 @@ module Sinatra
644
792
  end
645
793
  end
646
794
  end
795
+ raise boom if settings.show_exceptions? and keys == Exception
647
796
  nil
648
797
  end
649
798
 
@@ -654,13 +803,14 @@ module Sinatra
654
803
  end
655
804
 
656
805
  class << self
657
- attr_reader :routes, :before_filters, :after_filters, :templates, :errors
806
+ attr_reader :routes, :filters, :templates, :errors
658
807
 
808
+ # Removes all routes, filters, middleware and extension hooks from the
809
+ # current class (not routes/filters/... defined by its superclass).
659
810
  def reset!
660
811
  @conditions = []
661
812
  @routes = {}
662
- @before_filters = []
663
- @after_filters = []
813
+ @filters = {:before => [], :after => []}
664
814
  @errors = {}
665
815
  @middleware = []
666
816
  @prototype = nil
@@ -700,8 +850,8 @@ module Sinatra
700
850
  metadef(option, &value)
701
851
  metadef("#{option}?") { !!__send__(option) }
702
852
  metadef("#{option}=") { |val| metadef(option, &Proc.new{val}) }
703
- elsif value == self && option.respond_to?(:to_hash)
704
- option.to_hash.each { |k,v| set(k, v) }
853
+ elsif value == self && option.respond_to?(:each)
854
+ option.each { |k,v| set(k, v) }
705
855
  elsif respond_to?("#{option}=")
706
856
  __send__ "#{option}=", value
707
857
  else
@@ -746,7 +896,7 @@ module Sinatra
746
896
  # Load embeded templates from the file; uses the caller's __FILE__
747
897
  # when no file is specified.
748
898
  def inline_templates=(file=nil)
749
- file = (file.nil? || file == true) ? caller_files.first : file
899
+ file = (file.nil? || file == true) ? (caller_files.first || File.expand_path($0)) : file
750
900
 
751
901
  begin
752
902
  app, data =
@@ -760,7 +910,7 @@ module Sinatra
760
910
  template = nil
761
911
  data.each_line do |line|
762
912
  lines += 1
763
- if line =~ /^@@\s*(.*)/
913
+ if line =~ /^@@\s*(.*\S)\s*$/
764
914
  template = ''
765
915
  templates[$1.to_sym] = [template, file, lines]
766
916
  elsif template
@@ -781,15 +931,24 @@ module Sinatra
781
931
  # Define a before filter; runs before all requests within the same
782
932
  # context as route handlers and may access/modify the request and
783
933
  # response.
784
- def before(&block)
785
- @before_filters << block
934
+ def before(path = nil, &block)
935
+ add_filter(:before, path, &block)
786
936
  end
787
937
 
788
938
  # Define an after filter; runs after all requests within the same
789
939
  # context as route handlers and may access/modify the request and
790
940
  # response.
791
- def after(&block)
792
- @after_filters << block
941
+ def after(path = nil, &block)
942
+ add_filter(:after, path, &block)
943
+ end
944
+
945
+ # add a filter
946
+ def add_filter(type, path = nil, &block)
947
+ return filters[type] << block unless path
948
+ block, *arguments = compile!(type, path, block)
949
+ add_filter(type) do
950
+ process_route(*arguments) { instance_eval(&block) }
951
+ end
793
952
  end
794
953
 
795
954
  # Add a route condition. The route is considered non-matching when the
@@ -799,27 +958,30 @@ module Sinatra
799
958
  end
800
959
 
801
960
  private
961
+ # Condition for matching host name. Parameter might be String or Regexp.
802
962
  def host_name(pattern)
803
963
  condition { pattern === request.host }
804
964
  end
805
965
 
966
+ # Condition for matching user agent. Parameter should be Regexp.
967
+ # Will set params[:agent].
806
968
  def user_agent(pattern)
807
- condition {
969
+ condition do
808
970
  if request.user_agent =~ pattern
809
971
  @params[:agent] = $~[1..-1]
810
972
  true
811
973
  else
812
974
  false
813
975
  end
814
- }
976
+ end
815
977
  end
816
978
  alias_method :agent, :user_agent
817
979
 
980
+ # Condition for matching mimetypes. Accepts file extensions.
818
981
  def provides(*types)
819
- types = [types] unless types.kind_of? Array
820
- types.map!{|t| mime_type(t)}
982
+ types.map! { |t| mime_type(t) }
821
983
 
822
- condition {
984
+ condition do
823
985
  matching_types = (request.accept & types)
824
986
  unless matching_types.empty?
825
987
  response.headers['Content-Type'] = matching_types.first
@@ -827,7 +989,7 @@ module Sinatra
827
989
  else
828
990
  false
829
991
  end
830
- }
992
+ end
831
993
  end
832
994
 
833
995
  public
@@ -849,22 +1011,10 @@ module Sinatra
849
1011
  private
850
1012
  def route(verb, path, options={}, &block)
851
1013
  # Because of self.options.host
852
- host_name(options.delete(:bind)) if options.key?(:host)
853
-
854
- options.each {|option, args| send(option, *args)}
855
-
856
- pattern, keys = compile(path)
857
- conditions, @conditions = @conditions, []
858
-
859
- define_method "#{verb} #{path}", &block
860
- unbound_method = instance_method("#{verb} #{path}")
861
- block =
862
- if block.arity != 0
863
- proc { unbound_method.bind(self).call(*@block_params) }
864
- else
865
- proc { unbound_method.bind(self).call }
866
- end
1014
+ host_name(options.delete(:host)) if options.key?(:host)
1015
+ options.each { |option, args| send(option, *args) }
867
1016
 
1017
+ block, pattern, keys, conditions = compile! verb, path, block
868
1018
  invoke_hook(:route_added, verb, path, block)
869
1019
 
870
1020
  (@routes[verb] ||= []).
@@ -875,6 +1025,21 @@ module Sinatra
875
1025
  extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
876
1026
  end
877
1027
 
1028
+ def compile!(verb, path, block)
1029
+ method_name = "#{verb} #{path}"
1030
+
1031
+ define_method(method_name, &block)
1032
+ unbound_method = instance_method method_name
1033
+ pattern, keys = compile(path)
1034
+ conditions, @conditions = @conditions, []
1035
+ remove_method method_name
1036
+
1037
+ [ block.arity != 0 ?
1038
+ proc { unbound_method.bind(self).call(*@block_params) } :
1039
+ proc { unbound_method.bind(self).call },
1040
+ pattern, keys, conditions ]
1041
+ end
1042
+
878
1043
  def compile(path)
879
1044
  keys = []
880
1045
  if path.respond_to? :to_str
@@ -889,7 +1054,7 @@ module Sinatra
889
1054
  Regexp.escape(match)
890
1055
  else
891
1056
  keys << $2[1..-1]
892
- "([^/?&#]+)"
1057
+ "([^/?#]+)"
893
1058
  end
894
1059
  end
895
1060
  [/^#{pattern}$/, keys]
@@ -910,6 +1075,8 @@ module Sinatra
910
1075
  include(*extensions) if extensions.any?
911
1076
  end
912
1077
 
1078
+ # Register an extension. Alternatively take a block from which an
1079
+ # extension will be created and registered on the fly.
913
1080
  def register(*extensions, &block)
914
1081
  extensions << Module.new(&block) if block_given?
915
1082
  @extensions += extensions
@@ -935,6 +1102,12 @@ module Sinatra
935
1102
  @middleware << [middleware, args, block]
936
1103
  end
937
1104
 
1105
+ def quit!(server, handler_name)
1106
+ ## Use thins' hard #stop! if available, otherwise just #stop
1107
+ server.respond_to?(:stop!) ? server.stop! : server.stop
1108
+ puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i
1109
+ end
1110
+
938
1111
  # Run the Sinatra app as a self-hosted server using
939
1112
  # Thin, Mongrel or WEBrick (in that order)
940
1113
  def run!(options={})
@@ -944,11 +1117,7 @@ module Sinatra
944
1117
  puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " +
945
1118
  "on #{port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i
946
1119
  handler.run self, :Host => bind, :Port => port do |server|
947
- trap(:INT) do
948
- ## Use thins' hard #stop! if available, otherwise just #stop
949
- server.respond_to?(:stop!) ? server.stop! : server.stop
950
- puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i
951
- end
1120
+ [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } }
952
1121
  set :running, true
953
1122
  end
954
1123
  rescue Errno::EADDRINUSE => e
@@ -981,7 +1150,7 @@ module Sinatra
981
1150
 
982
1151
  private
983
1152
  def detect_rack_handler
984
- servers = Array(self.server)
1153
+ servers = Array(server)
985
1154
  servers.each do |server_name|
986
1155
  begin
987
1156
  return Rack::Handler.get(server_name.downcase)
@@ -1012,12 +1181,13 @@ module Sinatra
1012
1181
  end
1013
1182
 
1014
1183
  public
1015
- CALLERS_TO_IGNORE = [
1184
+ CALLERS_TO_IGNORE = [ # :nodoc:
1016
1185
  /\/sinatra(\/(base|main|showexceptions))?\.rb$/, # all sinatra code
1017
- /lib\/tilt.*\.rb$/, # all tilt code
1018
- /\(.*\)/, # generated code
1019
- /custom_require\.rb$/, # rubygems require hacks
1020
- /active_support/, # active_support require hacks
1186
+ /lib\/tilt.*\.rb$/, # all tilt code
1187
+ /\(.*\)/, # generated code
1188
+ /rubygems\/custom_require\.rb$/, # rubygems require hacks
1189
+ /active_support/, # active_support require hacks
1190
+ /<internal:/, # internal in ruby >= 1.9.2
1021
1191
  ]
1022
1192
 
1023
1193
  # add rubinius (and hopefully other VM impls) ignore patterns ...
@@ -1030,6 +1200,8 @@ module Sinatra
1030
1200
  map { |file,line| file }
1031
1201
  end
1032
1202
 
1203
+ # Like caller_files, but containing Arrays rather than strings with the
1204
+ # first element being the file, and the second being the line.
1033
1205
  def caller_locations
1034
1206
  caller(1).
1035
1207
  map { |line| line.split(/:(?=\d|in )/)[0,2] }.
@@ -1037,6 +1209,33 @@ module Sinatra
1037
1209
  end
1038
1210
  end
1039
1211
 
1212
+ # Fixes encoding issues by
1213
+ # * defaulting to UTF-8
1214
+ # * casting params to Encoding.default_external
1215
+ #
1216
+ # The latter might not be necessary if Rack handles it one day.
1217
+ # Keep an eye on Rack's LH #100.
1218
+ if defined? Encoding
1219
+ if Encoding.default_external.to_s =~ /^ASCII/
1220
+ Encoding.default_external = "UTF-8"
1221
+ end
1222
+ Encoding.default_internal ||= Encoding.default_external
1223
+
1224
+ def force_encoding(data)
1225
+ return if data == self || data.is_a?(Tempfile)
1226
+ if data.respond_to? :force_encoding
1227
+ data.force_encoding(Encoding.default_external)
1228
+ elsif data.respond_to? :each_value
1229
+ data.each_value { |v| force_encoding(v) }
1230
+ elsif data.respond_to? :each
1231
+ data.each { |v| force_encoding(v) }
1232
+ end
1233
+ end
1234
+ else
1235
+ def force_encoding(*) end
1236
+ end
1237
+
1238
+
1040
1239
  reset!
1041
1240
 
1042
1241
  set :environment, (ENV['RACK_ENV'] || :development).to_sym
@@ -1061,11 +1260,11 @@ module Sinatra
1061
1260
  set :app_file, nil
1062
1261
  set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) }
1063
1262
  set :views, Proc.new { root && File.join(root, 'views') }
1064
- set :reload_templates, Proc.new { development? }
1263
+ set :reload_templates, Proc.new { development? or RUBY_VERSION < '1.8.7' }
1065
1264
  set :lock, false
1066
1265
 
1067
1266
  set :public, Proc.new { root && File.join(root, 'public') }
1068
- set :static, Proc.new { self.public && File.exist?(self.public) }
1267
+ set :static, Proc.new { public && File.exist?(public) }
1069
1268
 
1070
1269
  error ::Exception do
1071
1270
  response.status = 500
@@ -1151,7 +1350,7 @@ module Sinatra
1151
1350
  # class scope.
1152
1351
  def self.new(base=Base, options={}, &block)
1153
1352
  base = Class.new(base)
1154
- base.send :class_eval, &block if block_given?
1353
+ base.class_eval(&block) if block_given?
1155
1354
  base
1156
1355
  end
1157
1356