sinatra 0.3.3 → 0.9.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 (82) hide show
  1. data/AUTHORS +40 -0
  2. data/CHANGES +189 -0
  3. data/README.rdoc +146 -117
  4. data/Rakefile +33 -10
  5. data/{test → compat}/app_test.rb +11 -10
  6. data/{test → compat}/application_test.rb +10 -5
  7. data/compat/builder_test.rb +101 -0
  8. data/{test → compat}/custom_error_test.rb +0 -0
  9. data/compat/erb_test.rb +136 -0
  10. data/{test → compat}/events_test.rb +16 -3
  11. data/compat/filter_test.rb +30 -0
  12. data/compat/haml_test.rb +233 -0
  13. data/compat/helper.rb +30 -0
  14. data/compat/mapped_error_test.rb +72 -0
  15. data/{test → compat}/pipeline_test.rb +9 -4
  16. data/{test → compat}/public/foo.xml +0 -0
  17. data/compat/sass_test.rb +57 -0
  18. data/{test → compat}/sessions_test.rb +0 -0
  19. data/{test → compat}/streaming_test.rb +4 -1
  20. data/{test → compat}/sym_params_test.rb +0 -0
  21. data/{test → compat}/template_test.rb +0 -0
  22. data/{test → compat}/use_in_file_templates_test.rb +0 -0
  23. data/{test → compat}/views/foo.builder +0 -0
  24. data/{test → compat}/views/foo.erb +0 -0
  25. data/{test → compat}/views/foo.haml +0 -0
  26. data/{test → compat}/views/foo.sass +0 -0
  27. data/{test → compat}/views/foo_layout.erb +0 -0
  28. data/{test → compat}/views/foo_layout.haml +0 -0
  29. data/{test → compat}/views/layout_test/foo.builder +0 -0
  30. data/{test → compat}/views/layout_test/foo.erb +0 -0
  31. data/{test → compat}/views/layout_test/foo.haml +0 -0
  32. data/{test → compat}/views/layout_test/foo.sass +0 -0
  33. data/{test → compat}/views/layout_test/layout.builder +0 -0
  34. data/{test → compat}/views/layout_test/layout.erb +0 -0
  35. data/{test → compat}/views/layout_test/layout.haml +0 -0
  36. data/{test → compat}/views/layout_test/layout.sass +0 -0
  37. data/{test → compat}/views/no_layout/no_layout.builder +0 -0
  38. data/{test → compat}/views/no_layout/no_layout.haml +0 -0
  39. data/lib/sinatra.rb +6 -1484
  40. data/lib/sinatra/base.rb +838 -0
  41. data/lib/sinatra/compat.rb +239 -0
  42. data/{images → lib/sinatra/images}/404.png +0 -0
  43. data/{images → lib/sinatra/images}/500.png +0 -0
  44. data/lib/sinatra/main.rb +48 -0
  45. data/lib/sinatra/test.rb +114 -0
  46. data/lib/sinatra/test/bacon.rb +17 -0
  47. data/lib/sinatra/test/rspec.rb +7 -8
  48. data/lib/sinatra/test/spec.rb +3 -4
  49. data/lib/sinatra/test/unit.rb +3 -5
  50. data/sinatra.gemspec +68 -35
  51. data/test/base_test.rb +68 -0
  52. data/test/builder_test.rb +50 -87
  53. data/test/data/reload_app_file.rb +3 -0
  54. data/test/erb_test.rb +38 -124
  55. data/test/filter_test.rb +27 -22
  56. data/test/haml_test.rb +51 -216
  57. data/test/helper.rb +22 -6
  58. data/test/helpers_test.rb +361 -0
  59. data/test/mapped_error_test.rb +137 -49
  60. data/test/middleware_test.rb +58 -0
  61. data/test/options_test.rb +97 -0
  62. data/test/reload_test.rb +61 -0
  63. data/test/request_test.rb +18 -0
  64. data/test/result_test.rb +88 -0
  65. data/test/routing_test.rb +391 -0
  66. data/test/sass_test.rb +27 -48
  67. data/test/sinatra_test.rb +13 -0
  68. data/test/static_test.rb +57 -0
  69. data/test/templates_test.rb +88 -0
  70. data/test/views/hello.builder +1 -0
  71. data/test/views/hello.erb +1 -0
  72. data/test/views/hello.haml +1 -0
  73. data/test/views/hello.sass +2 -0
  74. data/test/views/hello.test +1 -0
  75. data/test/views/layout2.builder +3 -0
  76. data/test/views/layout2.erb +2 -0
  77. data/test/views/layout2.haml +2 -0
  78. data/test/views/layout2.test +1 -0
  79. metadata +80 -48
  80. data/ChangeLog +0 -96
  81. data/lib/sinatra/test/methods.rb +0 -76
  82. data/test/event_context_test.rb +0 -15
