sinatra-sinatra 0.8.9

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