Syd-sinatra 0.3.2 → 0.9.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. data/AUTHORS +40 -0
  2. data/CHANGES +189 -0
  3. data/README.rdoc +148 -119
  4. data/Rakefile +34 -10
  5. data/{test → compat}/app_test.rb +11 -10
  6. data/{test → compat}/application_test.rb +21 -5
  7. data/compat/builder_test.rb +101 -0
  8. data/compat/erb_test.rb +136 -0
  9. data/{test → compat}/events_test.rb +16 -3
  10. data/compat/filter_test.rb +30 -0
  11. data/compat/haml_test.rb +233 -0
  12. data/compat/helper.rb +30 -0
  13. data/compat/mapped_error_test.rb +72 -0
  14. data/{test → compat}/pipeline_test.rb +9 -4
  15. data/compat/sass_test.rb +57 -0
  16. data/{test → compat}/streaming_test.rb +4 -1
  17. data/lib/sinatra/base.rb +843 -0
  18. data/lib/sinatra/compat.rb +239 -0
  19. data/lib/sinatra/main.rb +48 -0
  20. data/lib/sinatra/test/bacon.rb +17 -0
  21. data/lib/sinatra/test/rspec.rb +7 -8
  22. data/lib/sinatra/test/spec.rb +3 -4
  23. data/lib/sinatra/test/unit.rb +3 -5
  24. data/lib/sinatra/test.rb +114 -0
  25. data/lib/sinatra.rb +6 -1468
  26. data/sinatra.gemspec +68 -35
  27. data/test/base_test.rb +68 -0
  28. data/test/builder_test.rb +50 -87
  29. data/test/data/reload_app_file.rb +3 -0
  30. data/test/erb_test.rb +38 -124
  31. data/test/filter_test.rb +65 -20
  32. data/test/haml_test.rb +51 -216
  33. data/test/helper.rb +23 -5
  34. data/test/helpers_test.rb +361 -0
  35. data/test/mapped_error_test.rb +137 -49
  36. data/test/middleware_test.rb +58 -0
  37. data/test/options_test.rb +97 -0
  38. data/test/reload_test.rb +61 -0
  39. data/test/request_test.rb +18 -0
  40. data/test/result_test.rb +88 -0
  41. data/test/routing_test.rb +391 -0
  42. data/test/sass_test.rb +27 -48
  43. data/test/sinatra_test.rb +13 -0
  44. data/test/static_test.rb +57 -0
  45. data/test/templates_test.rb +88 -0
  46. data/test/views/hello.builder +1 -0
  47. data/test/views/hello.erb +1 -0
  48. data/test/views/hello.haml +1 -0
  49. data/test/views/hello.sass +2 -0
  50. data/test/views/hello.test +1 -0
  51. data/test/views/layout2.builder +3 -0
  52. data/test/views/layout2.erb +2 -0
  53. data/test/views/layout2.haml +2 -0
  54. data/test/views/layout2.test +1 -0
  55. metadata +79 -47
  56. data/ChangeLog +0 -78
  57. data/lib/sinatra/test/methods.rb +0 -76
  58. data/test/event_context_test.rb +0 -15
  59. /data/{test → compat}/custom_error_test.rb +0 -0
  60. /data/{test → compat}/public/foo.xml +0 -0
  61. /data/{test → compat}/sessions_test.rb +0 -0
  62. /data/{test → compat}/sym_params_test.rb +0 -0
  63. /data/{test → compat}/template_test.rb +0 -0
  64. /data/{test → compat}/use_in_file_templates_test.rb +0 -0
  65. /data/{test → compat}/views/foo.builder +0 -0
  66. /data/{test → compat}/views/foo.erb +0 -0
  67. /data/{test → compat}/views/foo.haml +0 -0
  68. /data/{test → compat}/views/foo.sass +0 -0
  69. /data/{test → compat}/views/foo_layout.erb +0 -0
  70. /data/{test → compat}/views/foo_layout.haml +0 -0
  71. /data/{test → compat}/views/layout_test/foo.builder +0 -0
  72. /data/{test → compat}/views/layout_test/foo.erb +0 -0
  73. /data/{test → compat}/views/layout_test/foo.haml +0 -0
  74. /data/{test → compat}/views/layout_test/foo.sass +0 -0
  75. /data/{test → compat}/views/layout_test/layout.builder +0 -0
  76. /data/{test → compat}/views/layout_test/layout.erb +0 -0
  77. /data/{test → compat}/views/layout_test/layout.haml +0 -0
  78. /data/{test → compat}/views/layout_test/layout.sass +0 -0
  79. /data/{test → compat}/views/no_layout/no_layout.builder +0 -0
  80. /data/{test → compat}/views/no_layout/no_layout.haml +0 -0
  81. /data/{images → lib/sinatra/images}/404.png +0 -0
  82. /data/{images → lib/sinatra/images}/500.png +0 -0
