darkhelmet-sinatra 0.9.0.5

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