@@ -0,0 +1,838 @@
1
+ require 'time'
2
+ require 'uri'
3
+ require 'rack'
4
+ require 'rack/builder'
5
+
6
+ module Sinatra
7
+ VERSION = '0.9.0'
8
+
9
+ class Request < Rack::Request
10
+ def user_agent
11
+ @env['HTTP_USER_AGENT']
12
+ end
13
+
14
+ def accept
15
+ @env['HTTP_ACCEPT'].split(',').map { |a| a.strip }
16
+ end
17
+
18
+ # Override Rack 0.9.x's #params implementation (see #72 in lighthouse)
19
+ def params
20
+ self.GET.update(self.POST)
21
+ rescue EOFError => boom
22
+ self.GET
23
+ end
24
+ end
25
+
26
+ class Response < Rack::Response
27
+ def initialize
28
+ @status, @body = 200, []
29
+ @header = Rack::Utils::HeaderHash.new({'Content-Type' => 'text/html'})
30
+ end
31
+
32
+ def write(str)
33
+ @body << str.to_s
34
+ str
35
+ end
36
+
37
+ def finish
38
+ @body = block if block_given?
39
+ if [204, 304].include?(status.to_i)
40
+ header.delete "Content-Type"
41
+ [status.to_i, header.to_hash, []]
42
+ else
43
+ body = @body || []
44
+ body = [body] if body.respond_to? :to_str
45
+ if header["Content-Length"].nil? && body.respond_to?(:to_ary)
46
+ header["Content-Length"] = body.to_ary.
47
+ inject(0) { |len, part| len + part.length }.to_s
48
+ end
49
+ [status.to_i, header.to_hash, body]
50
+ end
51
+ end
52
+ end
53
+
54
+ class NotFound < NameError # :)
55
+ def code ; 404 ; end
56
+ end
57
+
58
+ module Helpers
59
+ # Set or retrieve the response status code.
60
+ def status(value=nil)
61
+ response.status = value if value
62
+ response.status
63
+ end
64
+
65
+ # Set or retrieve the response body. When a block is given,
66
+ # evaluation is deferred until the body is read with #each.
67
+ def body(value=nil, &block)
68
+ if block_given?
69
+ def block.each ; yield call ; end
70
+ response.body = block
71
+ else
72
+ response.body = value
73
+ end
74
+ end
75
+
76
+ # Halt processing and redirect to the URI provided.
77
+ def redirect(uri, *args)
78
+ status 302
79
+ response['Location'] = uri
80
+ halt(*args)
81
+ end
82
+
83
+ # Halt processing and return the error status provided.
84
+ def error(code, body=nil)
85
+ code, body = 500, code.to_str if code.respond_to? :to_str
86
+ response.body = body unless body.nil?
87
+ halt code
88
+ end
89
+
90
+ # Halt processing and return a 404 Not Found.
91
+ def not_found(body=nil)
92
+ error 404, body
93
+ end
94
+
95
+ # Access the underlying Rack session.
96
+ def session
97
+ env['rack.session'] ||= {}
98
+ end
99
+
100
+ # Look up a media type by file extension in Rack's mime registry.
101
+ def media_type(type)
102
+ Base.media_type(type)
103
+ end
104
+
105
+ # Set the Content-Type of the response body given a media type or file
106
+ # extension.
107
+ def content_type(type, params={})
108
+ media_type = self.media_type(type)
109
+ fail "Unknown media type: %p" % type if media_type.nil?
110
+ if params.any?
111
+ params = params.collect { |kv| "%s=%s" % kv }.join(', ')
112
+ response['Content-Type'] = [media_type, params].join(";")
113
+ else
114
+ response['Content-Type'] = media_type
115
+ end
116
+ end
117
+
118
+ # Set the Content-Disposition to "attachment" with the specified filename,
119
+ # instructing the user agents to prompt to save.
120
+ def attachment(filename=nil)
121
+ response['Content-Disposition'] = 'attachment'
122
+ if filename
123
+ params = '; filename="%s"' % File.basename(filename)
124
+ response['Content-Disposition'] << params
125
+ end
126
+ end
127
+
128
+ # Use the contents of the file as the response body and attempt to
129
+ def send_file(path, opts={})
130
+ stat = File.stat(path)
131
+ last_modified stat.mtime
132
+ content_type media_type(opts[:type]) ||
133
+ media_type(File.extname(path)) ||
134
+ response['Content-Type'] ||
135
+ 'application/octet-stream'
136
+ response['Content-Length'] ||= (opts[:length] || stat.size).to_s
137
+ halt StaticFile.open(path, 'rb')
138
+ rescue Errno::ENOENT
139
+ not_found
140
+ end
141
+
142
+ class StaticFile < ::File #:nodoc:
143
+ alias_method :to_path, :path
144
+ def each
145
+ while buf = read(8192)
146
+ yield buf
147
+ end
148
+ end
149
+ end
150
+
151
+ # Set the last modified time of the resource (HTTP 'Last-Modified' header)
152
+ # and halt if conditional GET matches. The +time+ argument is a Time,
153
+ # DateTime, or other object that responds to +to_time+.
154
+ #
155
+ # When the current request includes an 'If-Modified-Since' header that
156
+ # matches the time specified, execution is immediately halted with a
157
+ # '304 Not Modified' response.
158
+ def last_modified(time)
159
+ time = time.to_time if time.respond_to?(:to_time)
160
+ time = time.httpdate if time.respond_to?(:httpdate)
161
+ response['Last-Modified'] = time
162
+ halt 304 if time == request.env['HTTP_IF_MODIFIED_SINCE']
163
+ time
164
+ end
165
+
166
+ # Set the response entity tag (HTTP 'ETag' header) and halt if conditional
167
+ # GET matches. The +value+ argument is an identifier that uniquely
168
+ # identifies the current version of the resource. The +strength+ argument
169
+ # indicates whether the etag should be used as a :strong (default) or :weak
170
+ # cache validator.
171
+ #
172
+ # When the current request includes an 'If-None-Match' header with a
173
+ # matching etag, execution is immediately halted. If the request method is
174
+ # GET or HEAD, a '304 Not Modified' response is sent.
175
+ def etag(value, kind=:strong)
176
+ raise TypeError, ":strong or :weak expected" if ![:strong,:weak].include?(kind)
177
+ value = '"%s"' % value
178
+ value = 'W/' + value if kind == :weak
179
+ response['ETag'] = value
180
+
181
+ # Conditional GET check
182
+ if etags = env['HTTP_IF_NONE_MATCH']
183
+ etags = etags.split(/\s*,\s*/)
184
+ halt 304 if etags.include?(value) || etags.include?('*')
185
+ end
186
+ end
187
+ end
188
+
189
+ module Templates
190
+ def render(engine, template, options={})
191
+ data = lookup_template(engine, template, options)
192
+ output = __send__("render_#{engine}", template, data, options)
193
+ layout, data = lookup_layout(engine, options)
194
+ if layout
195
+ __send__("render_#{engine}", layout, data, options) { output }
196
+ else
197
+ output
198
+ end
199
+ end
200
+
201
+ def lookup_template(engine, template, options={})
202
+ case template
203
+ when Symbol
204
+ if cached = self.class.templates[template]
205
+ lookup_template(engine, cached, options)
206
+ else
207
+ ::File.read(template_path(engine, template, options))
208
+ end
209
+ when Proc
210
+ template.call
211
+ when String
212
+ template
213
+ else
214
+ raise ArgumentError
215
+ end
216
+ end
217
+
218
+ def lookup_layout(engine, options)
219
+ return if options[:layout] == false
220
+ options.delete(:layout) if options[:layout] == true
221
+ template = options[:layout] || :layout
222
+ data = lookup_template(engine, template, options)
223
+ [template, data]
224
+ rescue Errno::ENOENT
225
+ nil
226
+ end
227
+
228
+ def template_path(engine, template, options={})
229
+ views_dir =
230
+ options[:views_directory] || self.options.views || "./views"
231
+ "#{views_dir}/#{template}.#{engine}"
232
+ end
233
+
234
+ def erb(template, options={})
235
+ require 'erb' unless defined? ::ERB
236
+ render :erb, template, options
237
+ end
238
+
239
+ def render_erb(template, data, options, &block)
240
+ data = data.call if data.kind_of? Proc
241
+ instance = ::ERB.new(data)
242
+ locals = options[:locals] || {}
243
+ locals_assigns = locals.to_a.collect { |k,v| "#{k} = locals[:#{k}]" }
244
+ src = "#{locals_assigns.join("\n")}\n#{instance.src}"
245
+ eval src, binding, '(__ERB__)', locals_assigns.length + 1
246
+ instance.result(binding)
247
+ end
248
+
249
+ def haml(template, options={})
250
+ require 'haml' unless defined? ::Haml
251
+ options[:options] ||= self.class.haml if self.class.respond_to? :haml
252
+ render :haml, template, options
253
+ end
254
+
255
+ def render_haml(template, data, options, &block)
256
+ engine = ::Haml::Engine.new(data, options[:options] || {})
257
+ engine.render(self, options[:locals] || {}, &block)
258
+ end
259
+
260
+ def sass(template, options={}, &block)
261
+ require 'sass' unless defined? ::Sass
262
+ options[:layout] = false
263
+ render :sass, template, options
264
+ end
265
+
266
+ def render_sass(template, data, options, &block)
267
+ engine = ::Sass::Engine.new(data, options[:sass] || {})
268
+ engine.render
269
+ end
270
+
271
+ def builder(template=nil, options={}, &block)
272
+ require 'builder' unless defined? ::Builder
273
+ options, template = template, nil if template.is_a?(Hash)
274
+ template = lambda { block } if template.nil?
275
+ render :builder, template, options
276
+ end
277
+
278
+ def render_builder(template, data, options, &block)
279
+ xml = ::Builder::XmlMarkup.new(:indent => 2)
280
+ if data.respond_to?(:to_str)
281
+ eval data.to_str, binding, '<BUILDER>', 1
282
+ elsif data.kind_of?(Proc)
283
+ data.call(xml)
284
+ end
285
+ xml.target!
286
+ end
287
+
288
+ end
289
+
290
+ class Base
291
+ include Rack::Utils
292
+ include Helpers
293
+ include Templates
294
+
295
+ attr_accessor :app
296
+
297
+ def initialize(app=nil)
298
+ @app = app
299
+ yield self if block_given?
300
+ end
301
+
302
+ def call(env)
303
+ dup.call!(env)
304
+ end
305
+
306
+ attr_accessor :env, :request, :response, :params
307
+
308
+ def call!(env)
309
+ @env = env
310
+ @request = Request.new(env)
311
+ @response = Response.new
312
+ @params = nil
313
+ error_detection { dispatch! }
314
+ @response.finish
315
+ end
316
+
317
+ def options
318
+ self.class
319
+ end
320
+
321
+ def halt(*response)
322
+ throw :halt, *response
323
+ end
324
+
325
+ def pass
326
+ throw :pass
327
+ end
328
+
329
+ private
330
+ def dispatch!
331
+ self.class.filters.each {|block| instance_eval(&block)}
332
+ if routes = self.class.routes[@request.request_method]
333
+ path = @request.path_info
334
+ original_params = nested_params(@request.params)
335
+
336
+ routes.each do |pattern, keys, conditions, method_name|
337
+ if pattern =~ path
338
+ values = $~.captures.map{|val| val && unescape(val) }
339
+ params =
340
+ if keys.any?
341
+ keys.zip(values).inject({}) do |hash,(k,v)|
342
+ if k == 'splat'
343
+ (hash[k] ||= []) << v
344
+ else
345
+ hash[k] = v
346
+ end
347
+ hash
348
+ end
349
+ elsif values.any?
350
+ {'captures' => values}
351
+ else
352
+ {}
353
+ end
354
+ @params = original_params.merge(params)
355
+
356
+ catch(:pass) {
357
+ conditions.each { |cond|
358
+ throw :pass if instance_eval(&cond) == false }
359
+ return invoke(method_name)
360
+ }
361
+ end
362
+ end
363
+ end
364
+ raise NotFound
365
+ end
366
+
367
+ def nested_params(params)
368
+ return indifferent_hash.merge(params) if !params.keys.join.include?('[')
369
+ params.inject indifferent_hash do |res, (key,val)|
370
+ if key =~ /\[.*\]/
371
+ splat = key.scan(/(^[^\[]+)|\[([^\]]+)\]/).flatten.compact
372
+ head, last = splat[0..-2], splat[-1]
373
+ head.inject(res){ |s,v| s[v] ||= indifferent_hash }[last] = val
374
+ end
375
+ res
376
+ end
377
+ end
378
+
379
+ def indifferent_hash
380
+ Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
381
+ end
382
+
383
+ def invoke(block)
384
+ res = catch(:halt) { instance_eval(&block) }
385
+ case
386
+ when res.respond_to?(:to_str)
387
+ @response.body = [res]
388
+ when res.respond_to?(:to_ary)
389
+ res = res.to_ary
390
+ if Fixnum === res.first
391
+ if res.length == 3
392
+ @response.status, headers, body = res
393
+ @response.body = body if body
394
+ headers.each { |k, v| @response.headers[k] = v } if headers
395
+ elsif res.length == 2
396
+ @response.status = res.first
397
+ @response.body = res.last
398
+ else
399
+ raise TypeError, "#{res.inspect} not supported"
400
+ end
401
+ else
402
+ @response.body = res
403
+ end
404
+ when res.respond_to?(:each)
405
+ @response.body = res
406
+ when (100...599) === res
407
+ @response.status = res
408
+ when res.nil?
409
+ @response.body = []
410
+ end
411
+ res
412
+ end
413
+
414
+ def error_detection
415
+ errmap = self.class.errors
416
+ yield
417
+ rescue NotFound => boom
418
+ @env['sinatra.error'] = boom
419
+ @response.status = 404
420
+ @response.body = ['<h1>Not Found</h1>']
421
+ handler = errmap[boom.class] || errmap[NotFound]
422
+ invoke handler unless handler.nil?
423
+ rescue ::Exception => boom
424
+ @env['sinatra.error'] = boom
425
+
426
+ if options.dump_errors?
427
+ msg = ["#{boom.class} - #{boom.message}:", *boom.backtrace].join("\n ")
428
+ @env['rack.errors'] << msg
429
+ end
430
+
431
+ raise boom if options.raise_errors?
432
+ @response.status = 500
433
+ invoke errmap[boom.class] || errmap[Exception]
434
+ ensure
435
+ if @response.status >= 400 && errmap.key?(response.status)
436
+ invoke errmap[response.status]
437
+ end
438
+ end
439
+
440
+ @routes = {}
441
+ @filters = []
442
+ @conditions = []
443
+ @templates = {}
444
+ @middleware = []
445
+ @callsite = nil
446
+ @errors = {}
447
+
448
+ class << self
449
+ attr_accessor :routes, :filters, :conditions, :templates,
450
+ :middleware, :errors
451
+
452
+ def set(option, value=self)
453
+ if value.kind_of?(Proc)
454
+ metadef(option, &value)
455
+ metadef("#{option}?") { !!__send__(option) }
456
+ metadef("#{option}=") { |val| set(option, Proc.new{val}) }
457
+ elsif value == self && option.respond_to?(:to_hash)
458
+ option.to_hash.each(&method(:set))
459
+ elsif respond_to?("#{option}=")
460
+ __send__ "#{option}=", value
461
+ else
462
+ set option, Proc.new{value}
463
+ end
464
+ self
465
+ end
466
+
467
+ def enable(*opts)
468
+ opts.each { |key| set(key, true) }
469
+ end
470
+
471
+ def disable(*opts)
472
+ opts.each { |key| set(key, false) }
473
+ end
474
+
475
+ def error(codes=Exception, &block)
476
+ if codes.respond_to? :each
477
+ codes.each { |err| error(err, &block) }
478
+ else
479
+ @errors[codes] = block
480
+ end
481
+ end
482
+
483
+ def not_found(&block)
484
+ error 404, &block
485
+ end
486
+
487
+ def template(name, &block)
488
+ templates[name] = block
489
+ end
490
+
491
+ def layout(name=:layout, &block)
492
+ template name, &block
493
+ end
494
+
495
+ def use_in_file_templates!
496
+ line = caller.detect do |s|
497
+ [
498
+ /lib\/sinatra.*\.rb/,
499
+ /\(.*\)/,
500
+ /rubygems\/custom_require\.rb/
501
+ ].all? { |x| s !~ x }
502
+ end
503
+ file = line.sub(/:\d+.*$/, '')
504
+ if data = ::IO.read(file).split('__END__')[1]
505
+ data.gsub!(/\r\n/, "\n")
506
+ template = nil
507
+ data.each_line do |line|
508
+ if line =~ /^@@\s*(.*)/
509
+ template = templates[$1.to_sym] = ''
510
+ elsif template
511
+ template << line
512
+ end
513
+ end
514
+ end
515
+ end
516
+
517
+ # Look up a media type by file extension in Rack's mime registry.
518
+ def media_type(type)
519
+ return type if type.nil? || type.to_s.include?('/')
520
+ type = ".#{type}" unless type.to_s[0] == ?.
521
+ Rack::Mime.mime_type(type, nil)
522
+ end
523
+
524
+ def before(&block)
525
+ @filters << block
526
+ end
527
+
528
+ def condition(&block)
529
+ @conditions << block
530
+ end
531
+
532
+ def host_name(pattern)
533
+ condition { pattern === request.host }
534
+ end
535
+
536
+ def user_agent(pattern)
537
+ condition {
538
+ if request.user_agent =~ pattern
539
+ @params[:agent] = $~[1..-1]
540
+ true
541
+ else
542
+ false
543
+ end
544
+ }
545
+ end
546
+
547
+ def accept_mime_types(types)
548
+ types = [types] unless types.kind_of? Array
549
+ types.map!{|t| media_type(t)}
550
+
551
+ condition {
552
+ matching_types = (request.accept & types)
553
+ unless matching_types.empty?
554
+ response.headers['Content-Type'] = matching_types.first
555
+ true
556
+ else
557
+ false
558
+ end
559
+ }
560
+ end
561
+
562
+ def get(path, opts={}, &block)
563
+ conditions = @conditions.dup
564
+ route('GET', path, opts, &block)
565
+
566
+ @conditions = conditions
567
+ head(path, opts) { invoke(block) ; [] }
568
+ end
569
+
570
+ def put(path, opts={}, &bk); route 'PUT', path, opts, &bk; end
571
+ def post(path, opts={}, &bk); route 'POST', path, opts, &bk; end
572
+ def delete(path, opts={}, &bk); route 'DELETE', path, opts, &bk; end
573
+ def head(path, opts={}, &bk); route 'HEAD', path, opts, &bk; end
574
+
575
+ private
576
+ def route(verb, path, opts={}, &block)
577
+ host_name opts[:host] if opts.key?(:host)
578
+ user_agent opts[:agent] if opts.key?(:agent)
579
+ accept_mime_types opts[:provides] if opts.key?(:provides)
580
+
581
+ pattern, keys = compile(path)
582
+ conditions, @conditions = @conditions, []
583
+
584
+ define_method "#{verb} #{path}", &block
585
+ unbound_method = instance_method("#{verb} #{path}")
586
+ block = lambda { unbound_method.bind(self).call }
587
+
588
+ (routes[verb] ||= []).
589
+ push([pattern, keys, conditions, block]).last
590
+ end
591
+
592
+ def compile(path)
593
+ keys = []
594
+ if path.respond_to? :to_str
595
+ pattern =
596
+ URI.encode(path).gsub(/((:\w+)|\*)/) do |match|
597
+ if match == "*"
598
+ keys << 'splat'
599
+ "(.*?)"
600
+ else
601
+ keys << $2[1..-1]
602
+ "([^/?&#\.]+)"
603
+ end
604
+ end
605
+ [/^#{pattern}$/, keys]
606
+ elsif path.respond_to? :=~
607
+ [path, keys]
608
+ else
609
+ raise TypeError, path
610
+ end
611
+ end
612
+
613
+ public
614
+ def development? ; environment == :development ; end
615
+ def test? ; environment == :test ; end
616
+ def production? ; environment == :production ; end
617
+
618
+ def configure(*envs, &block)
619
+ yield if envs.empty? || envs.include?(environment.to_sym)
620
+ end
621
+
622
+ def use(middleware, *args, &block)
623
+ reset_middleware
624
+ @middleware << [middleware, args, block]
625
+ end
626
+
627
+ def run!(options={})
628
+ set(options)
629
+ handler = Rack::Handler.get(server)
630
+ handler_name = handler.name.gsub(/.*::/, '')
631
+ puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " +
632
+ "on #{port} for #{environment} with backup from #{handler_name}"
633
+ handler.run self, :Host => host, :Port => port do |server|
634
+ trap(:INT) do
635
+ ## Use thins' hard #stop! if available, otherwise just #stop
636
+ server.respond_to?(:stop!) ? server.stop! : server.stop
637
+ puts "\n== Sinatra has ended his set (crowd applauds)"
638
+ end
639
+ end
640
+ rescue Errno::EADDRINUSE => e
641
+ puts "== Someone is already performing on port #{port}!"
642
+ end
643
+
644
+ def call(env)
645
+ construct_middleware if @callsite.nil?
646
+ @callsite.call(env)
647
+ end
648
+
649
+ private
650
+ def construct_middleware(builder=Rack::Builder.new)
651
+ builder.use Rack::Session::Cookie if sessions?
652
+ builder.use Rack::CommonLogger if logging?
653
+ builder.use Rack::MethodOverride if methodoverride?
654
+ @middleware.each { |c, args, bk| builder.use(c, *args, &bk) }
655
+ builder.run new
656
+ @callsite = builder.to_app
657
+ end
658
+
659
+ def reset_middleware
660
+ @callsite = nil
661
+ end
662
+
663
+ def inherited(subclass)
664
+ subclass.routes = dupe_routes
665
+ subclass.templates = templates.dup
666
+ subclass.conditions = []
667
+ subclass.filters = filters.dup
668
+ subclass.errors = errors.dup
669
+ subclass.middleware = middleware.dup
670
+ subclass.send :reset_middleware
671
+ super
672
+ end
673
+
674
+ def dupe_routes
675
+ routes.inject({}) do |hash,(request_method,routes)|
676
+ hash[request_method] = routes.dup
677
+ hash
678
+ end
679
+ end
680
+
681
+ def metadef(message, &block)
682
+ (class << self; self; end).
683
+ send :define_method, message, &block
684
+ end
685
+ end
686
+
687
+ set :raise_errors, true
688
+ set :dump_errors, false
689
+ set :sessions, false
690
+ set :logging, false
691
+ set :methodoverride, false
692
+ set :static, false
693
+ set :environment, (ENV['RACK_ENV'] || :development).to_sym
694
+
695
+ set :run, false
696
+ set :server, (defined?(Rack::Handler::Thin) ? "thin" : "mongrel")
697
+ set :host, '0.0.0.0'
698
+ set :port, 4567
699
+
700
+ set :app_file, nil
701
+ set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) }
702
+ set :views, Proc.new { root && File.join(root, 'views') }
703
+ set :public, Proc.new { root && File.join(root, 'public') }
704
+
705
+ # static files route
706
+ get(/.*[^\/]$/) do
707
+ pass unless options.static? && options.public?
708
+ path = options.public + unescape(request.path_info)
709
+ pass unless File.file?(path)
710
+ send_file path, :disposition => nil
711
+ end
712
+
713
+ error ::Exception do
714
+ response.status = 500
715
+ content_type 'text/html'
716
+ '<h1>Internal Server Error</h1>'
717
+ end
718
+
719
+ configure :development do
720
+ get '/__sinatra__/:image.png' do
721
+ filename = File.dirname(__FILE__) + "/images/#{params[:image]}.png"
722
+ content_type :png
723
+ send_file filename
724
+ end
725
+
726
+ error NotFound do
727
+ (<<-HTML).gsub(/^ {8}/, '')
728
+ <!DOCTYPE html>
729
+ <html>
730
+ <head>
731
+ <style type="text/css">
732
+ body { text-align:center;font-family:helvetica,arial;font-size:22px;
733
+ color:#888;margin:20px}
734
+ #c {margin:0 auto;width:500px;text-align:left}
735
+ </style>
736
+ </head>
737
+ <body>
738
+ <h2>Sinatra doesn't know this ditty.</h2>
739
+ <img src='/__sinatra__/404.png'>
740
+ <div id="c">
741
+ Try this:
742
+ <pre>#{request.request_method.downcase} '#{request.path_info}' do\n "Hello World"\nend</pre>
743
+ </div>
744
+ </body>
745
+ </html>
746
+ HTML
747
+ end
748
+
749
+ error do
750
+ next unless err = request.env['sinatra.error']
751
+ heading = err.class.name + ' - ' + err.message.to_s
752
+ (<<-HTML).gsub(/^ {8}/, '')
753
+ <!DOCTYPE html>
754
+ <html>
755
+ <head>
756
+ <style type="text/css">
757
+ body {font-family:verdana;color:#333}
758
+ #c {margin-left:20px}
759
+ h1 {color:#1D6B8D;margin:0;margin-top:-30px}
760
+ h2 {color:#1D6B8D;font-size:18px}
761
+ pre {border-left:2px solid #ddd;padding-left:10px;color:#000}
762
+ img {margin-top:10px}
763
+ </style>
764
+ </head>
765
+ <body>
766
+ <div id="c">
767
+ <img src="/__sinatra__/500.png">
768
+ <h1>#{escape_html(heading)}</h1>
769
+ <pre class='trace'>#{escape_html(err.backtrace.join("\n"))}</pre>
770
+ <h2>Params</h2>
771
+ <pre>#{escape_html(params.inspect)}</pre>
772
+ </div>
773
+ </body>
774
+ </html>
775
+ HTML
776
+ end
777
+ end
778
+ end
779
+
780
+ class Default < Base
781
+ set :raise_errors, false
782
+ set :dump_errors, true
783
+ set :sessions, false
784
+ set :logging, true
785
+ set :methodoverride, true
786
+ set :static, true
787
+ set :run, false
788
+ set :reload, Proc.new { app_file? && development? }
789
+
790
+ def self.reloading?
791
+ @reloading ||= false
792
+ end
793
+
794
+ def self.configure(*envs)
795
+ super unless reloading?
796
+ end
797
+
798
+ def self.call(env)
799
+ reload! if reload?
800
+ super
801
+ end
802
+
803
+ def self.reload!
804
+ @reloading = true
805
+ superclass.send :inherited, self
806
+ $LOADED_FEATURES.delete("sinatra.rb")
807
+ ::Kernel.load app_file
808
+ @reloading = false
809
+ end
810
+
811
+ end
812
+
813
+ class Application < Default
814
+ end
815
+
816
+ module Delegator
817
+ METHODS = %w[
818
+ get put post delete head template layout before error not_found
819
+ configures configure set set_option set_options enable disable use
820
+ development? test? production? use_in_file_templates!
821
+ ]
822
+
823
+ METHODS.each do |method_name|
824
+ eval <<-RUBY, binding, '(__DELEGATE__)', 1
825
+ def #{method_name}(*args, &b)
826
+ ::Sinatra::Application.#{method_name}(*args, &b)
827
+ end
828
+ private :#{method_name}
829
+ RUBY
830
+ end
831
+ end
832
+
833
+ def self.new(base=Base, options={}, &block)
834
+ base = Class.new(base)
835
+ base.send :class_eval, &block if block_given?
836
+ base
837
+ end
838
+ end