@@ -0,0 +1,843 @@
1
+ require 'time'
2
+ require 'uri'
3
+ require 'rack'
4
+ require 'rack/builder'
5
+
6
+ module Sinatra
7
+ VERSION = '0.9.0.2'
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 do |block|
332
+ res = catch(:halt) { instance_eval(&block) ; :continue }
333
+ return unless res == :continue
334
+ end
335
+
336
+ if routes = self.class.routes[@request.request_method]
337
+ path = @request.path_info
338
+ original_params = nested_params(@request.params)
339
+
340
+ routes.each do |pattern, keys, conditions, method_name|
341
+ if pattern =~ path
342
+ values = $~.captures.map{|val| val && unescape(val) }
343
+ params =
344
+ if keys.any?
345
+ keys.zip(values).inject({}) do |hash,(k,v)|
346
+ if k == 'splat'
347
+ (hash[k] ||= []) << v
348
+ else
349
+ hash[k] = v
350
+ end
351
+ hash
352
+ end
353
+ elsif values.any?
354
+ {'captures' => values}
355
+ else
356
+ {}
357
+ end
358
+ @params = original_params.merge(params)
359
+
360
+ catch(:pass) {
361
+ conditions.each { |cond|
362
+ throw :pass if instance_eval(&cond) == false }
363
+ return invoke(method_name)
364
+ }
365
+ end
366
+ end
367
+ end
368
+ raise NotFound
369
+ end
370
+
371
+ def nested_params(params)
372
+ return indifferent_hash.merge(params) if !params.keys.join.include?('[')
373
+ params.inject indifferent_hash do |res, (key,val)|
374
+ if key =~ /\[.*\]/
375
+ splat = key.scan(/(^[^\[]+)|\[([^\]]+)\]/).flatten.compact
376
+ head, last = splat[0..-2], splat[-1]
377
+ head.inject(res){ |s,v| s[v] ||= indifferent_hash }[last] = val
378
+ end
379
+ res
380
+ end
381
+ end
382
+
383
+ def indifferent_hash
384
+ Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
385
+ end
386
+
387
+ def invoke(block)
388
+ res = catch(:halt) { instance_eval(&block) }
389
+ return if res.nil?
390
+
391
+ case
392
+ when res.respond_to?(:to_str)
393
+ @response.body = [res]
394
+ when res.respond_to?(:to_ary)
395
+ res = res.to_ary
396
+ if Fixnum === res.first
397
+ if res.length == 3
398
+ @response.status, headers, body = res
399
+ @response.body = body if body
400
+ headers.each { |k, v| @response.headers[k] = v } if headers
401
+ elsif res.length == 2
402
+ @response.status = res.first
403
+ @response.body = res.last
404
+ else
405
+ raise TypeError, "#{res.inspect} not supported"
406
+ end
407
+ else
408
+ @response.body = res
409
+ end
410
+ when res.respond_to?(:each)
411
+ @response.body = res
412
+ when (100...599) === res
413
+ @response.status = res
414
+ end
415
+
416
+ res
417
+ end
418
+
419
+ def error_detection
420
+ errmap = self.class.errors
421
+ yield
422
+ rescue NotFound => boom
423
+ @env['sinatra.error'] = boom
424
+ @response.status = 404
425
+ @response.body = ['<h1>Not Found</h1>']
426
+ handler = errmap[boom.class] || errmap[NotFound]
427
+ invoke handler unless handler.nil?
428
+ rescue ::Exception => boom
429
+ @env['sinatra.error'] = boom
430
+
431
+ if options.dump_errors?
432
+ msg = ["#{boom.class} - #{boom.message}:", *boom.backtrace].join("\n ")
433
+ @env['rack.errors'] << msg
434
+ end
435
+
436
+ raise boom if options.raise_errors?
437
+ @response.status = 500
438
+ invoke errmap[boom.class] || errmap[Exception]
439
+ ensure
440
+ if @response.status >= 400 && errmap.key?(response.status)
441
+ invoke errmap[response.status]
442
+ end
443
+ end
444
+
445
+ @routes = {}
446
+ @filters = []
447
+ @conditions = []
448
+ @templates = {}
449
+ @middleware = []
450
+ @callsite = nil
451
+ @errors = {}
452
+
453
+ class << self
454
+ attr_accessor :routes, :filters, :conditions, :templates,
455
+ :middleware, :errors
456
+
457
+ def set(option, value=self)
458
+ if value.kind_of?(Proc)
459
+ metadef(option, &value)
460
+ metadef("#{option}?") { !!__send__(option) }
461
+ metadef("#{option}=") { |val| set(option, Proc.new{val}) }
462
+ elsif value == self && option.respond_to?(:to_hash)
463
+ option.to_hash.each(&method(:set))
464
+ elsif respond_to?("#{option}=")
465
+ __send__ "#{option}=", value
466
+ else
467
+ set option, Proc.new{value}
468
+ end
469
+ self
470
+ end
471
+
472
+ def enable(*opts)
473
+ opts.each { |key| set(key, true) }
474
+ end
475
+
476
+ def disable(*opts)
477
+ opts.each { |key| set(key, false) }
478
+ end
479
+
480
+ def error(codes=Exception, &block)
481
+ if codes.respond_to? :each
482
+ codes.each { |err| error(err, &block) }
483
+ else
484
+ @errors[codes] = block
485
+ end
486
+ end
487
+
488
+ def not_found(&block)
489
+ error 404, &block
490
+ end
491
+
492
+ def template(name, &block)
493
+ templates[name] = block
494
+ end
495
+
496
+ def layout(name=:layout, &block)
497
+ template name, &block
498
+ end
499
+
500
+ def use_in_file_templates!
501
+ line = caller.detect do |s|
502
+ [
503
+ /lib\/sinatra.*\.rb/,
504
+ /\(.*\)/,
505
+ /rubygems\/custom_require\.rb/
506
+ ].all? { |x| s !~ x }
507
+ end
508
+ file = line.sub(/:\d+.*$/, '')
509
+ if data = ::IO.read(file).split('__END__')[1]
510
+ data.gsub!(/\r\n/, "\n")
511
+ template = nil
512
+ data.each_line do |line|
513
+ if line =~ /^@@\s*(.*)/
514
+ template = templates[$1.to_sym] = ''
515
+ elsif template
516
+ template << line
517
+ end
518
+ end
519
+ end
520
+ end
521
+
522
+ # Look up a media type by file extension in Rack's mime registry.
523
+ def media_type(type)
524
+ return type if type.nil? || type.to_s.include?('/')
525
+ type = ".#{type}" unless type.to_s[0] == ?.
526
+ Rack::Mime.mime_type(type, nil)
527
+ end
528
+
529
+ def before(&block)
530
+ @filters << block
531
+ end
532
+
533
+ def condition(&block)
534
+ @conditions << block
535
+ end
536
+
537
+ def host_name(pattern)
538
+ condition { pattern === request.host }
539
+ end
540
+
541
+ def user_agent(pattern)
542
+ condition {
543
+ if request.user_agent =~ pattern
544
+ @params[:agent] = $~[1..-1]
545
+ true
546
+ else
547
+ false
548
+ end
549
+ }
550
+ end
551
+
552
+ def accept_mime_types(types)
553
+ types = [types] unless types.kind_of? Array
554
+ types.map!{|t| media_type(t)}
555
+
556
+ condition {
557
+ matching_types = (request.accept & types)
558
+ unless matching_types.empty?
559
+ response.headers['Content-Type'] = matching_types.first
560
+ true
561
+ else
562
+ false
563
+ end
564
+ }
565
+ end
566
+
567
+ def get(path, opts={}, &block)
568
+ conditions = @conditions.dup
569
+ route('GET', path, opts, &block)
570
+
571
+ @conditions = conditions
572
+ head(path, opts) { invoke(block) ; [] }
573
+ end
574
+
575
+ def put(path, opts={}, &bk); route 'PUT', path, opts, &bk; end
576
+ def post(path, opts={}, &bk); route 'POST', path, opts, &bk; end
577
+ def delete(path, opts={}, &bk); route 'DELETE', path, opts, &bk; end
578
+ def head(path, opts={}, &bk); route 'HEAD', path, opts, &bk; end
579
+
580
+ private
581
+ def route(verb, path, opts={}, &block)
582
+ host_name opts[:host] if opts.key?(:host)
583
+ user_agent opts[:agent] if opts.key?(:agent)
584
+ accept_mime_types opts[:provides] if opts.key?(:provides)
585
+
586
+ pattern, keys = compile(path)
587
+ conditions, @conditions = @conditions, []
588
+
589
+ define_method "#{verb} #{path}", &block
590
+ unbound_method = instance_method("#{verb} #{path}")
591
+ block = lambda { unbound_method.bind(self).call }
592
+
593
+ (routes[verb] ||= []).
594
+ push([pattern, keys, conditions, block]).last
595
+ end
596
+
597
+ def compile(path)
598
+ keys = []
599
+ if path.respond_to? :to_str
600
+ pattern =
601
+ URI.encode(path).gsub(/((:\w+)|\*)/) do |match|
602
+ if match == "*"
603
+ keys << 'splat'
604
+ "(.*?)"
605
+ else
606
+ keys << $2[1..-1]
607
+ "([^/?&#\.]+)"
608
+ end
609
+ end
610
+ [/^#{pattern}$/, keys]
611
+ elsif path.respond_to? :=~
612
+ [path, keys]
613
+ else
614
+ raise TypeError, path
615
+ end
616
+ end
617
+
618
+ public
619
+ def development? ; environment == :development ; end
620
+ def test? ; environment == :test ; end
621
+ def production? ; environment == :production ; end
622
+
623
+ def configure(*envs, &block)
624
+ yield if envs.empty? || envs.include?(environment.to_sym)
625
+ end
626
+
627
+ def use(middleware, *args, &block)
628
+ reset_middleware
629
+ @middleware << [middleware, args, block]
630
+ end
631
+
632
+ def run!(options={})
633
+ set(options)
634
+ handler = Rack::Handler.get(server)
635
+ handler_name = handler.name.gsub(/.*::/, '')
636
+ puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " +
637
+ "on #{port} for #{environment} with backup from #{handler_name}"
638
+ handler.run self, :Host => host, :Port => port do |server|
639
+ trap(:INT) do
640
+ ## Use thins' hard #stop! if available, otherwise just #stop
641
+ server.respond_to?(:stop!) ? server.stop! : server.stop
642
+ puts "\n== Sinatra has ended his set (crowd applauds)"
643
+ end
644
+ end
645
+ rescue Errno::EADDRINUSE => e
646
+ puts "== Someone is already performing on port #{port}!"
647
+ end
648
+
649
+ def call(env)
650
+ construct_middleware if @callsite.nil?
651
+ @callsite.call(env)
652
+ end
653
+
654
+ private
655
+ def construct_middleware(builder=Rack::Builder.new)
656
+ builder.use Rack::Session::Cookie if sessions?
657
+ builder.use Rack::CommonLogger if logging?
658
+ builder.use Rack::MethodOverride if methodoverride?
659
+ @middleware.each { |c, args, bk| builder.use(c, *args, &bk) }
660
+ builder.run new
661
+ @callsite = builder.to_app
662
+ end
663
+
664
+ def reset_middleware
665
+ @callsite = nil
666
+ end
667
+
668
+ def inherited(subclass)
669
+ subclass.routes = dupe_routes
670
+ subclass.templates = templates.dup
671
+ subclass.conditions = []
672
+ subclass.filters = filters.dup
673
+ subclass.errors = errors.dup
674
+ subclass.middleware = middleware.dup
675
+ subclass.send :reset_middleware
676
+ super
677
+ end
678
+
679
+ def dupe_routes
680
+ routes.inject({}) do |hash,(request_method,routes)|
681
+ hash[request_method] = routes.dup
682
+ hash
683
+ end
684
+ end
685
+
686
+ def metadef(message, &block)
687
+ (class << self; self; end).
688
+ send :define_method, message, &block
689
+ end
690
+ end
691
+
692
+ set :raise_errors, true
693
+ set :dump_errors, false
694
+ set :sessions, false
695
+ set :logging, false
696
+ set :methodoverride, false
697
+ set :static, false
698
+ set :environment, (ENV['RACK_ENV'] || :development).to_sym
699
+
700
+ set :run, false
701
+ set :server, (defined?(Rack::Handler::Thin) ? "thin" : "mongrel")
702
+ set :host, '0.0.0.0'
703
+ set :port, 4567
704
+
705
+ set :app_file, nil
706
+ set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) }
707
+ set :views, Proc.new { root && File.join(root, 'views') }
708
+ set :public, Proc.new { root && File.join(root, 'public') }
709
+
710
+ # static files route
711
+ get(/.*[^\/]$/) do
712
+ pass unless options.static? && options.public?
713
+ path = options.public + unescape(request.path_info)
714
+ pass unless File.file?(path)
715
+ send_file path, :disposition => nil
716
+ end
717
+
718
+ error ::Exception do
719
+ response.status = 500
720
+ content_type 'text/html'
721
+ '<h1>Internal Server Error</h1>'
722
+ end
723
+
724
+ configure :development do
725
+ get '/__sinatra__/:image.png' do
726
+ filename = File.dirname(__FILE__) + "/images/#{params[:image]}.png"
727
+ content_type :png
728
+ send_file filename
729
+ end
730
+
731
+ error NotFound do
732
+ (<<-HTML).gsub(/^ {8}/, '')
733
+ <!DOCTYPE html>
734
+ <html>
735
+ <head>
736
+ <style type="text/css">
737
+ body { text-align:center;font-family:helvetica,arial;font-size:22px;
738
+ color:#888;margin:20px}
739
+ #c {margin:0 auto;width:500px;text-align:left}
740
+ </style>
741
+ </head>
742
+ <body>
743
+ <h2>Sinatra doesn't know this ditty.</h2>
744
+ <img src='/__sinatra__/404.png'>
745
+ <div id="c">
746
+ Try this:
747
+ <pre>#{request.request_method.downcase} '#{request.path_info}' do\n "Hello World"\nend</pre>
748
+ </div>
749
+ </body>
750
+ </html>
751
+ HTML
752
+ end
753
+
754
+ error do
755
+ next unless err = request.env['sinatra.error']
756
+ heading = err.class.name + ' - ' + err.message.to_s
757
+ (<<-HTML).gsub(/^ {8}/, '')
758
+ <!DOCTYPE html>
759
+ <html>
760
+ <head>
761
+ <style type="text/css">
762
+ body {font-family:verdana;color:#333}
763
+ #c {margin-left:20px}
764
+ h1 {color:#1D6B8D;margin:0;margin-top:-30px}
765
+ h2 {color:#1D6B8D;font-size:18px}
766
+ pre {border-left:2px solid #ddd;padding-left:10px;color:#000}
767
+ img {margin-top:10px}
768
+ </style>
769
+ </head>
770
+ <body>
771
+ <div id="c">
772
+ <img src="/__sinatra__/500.png">
773
+ <h1>#{escape_html(heading)}</h1>
774
+ <pre class='trace'>#{escape_html(err.backtrace.join("\n"))}</pre>
775
+ <h2>Params</h2>
776
+ <pre>#{escape_html(params.inspect)}</pre>
777
+ </div>
778
+ </body>
779
+ </html>
780
+ HTML
781
+ end
782
+ end
783
+ end
784
+
785
+ class Default < Base
786
+ set :raise_errors, false
787
+ set :dump_errors, true
788
+ set :sessions, false
789
+ set :logging, true
790
+ set :methodoverride, true
791
+ set :static, true
792
+ set :run, false
793
+ set :reload, Proc.new { app_file? && development? }
794
+
795
+ def self.reloading?
796
+ @reloading ||= false
797
+ end
798
+
799
+ def self.configure(*envs)
800
+ super unless reloading?
801
+ end
802
+
803
+ def self.call(env)
804
+ reload! if reload?
805
+ super
806
+ end
807
+
808
+ def self.reload!
809
+ @reloading = true
810
+ superclass.send :inherited, self
811
+ $LOADED_FEATURES.delete("sinatra.rb")
812
+ ::Kernel.load app_file
813
+ @reloading = false
814
+ end
815
+
816
+ end
817
+
818
+ class Application < Default
819
+ end
820
+
821
+ module Delegator
822
+ METHODS = %w[
823
+ get put post delete head template layout before error not_found
824
+ configures configure set set_option set_options enable disable use
825
+ development? test? production? use_in_file_templates!
826
+ ]
827
+
828
+ METHODS.each do |method_name|
829
+ eval <<-RUBY, binding, '(__DELEGATE__)', 1
830
+ def #{method_name}(*args, &b)
831
+ ::Sinatra::Application.#{method_name}(*args, &b)
832
+ end
833
+ private :#{method_name}
834
+ RUBY
835
+ end
836
+ end
837
+
838
+ def self.new(base=Base, options={}, &block)
839
+ base = Class.new(base)
840
+ base.send :class_eval, &block if block_given?
841
+ base
842
+ end
843
+ end