darkhelmet-sinatra 0.9.0.5

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 (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