Syd-sinatra 0.3.2 → 0.9.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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