bmizerany-sinatra 0.3.2 → 0.8.9

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 +174 -0
  3. data/README.rdoc +138 -116
  4. data/Rakefile +27 -9
  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/{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/base.rb +818 -0
  40. data/lib/sinatra/compat.rb +239 -0
  41. data/{images → lib/sinatra/images}/404.png +0 -0
  42. data/{images → lib/sinatra/images}/500.png +0 -0
  43. data/lib/sinatra/main.rb +48 -0
  44. data/lib/sinatra/test/bacon.rb +17 -0
  45. data/lib/sinatra/test/rspec.rb +7 -8
  46. data/lib/sinatra/test/spec.rb +3 -4
  47. data/lib/sinatra/test/unit.rb +3 -5
  48. data/lib/sinatra/test.rb +109 -0
  49. data/lib/sinatra.rb +4 -1466
  50. data/sinatra.gemspec +67 -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 +18 -5
  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 +9 -0
  64. data/test/result_test.rb +88 -0
  65. data/test/routing_test.rb +334 -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 +78 -46
  80. data/ChangeLog +0 -78
  81. data/lib/sinatra/test/methods.rb +0 -76
  82. data/test/event_context_test.rb +0 -15
File without changes
@@ -62,6 +62,7 @@ context "Static files (by default)" do
62
62
  headers['Last-Modified'].should.equal last_modified.httpdate
63
63
  end
64
64
 
65
+ # Deprecated. Use: ConditionalGet middleware.
65
66
  specify "are not served when If-Modified-Since matches" do
66
67
  last_modified = File.mtime(Sinatra.application.options.public + '/foo.xml')
67
68
  @request = Rack::MockRequest.new(Sinatra.application)
@@ -91,7 +92,8 @@ context "SendData" do
91
92
  Sinatra.application = nil
92
93
  end
93
94
 
94
- specify "should send the data with options" do
95
+ # Deprecated. send_data is going away.
96
+ xspecify "should send the data with options" do
95
97
  get '/' do
96
98
  send_data 'asdf', :status => 500
97
99
  end
@@ -102,6 +104,7 @@ context "SendData" do
102
104
  body.should.equal 'asdf'
103
105
  end
104
106
 
107
+ # Deprecated. The Content-Disposition is no longer handled by sendfile.
105
108
  specify "should include a Content-Disposition header" do
106
109
  get '/' do
107
110
  send_file File.dirname(__FILE__) + '/public/foo.xml'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -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