Syd-sinatra 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/ChangeLog +78 -0
  2. data/LICENSE +22 -0
  3. data/README.rdoc +523 -0
  4. data/Rakefile +162 -0
  5. data/images/404.png +0 -0
  6. data/images/500.png +0 -0
  7. data/lib/sinatra/test/methods.rb +76 -0
  8. data/lib/sinatra/test/rspec.rb +10 -0
  9. data/lib/sinatra/test/spec.rb +10 -0
  10. data/lib/sinatra/test/unit.rb +13 -0
  11. data/lib/sinatra.rb +1470 -0
  12. data/sinatra.gemspec +77 -0
  13. data/test/app_test.rb +299 -0
  14. data/test/application_test.rb +318 -0
  15. data/test/builder_test.rb +101 -0
  16. data/test/custom_error_test.rb +62 -0
  17. data/test/erb_test.rb +136 -0
  18. data/test/event_context_test.rb +15 -0
  19. data/test/events_test.rb +65 -0
  20. data/test/filter_test.rb +30 -0
  21. data/test/haml_test.rb +233 -0
  22. data/test/helper.rb +7 -0
  23. data/test/mapped_error_test.rb +72 -0
  24. data/test/pipeline_test.rb +66 -0
  25. data/test/public/foo.xml +1 -0
  26. data/test/sass_test.rb +57 -0
  27. data/test/sessions_test.rb +39 -0
  28. data/test/streaming_test.rb +118 -0
  29. data/test/sym_params_test.rb +19 -0
  30. data/test/template_test.rb +30 -0
  31. data/test/use_in_file_templates_test.rb +47 -0
  32. data/test/views/foo.builder +1 -0
  33. data/test/views/foo.erb +1 -0
  34. data/test/views/foo.haml +1 -0
  35. data/test/views/foo.sass +2 -0
  36. data/test/views/foo_layout.erb +2 -0
  37. data/test/views/foo_layout.haml +2 -0
  38. data/test/views/layout_test/foo.builder +1 -0
  39. data/test/views/layout_test/foo.erb +1 -0
  40. data/test/views/layout_test/foo.haml +1 -0
  41. data/test/views/layout_test/foo.sass +2 -0
  42. data/test/views/layout_test/layout.builder +3 -0
  43. data/test/views/layout_test/layout.erb +1 -0
  44. data/test/views/layout_test/layout.haml +1 -0
  45. data/test/views/layout_test/layout.sass +2 -0
  46. data/test/views/no_layout/no_layout.builder +1 -0
  47. data/test/views/no_layout/no_layout.haml +1 -0
  48. metadata +129 -0
data/lib/sinatra.rb ADDED
@@ -0,0 +1,1470 @@
1
+ require 'time'
2
+ require 'ostruct'
3
+ require 'uri'
4
+ require 'rack'
5
+
6
+ if ENV['SWIFT']
7
+ require 'swiftcore/swiftiplied_mongrel'
8
+ puts "Using Swiftiplied Mongrel"
9
+ elsif ENV['EVENT']
10
+ require 'swiftcore/evented_mongrel'
11
+ puts "Using Evented Mongrel"
12
+ end
13
+
14
+ module Rack #:nodoc:
15
+
16
+ class Request #:nodoc:
17
+
18
+ # Set of request method names allowed via the _method parameter hack. By
19
+ # default, all request methods defined in RFC2616 are included, with the
20
+ # exception of TRACE and CONNECT.
21
+ POST_TUNNEL_METHODS_ALLOWED = %w( PUT DELETE OPTIONS HEAD )
22
+
23
+ # Return the HTTP request method with support for method tunneling using
24
+ # the POST _method parameter hack. If the real request method is POST and
25
+ # a _method param is given and the value is one defined in
26
+ # +POST_TUNNEL_METHODS_ALLOWED+, return the value of the _method param
27
+ # instead.
28
+ def request_method
29
+ if post_tunnel_method_hack?
30
+ params['_method'].upcase
31
+ else
32
+ @env['REQUEST_METHOD']
33
+ end
34
+ end
35
+
36
+ def user_agent
37
+ @env['HTTP_USER_AGENT']
38
+ end
39
+
40
+ private
41
+
42
+ # Return truthfully if the request is a valid verb-over-post hack.
43
+ def post_tunnel_method_hack?
44
+ @env['REQUEST_METHOD'] == 'POST' &&
45
+ POST_TUNNEL_METHODS_ALLOWED.include?(self.POST.fetch('_method', '').upcase)
46
+ end
47
+ end
48
+
49
+ module Utils
50
+ extend self
51
+ end
52
+ end
53
+
54
+
55
+ module Sinatra
56
+ extend self
57
+
58
+ VERSION = '0.3.2'
59
+
60
+ class NotFound < RuntimeError
61
+ def self.code ; 404 ; end
62
+ end
63
+ class ServerError < RuntimeError
64
+ def self.code ; 500 ; end
65
+ end
66
+
67
+ Result = Struct.new(:block, :params, :status) unless defined?(Result)
68
+
69
+ def options
70
+ application.options
71
+ end
72
+
73
+ def application
74
+ @app ||= Application.new
75
+ end
76
+
77
+ def application=(app)
78
+ @app = app
79
+ end
80
+
81
+ def port
82
+ application.options.port
83
+ end
84
+
85
+ def host
86
+ application.options.host
87
+ end
88
+
89
+ def env
90
+ application.options.env
91
+ end
92
+
93
+ # Deprecated: use application instead of build_application.
94
+ alias :build_application :application
95
+
96
+ def server
97
+ options.server ||= defined?(Rack::Handler::Thin) ? "thin" : "mongrel"
98
+
99
+ # Convert the server into the actual handler name
100
+ handler = options.server.capitalize
101
+
102
+ # If the convenience conversion didn't get us anything,
103
+ # fall back to what the user actually set.
104
+ handler = options.server unless Rack::Handler.const_defined?(handler)
105
+
106
+ @server ||= eval("Rack::Handler::#{handler}")
107
+ end
108
+
109
+ def run
110
+ begin
111
+ puts "== Sinatra/#{Sinatra::VERSION} has taken the stage on port #{port} for #{env} with backup by #{server.name}"
112
+ server.run(application, {:Port => port, :Host => host}) do |server|
113
+ trap(:INT) do
114
+ server.stop
115
+ puts "\n== Sinatra has ended his set (crowd applauds)"
116
+ end
117
+ end
118
+ rescue Errno::EADDRINUSE => e
119
+ puts "== Someone is already performing on port #{port}!"
120
+ end
121
+ end
122
+
123
+ class Event
124
+ include Rack::Utils
125
+
126
+ URI_CHAR = '[^/?:,&#\.]'.freeze unless defined?(URI_CHAR)
127
+ PARAM = /(:(#{URI_CHAR}+)|\*)/.freeze unless defined?(PARAM)
128
+ SPLAT = /(.*?)/
129
+ attr_reader :path, :block, :param_keys, :pattern, :options
130
+
131
+ def initialize(path, options = {}, &b)
132
+ @path = URI.encode(path)
133
+ @block = b
134
+ @param_keys = []
135
+ @options = options
136
+ splats = 0
137
+ regex = @path.to_s.gsub(PARAM) do |match|
138
+ if match == "*"
139
+ @param_keys << "_splat_#{splats}"
140
+ splats += 1
141
+ SPLAT.to_s
142
+ else
143
+ @param_keys << $2
144
+ "(#{URI_CHAR}+)"
145
+ end
146
+ end
147
+
148
+ @pattern = /^#{regex}$/
149
+ end
150
+
151
+ def invoke(request)
152
+ params = {}
153
+ if agent = options[:agent]
154
+ return unless request.user_agent =~ agent
155
+ params[:agent] = $~[1..-1]
156
+ end
157
+ if host = options[:host]
158
+ return unless host === request.host
159
+ end
160
+ return unless pattern =~ request.path_info.squeeze('/')
161
+ path_params = param_keys.zip($~.captures.map{|s| unescape(s) if s}).to_hash
162
+ params.merge!(path_params)
163
+ splats = params.select { |k, v| k =~ /^_splat_\d+$/ }.sort.map(&:last)
164
+ unless splats.empty?
165
+ params.delete_if { |k, v| k =~ /^_splat_\d+$/ }
166
+ params["splat"] = splats
167
+ end
168
+ Result.new(block, params, 200)
169
+ end
170
+
171
+ end
172
+
173
+ class Error
174
+
175
+ attr_reader :type, :block, :options
176
+
177
+ def initialize(type, options={}, &block)
178
+ @type = type
179
+ @block = block
180
+ @options = options
181
+ end
182
+
183
+ def invoke(request)
184
+ Result.new(block, options, code)
185
+ end
186
+
187
+ def code
188
+ if type.respond_to?(:code)
189
+ type.code
190
+ else
191
+ 500
192
+ end
193
+ end
194
+
195
+ end
196
+
197
+ class Static
198
+ include Rack::Utils
199
+
200
+ def initialize(app)
201
+ @app = app
202
+ end
203
+
204
+ def invoke(request)
205
+ path = @app.options.public + unescape(request.path_info)
206
+ return unless File.file?(path)
207
+ block = Proc.new { send_file path, :disposition => nil }
208
+ Result.new(block, {}, 200)
209
+ end
210
+ end
211
+
212
+ # Methods for sending files and streams to the browser instead of rendering.
213
+ module Streaming
214
+ DEFAULT_SEND_FILE_OPTIONS = {
215
+ :type => 'application/octet-stream'.freeze,
216
+ :disposition => 'attachment'.freeze,
217
+ :stream => true,
218
+ :buffer_size => 8192
219
+ }.freeze
220
+
221
+ class MissingFile < RuntimeError; end
222
+
223
+ class FileStreamer
224
+ attr_reader :path, :options
225
+
226
+ def initialize(path, options)
227
+ @path, @options = path, options
228
+ end
229
+
230
+ def to_result(cx, *args)
231
+ self
232
+ end
233
+
234
+ def each
235
+ size = options[:buffer_size]
236
+ File.open(path, 'rb') do |file|
237
+ while buf = file.read(size)
238
+ yield buf
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ protected
245
+ # Sends the file by streaming it 8192 bytes at a time. This way the
246
+ # whole file doesn't need to be read into memory at once. This makes
247
+ # it feasible to send even large files.
248
+ #
249
+ # Be careful to sanitize the path parameter if it coming from a web
250
+ # page. send_file(params[:path]) allows a malicious user to
251
+ # download any file on your server.
252
+ #
253
+ # Options:
254
+ # * <tt>:filename</tt> - suggests a filename for the browser to use.
255
+ # Defaults to File.basename(path).
256
+ # * <tt>:type</tt> - specifies an HTTP content type.
257
+ # Defaults to 'application/octet-stream'.
258
+ # * <tt>:disposition</tt> - specifies whether the file will be shown
259
+ # inline or downloaded. Valid values are 'inline' and 'attachment'
260
+ # (default). When set to nil, the Content-Disposition and
261
+ # Content-Transfer-Encoding headers are omitted entirely.
262
+ # * <tt>:stream</tt> - whether to send the file to the user agent as it
263
+ # is read (true) or to read the entire file before sending (false).
264
+ # Defaults to true.
265
+ # * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used
266
+ # to stream the file. Defaults to 8192.
267
+ # * <tt>:status</tt> - specifies the status code to send with the
268
+ # response. Defaults to '200 OK'.
269
+ # * <tt>:last_modified</tt> - an optional RFC 2616 formatted date value
270
+ # (See Time#httpdate) indicating the last modified time of the file.
271
+ # If the request includes an If-Modified-Since header that matches this
272
+ # value exactly, a 304 Not Modified response is sent instead of the file.
273
+ # Defaults to the file's last modified time.
274
+ #
275
+ # The default Content-Type and Content-Disposition headers are
276
+ # set to download arbitrary binary files in as many browsers as
277
+ # possible. IE versions 4, 5, 5.5, and 6 are all known to have
278
+ # a variety of quirks (especially when downloading over SSL).
279
+ #
280
+ # Simple download:
281
+ # send_file '/path/to.zip'
282
+ #
283
+ # Show a JPEG in the browser:
284
+ # send_file '/path/to.jpeg',
285
+ # :type => 'image/jpeg',
286
+ # :disposition => 'inline'
287
+ #
288
+ # Show a 404 page in the browser:
289
+ # send_file '/path/to/404.html,
290
+ # :type => 'text/html; charset=utf-8',
291
+ # :status => 404
292
+ #
293
+ # Read about the other Content-* HTTP headers if you'd like to
294
+ # provide the user with more information (such as Content-Description).
295
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
296
+ #
297
+ # Also be aware that the document may be cached by proxies and browsers.
298
+ # The Pragma and Cache-Control headers declare how the file may be cached
299
+ # by intermediaries. They default to require clients to validate with
300
+ # the server before releasing cached responses. See
301
+ # http://www.mnot.net/cache_docs/ for an overview of web caching and
302
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
303
+ # for the Cache-Control header spec.
304
+ def send_file(path, options = {}) #:doc:
305
+ raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path)
306
+
307
+ options[:length] ||= File.size(path)
308
+ options[:filename] ||= File.basename(path)
309
+ options[:type] ||= Rack::File::MIME_TYPES[File.extname(options[:filename])[1..-1]] || 'text/plain'
310
+ options[:last_modified] ||= File.mtime(path).httpdate
311
+ options[:stream] = true unless options.key?(:stream)
312
+ options[:buffer_size] ||= DEFAULT_SEND_FILE_OPTIONS[:buffer_size]
313
+ send_file_headers! options
314
+
315
+ if options[:stream]
316
+ throw :halt, [options[:status] || 200, FileStreamer.new(path, options)]
317
+ else
318
+ File.open(path, 'rb') { |file| throw :halt, [options[:status] || 200, [file.read]] }
319
+ end
320
+ end
321
+
322
+ # Send binary data to the user as a file download. May set content type,
323
+ # apparent file name, and specify whether to show data inline or download
324
+ # as an attachment.
325
+ #
326
+ # Options:
327
+ # * <tt>:filename</tt> - Suggests a filename for the browser to use.
328
+ # * <tt>:type</tt> - specifies an HTTP content type.
329
+ # Defaults to 'application/octet-stream'.
330
+ # * <tt>:disposition</tt> - specifies whether the file will be shown inline
331
+ # or downloaded. Valid values are 'inline' and 'attachment' (default).
332
+ # * <tt>:status</tt> - specifies the status code to send with the response.
333
+ # Defaults to '200 OK'.
334
+ # * <tt>:last_modified</tt> - an optional RFC 2616 formatted date value (See
335
+ # Time#httpdate) indicating the last modified time of the response entity.
336
+ # If the request includes an If-Modified-Since header that matches this
337
+ # value exactly, a 304 Not Modified response is sent instead of the data.
338
+ #
339
+ # Generic data download:
340
+ # send_data buffer
341
+ #
342
+ # Download a dynamically-generated tarball:
343
+ # send_data generate_tgz('dir'), :filename => 'dir.tgz'
344
+ #
345
+ # Display an image Active Record in the browser:
346
+ # send_data image.data,
347
+ # :type => image.content_type,
348
+ # :disposition => 'inline'
349
+ #
350
+ # See +send_file+ for more information on HTTP Content-* headers and caching.
351
+ def send_data(data, options = {}) #:doc:
352
+ send_file_headers! options.merge(:length => data.size)
353
+ throw :halt, [options[:status] || 200, [data]]
354
+ end
355
+
356
+ private
357
+
358
+ def send_file_headers!(options)
359
+ options = DEFAULT_SEND_FILE_OPTIONS.merge(options)
360
+ [:length, :type, :disposition].each do |arg|
361
+ raise ArgumentError, ":#{arg} option required" unless options.key?(arg)
362
+ end
363
+
364
+ # Send a "304 Not Modified" if the last_modified option is provided and
365
+ # matches the If-Modified-Since request header value.
366
+ if last_modified = options[:last_modified]
367
+ header 'Last-Modified' => last_modified
368
+ throw :halt, [ 304, '' ] if last_modified == request.env['HTTP_IF_MODIFIED_SINCE']
369
+ end
370
+
371
+ headers(
372
+ 'Content-Length' => options[:length].to_s,
373
+ 'Content-Type' => options[:type].strip # fixes a problem with extra '\r' with some browsers
374
+ )
375
+
376
+ # Omit Content-Disposition and Content-Transfer-Encoding headers if
377
+ # the :disposition option set to nil.
378
+ if !options[:disposition].nil?
379
+ disposition = options[:disposition].dup || 'attachment'
380
+ disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
381
+ headers 'Content-Disposition' => disposition, 'Content-Transfer-Encoding' => 'binary'
382
+ end
383
+
384
+ # Fix a problem with IE 6.0 on opening downloaded files:
385
+ # If Cache-Control: no-cache is set (which Rails does by default),
386
+ # IE removes the file it just downloaded from its cache immediately
387
+ # after it displays the "open/save" dialog, which means that if you
388
+ # hit "open" the file isn't there anymore when the application that
389
+ # is called for handling the download is run, so let's workaround that
390
+ header('Cache-Control' => 'private') if headers['Cache-Control'] == 'no-cache'
391
+ end
392
+ end
393
+
394
+
395
+ # Helper methods for building various aspects of the HTTP response.
396
+ module ResponseHelpers
397
+
398
+ # Immediately halt response execution by redirecting to the resource
399
+ # specified. The +path+ argument may be an absolute URL or a path
400
+ # relative to the site root. Additional arguments are passed to the
401
+ # halt.
402
+ #
403
+ # With no integer status code, a '302 Temporary Redirect' response is
404
+ # sent. To send a permanent redirect, pass an explicit status code of
405
+ # 301:
406
+ #
407
+ # redirect '/somewhere/else', 301
408
+ #
409
+ # NOTE: No attempt is made to rewrite the path based on application
410
+ # context. The 'Location' response header is set verbatim to the value
411
+ # provided.
412
+ def redirect(path, *args)
413
+ status(302)
414
+ header 'Location' => path
415
+ throw :halt, *args
416
+ end
417
+
418
+ # Access or modify response headers. With no argument, return the
419
+ # underlying headers Hash. With a Hash argument, add or overwrite
420
+ # existing response headers with the values provided:
421
+ #
422
+ # headers 'Content-Type' => "text/html;charset=utf-8",
423
+ # 'Last-Modified' => Time.now.httpdate,
424
+ # 'X-UA-Compatible' => 'IE=edge'
425
+ #
426
+ # This method also available in singular form (#header).
427
+ def headers(header = nil)
428
+ @response.headers.merge!(header) if header
429
+ @response.headers
430
+ end
431
+ alias :header :headers
432
+
433
+ # Set the content type of the response body (HTTP 'Content-Type' header).
434
+ #
435
+ # The +type+ argument may be an internet media type (e.g., 'text/html',
436
+ # 'application/xml+atom', 'image/png') or a Symbol key into the
437
+ # Rack::File::MIME_TYPES table.
438
+ #
439
+ # Media type parameters, such as "charset", may also be specified using the
440
+ # optional hash argument:
441
+ #
442
+ # get '/foo.html' do
443
+ # content_type 'text/html', :charset => 'utf-8'
444
+ # "<h1>Hello World</h1>"
445
+ # end
446
+ #
447
+ def content_type(type, params={})
448
+ type = Rack::File::MIME_TYPES[type.to_s] if type.kind_of?(Symbol)
449
+ fail "Invalid or undefined media_type: #{type}" if type.nil?
450
+ if params.any?
451
+ params = params.collect { |kv| "%s=%s" % kv }.join(', ')
452
+ type = [ type, params ].join(";")
453
+ end
454
+ response.header['Content-Type'] = type
455
+ end
456
+
457
+ # Set the last modified time of the resource (HTTP 'Last-Modified' header)
458
+ # and halt if conditional GET matches. The +time+ argument is a Time,
459
+ # DateTime, or other object that responds to +to_time+.
460
+ #
461
+ # When the current request includes an 'If-Modified-Since' header that
462
+ # matches the time specified, execution is immediately halted with a
463
+ # '304 Not Modified' response.
464
+ #
465
+ # Calling this method before perfoming heavy processing (e.g., lengthy
466
+ # database queries, template rendering, complex logic) can dramatically
467
+ # increase overall throughput with caching clients.
468
+ def last_modified(time)
469
+ time = time.to_time if time.respond_to?(:to_time)
470
+ time = time.httpdate if time.respond_to?(:httpdate)
471
+ response.header['Last-Modified'] = time
472
+ throw :halt, 304 if time == request.env['HTTP_IF_MODIFIED_SINCE']
473
+ time
474
+ end
475
+
476
+ # Set the response entity tag (HTTP 'ETag' header) and halt if conditional
477
+ # GET matches. The +value+ argument is an identifier that uniquely
478
+ # identifies the current version of the resource. The +strength+ argument
479
+ # indicates whether the etag should be used as a :strong (default) or :weak
480
+ # cache validator.
481
+ #
482
+ # When the current request includes an 'If-None-Match' header with a
483
+ # matching etag, execution is immediately halted. If the request method is
484
+ # GET or HEAD, a '304 Not Modified' response is sent. For all other request
485
+ # methods, a '412 Precondition Failed' response is sent.
486
+ #
487
+ # Calling this method before perfoming heavy processing (e.g., lengthy
488
+ # database queries, template rendering, complex logic) can dramatically
489
+ # increase overall throughput with caching clients.
490
+ #
491
+ # ==== See Also
492
+ # {RFC2616: ETag}[http://w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19],
493
+ # ResponseHelpers#last_modified
494
+ def entity_tag(value, strength=:strong)
495
+ value =
496
+ case strength
497
+ when :strong then '"%s"' % value
498
+ when :weak then 'W/"%s"' % value
499
+ else raise TypeError, "strength must be one of :strong or :weak"
500
+ end
501
+ response.header['ETag'] = value
502
+
503
+ # Check for If-None-Match request header and halt if match is found.
504
+ etags = (request.env['HTTP_IF_NONE_MATCH'] || '').split(/\s*,\s*/)
505
+ if etags.include?(value) || etags.include?('*')
506
+ # GET/HEAD requests: send Not Modified response
507
+ throw :halt, 304 if request.get? || request.head?
508
+ # Other requests: send Precondition Failed response
509
+ throw :halt, 412
510
+ end
511
+ end
512
+
513
+ alias :etag :entity_tag
514
+
515
+ end
516
+
517
+ module RenderingHelpers
518
+
519
+ def render(renderer, template, options={})
520
+ m = method("render_#{renderer}")
521
+ result = m.call(resolve_template(renderer, template, options), options)
522
+ if layout = determine_layout(renderer, template, options)
523
+ result = m.call(resolve_template(renderer, layout, options), options) { result }
524
+ end
525
+ result
526
+ end
527
+
528
+ def determine_layout(renderer, template, options)
529
+ return if options[:layout] == false
530
+ layout_from_options = options[:layout] || :layout
531
+ resolve_template(renderer, layout_from_options, options, false)
532
+ end
533
+
534
+ private
535
+
536
+ def resolve_template(renderer, template, options, scream = true)
537
+ case template
538
+ when String
539
+ template
540
+ when Proc
541
+ template.call
542
+ when Symbol
543
+ if proc = templates[template]
544
+ resolve_template(renderer, proc, options, scream)
545
+ else
546
+ read_template_file(renderer, template, options, scream)
547
+ end
548
+ else
549
+ nil
550
+ end
551
+ end
552
+
553
+ def read_template_file(renderer, template, options, scream = true)
554
+ path = File.join(
555
+ options[:views_directory] || Sinatra.application.options.views,
556
+ "#{template}.#{renderer}"
557
+ )
558
+ unless File.exists?(path)
559
+ raise Errno::ENOENT.new(path) if scream
560
+ nil
561
+ else
562
+ File.read(path)
563
+ end
564
+ end
565
+
566
+ def templates
567
+ Sinatra.application.templates
568
+ end
569
+
570
+ end
571
+
572
+ module Erb
573
+
574
+ def erb(content, options={})
575
+ require 'erb'
576
+ render(:erb, content, options)
577
+ end
578
+
579
+ private
580
+
581
+ def render_erb(content, options = {})
582
+ locals_opt = options.delete(:locals) || {}
583
+
584
+ locals_code = ""
585
+ locals_hash = {}
586
+ locals_opt.each do |key, value|
587
+ locals_code << "#{key} = locals_hash[:#{key}]\n"
588
+ locals_hash[:"#{key}"] = value
589
+ end
590
+
591
+ body = ::ERB.new(content).src
592
+ eval("#{locals_code}#{body}", binding)
593
+ end
594
+
595
+ end
596
+
597
+ module Haml
598
+
599
+ def haml(content, options={})
600
+ require 'haml'
601
+ render(:haml, content, options)
602
+ end
603
+
604
+ private
605
+
606
+ def render_haml(content, options = {}, &b)
607
+ haml_options = (options[:options] || {}).
608
+ merge(Sinatra.options.haml || {})
609
+ ::Haml::Engine.new(content, haml_options).
610
+ render(options[:scope] || self, options[:locals] || {}, &b)
611
+ end
612
+
613
+ end
614
+
615
+ # Generate valid CSS using Sass (part of Haml)
616
+ #
617
+ # Sass templates can be in external files with <tt>.sass</tt> extension
618
+ # or can use Sinatra's in_file_templates. In either case, the file can
619
+ # be rendered by passing the name of the template to the +sass+ method
620
+ # as a symbol.
621
+ #
622
+ # Unlike Haml, Sass does not support a layout file, so the +sass+ method
623
+ # will ignore both the default <tt>layout.sass</tt> file and any parameters
624
+ # passed in as <tt>:layout</tt> in the options hash.
625
+ #
626
+ # === Sass Template Files
627
+ #
628
+ # Sass templates can be stored in separate files with a <tt>.sass</tt>
629
+ # extension under the view path.
630
+ #
631
+ # Example:
632
+ # get '/stylesheet.css' do
633
+ # header 'Content-Type' => 'text/css; charset=utf-8'
634
+ # sass :stylesheet
635
+ # end
636
+ #
637
+ # The "views/stylesheet.sass" file might contain the following:
638
+ #
639
+ # body
640
+ # #admin
641
+ # :background-color #CCC
642
+ # #main
643
+ # :background-color #000
644
+ # #form
645
+ # :border-color #AAA
646
+ # :border-width 10px
647
+ #
648
+ # And yields the following output:
649
+ #
650
+ # body #admin {
651
+ # background-color: #CCC; }
652
+ # body #main {
653
+ # background-color: #000; }
654
+ #
655
+ # #form {
656
+ # border-color: #AAA;
657
+ # border-width: 10px; }
658
+ #
659
+ #
660
+ # NOTE: Haml must be installed or a LoadError will be raised the first time an
661
+ # attempt is made to render a Sass template.
662
+ #
663
+ # See http://haml.hamptoncatlin.com/docs/rdoc/classes/Sass.html for comprehensive documentation on Sass.
664
+ module Sass
665
+
666
+ def sass(content, options = {})
667
+ require 'sass'
668
+
669
+ # Sass doesn't support a layout, so we override any possible layout here
670
+ options[:layout] = false
671
+
672
+ render(:sass, content, options)
673
+ end
674
+
675
+ private
676
+
677
+ def render_sass(content, options = {})
678
+ ::Sass::Engine.new(content).render
679
+ end
680
+
681
+ end
682
+
683
+ # Generating conservative XML content using Builder templates.
684
+ #
685
+ # Builder templates can be inline by passing a block to the builder method,
686
+ # or in external files with +.builder+ extension by passing the name of the
687
+ # template to the +builder+ method as a Symbol.
688
+ #
689
+ # === Inline Rendering
690
+ #
691
+ # If the builder method is given a block, the block is called directly with
692
+ # an +XmlMarkup+ instance and the result is returned as String:
693
+ # get '/who.xml' do
694
+ # builder do |xml|
695
+ # xml.instruct!
696
+ # xml.person do
697
+ # xml.name "Francis Albert Sinatra",
698
+ # :aka => "Frank Sinatra"
699
+ # xml.email 'frank@capitolrecords.com'
700
+ # end
701
+ # end
702
+ # end
703
+ #
704
+ # Yields the following XML:
705
+ # <?xml version='1.0' encoding='UTF-8'?>
706
+ # <person>
707
+ # <name aka='Frank Sinatra'>Francis Albert Sinatra</name>
708
+ # <email>Frank Sinatra</email>
709
+ # </person>
710
+ #
711
+ # === Builder Template Files
712
+ #
713
+ # Builder templates can be stored in separate files with a +.builder+
714
+ # extension under the view path. An +XmlMarkup+ object named +xml+ is
715
+ # automatically made available to template.
716
+ #
717
+ # Example:
718
+ # get '/bio.xml' do
719
+ # builder :bio
720
+ # end
721
+ #
722
+ # The "views/bio.builder" file might contain the following:
723
+ # xml.instruct! :xml, :version => '1.1'
724
+ # xml.person do
725
+ # xml.name "Francis Albert Sinatra"
726
+ # xml.aka "Frank Sinatra"
727
+ # xml.aka "Ol' Blue Eyes"
728
+ # xml.aka "The Chairman of the Board"
729
+ # xml.born 'date' => '1915-12-12' do
730
+ # xml.text! "Hoboken, New Jersey, U.S.A."
731
+ # end
732
+ # xml.died 'age' => 82
733
+ # end
734
+ #
735
+ # And yields the following output:
736
+ # <?xml version='1.1' encoding='UTF-8'?>
737
+ # <person>
738
+ # <name>Francis Albert Sinatra</name>
739
+ # <aka>Frank Sinatra</aka>
740
+ # <aka>Ol&apos; Blue Eyes</aka>
741
+ # <aka>The Chairman of the Board</aka>
742
+ # <born date='1915-12-12'>Hoboken, New Jersey, U.S.A.</born>
743
+ # <died age='82' />
744
+ # </person>
745
+ #
746
+ # NOTE: Builder must be installed or a LoadError will be raised the first
747
+ # time an attempt is made to render a builder template.
748
+ #
749
+ # See http://builder.rubyforge.org/ for comprehensive documentation on
750
+ # Builder.
751
+ module Builder
752
+
753
+ def builder(content=nil, options={}, &block)
754
+ options, content = content, nil if content.is_a?(Hash)
755
+ content = Proc.new { block } if content.nil?
756
+ render(:builder, content, options)
757
+ end
758
+
759
+ private
760
+
761
+ def render_builder(content, options = {}, &b)
762
+ require 'builder'
763
+ xml = ::Builder::XmlMarkup.new(:indent => 2)
764
+ case content
765
+ when String
766
+ eval(content, binding, '<BUILDER>', 1)
767
+ when Proc
768
+ content.call(xml)
769
+ end
770
+ xml.target!
771
+ end
772
+
773
+ end
774
+
775
+ class EventContext
776
+ include Rack::Utils
777
+ include ResponseHelpers
778
+ include Streaming
779
+ include RenderingHelpers
780
+ include Erb
781
+ include Haml
782
+ include Builder
783
+ include Sass
784
+
785
+ attr_accessor :request, :response
786
+
787
+ attr_accessor :route_params
788
+
789
+ def initialize(request, response, route_params)
790
+ @params = nil
791
+ @data = nil
792
+ @request = request
793
+ @response = response
794
+ @route_params = route_params
795
+ @response.body = nil
796
+ end
797
+
798
+ def status(value=nil)
799
+ response.status = value if value
800
+ response.status
801
+ end
802
+
803
+ def body(value=nil)
804
+ response.body = value if value
805
+ response.body
806
+ end
807
+
808
+ def params
809
+ @params ||=
810
+ begin
811
+ hash = Hash.new {|h,k| h[k.to_s] if Symbol === k}
812
+ hash.merge! @request.params
813
+ hash.merge! @route_params
814
+ hash
815
+ end
816
+ end
817
+
818
+ def data
819
+ @data ||= params.keys.first
820
+ end
821
+
822
+ def stop(*args)
823
+ throw :halt, args
824
+ end
825
+
826
+ def complete(returned)
827
+ @response.body || returned
828
+ end
829
+
830
+ def session
831
+ request.env['rack.session'] ||= {}
832
+ end
833
+
834
+ def reset!
835
+ @params = nil
836
+ @data = nil
837
+ end
838
+
839
+ private
840
+
841
+ def method_missing(name, *args, &b)
842
+ if @response.respond_to?(name)
843
+ @response.send(name, *args, &b)
844
+ else
845
+ super
846
+ end
847
+ end
848
+
849
+ end
850
+
851
+
852
+ # The Application class represents the top-level working area of a
853
+ # Sinatra app. It provides the DSL for defining various aspects of the
854
+ # application and implements a Rack compatible interface for dispatching
855
+ # requests.
856
+ #
857
+ # Many of the instance methods defined in this class (#get, #post,
858
+ # #put, #delete, #layout, #before, #error, #not_found, etc.) are
859
+ # available at top-level scope. When invoked from top-level, the
860
+ # messages are forwarded to the "default application" (accessible
861
+ # at Sinatra::application).
862
+ class Application
863
+
864
+ # Hash of event handlers with request method keys and
865
+ # arrays of potential handlers as values.
866
+ attr_reader :events
867
+
868
+ # Hash of error handlers with error status codes as keys and
869
+ # handlers as values.
870
+ attr_reader :errors
871
+
872
+ # Hash of template name mappings.
873
+ attr_reader :templates
874
+
875
+ # Hash of filters with event name keys (:before) and arrays of
876
+ # handlers as values.
877
+ attr_reader :filters
878
+
879
+ # Array of objects to clear during reload. The objects in this array
880
+ # must respond to :clear.
881
+ attr_reader :clearables
882
+
883
+ # Object including open attribute methods for modifying Application
884
+ # configuration.
885
+ attr_reader :options
886
+
887
+ # List of methods available from top-level scope. When invoked from
888
+ # top-level the method is forwarded to the default application
889
+ # (Sinatra::application).
890
+ FORWARD_METHODS = %w[
891
+ get put post delete head template layout before error not_found
892
+ configures configure set set_options set_option enable disable use
893
+ development? test? production?
894
+ ]
895
+
896
+ # Create a new Application with a default configuration taken
897
+ # from the default_options Hash.
898
+ #
899
+ # NOTE: A default Application is automatically created the first
900
+ # time any of Sinatra's DSL related methods is invoked so there
901
+ # is typically no need to create an instance explicitly. See
902
+ # Sinatra::application for more information.
903
+ def initialize
904
+ @reloading = false
905
+ @clearables = [
906
+ @events = Hash.new { |hash, key| hash[key] = [] },
907
+ @errors = Hash.new,
908
+ @filters = Hash.new { |hash, key| hash[key] = [] },
909
+ @templates = Hash.new,
910
+ @middleware = []
911
+ ]
912
+ @options = OpenStruct.new(self.class.default_options)
913
+ load_default_configuration!
914
+ end
915
+
916
+ # Hash of default application configuration options. When a new
917
+ # Application is created, the #options object takes its initial values
918
+ # from here.
919
+ #
920
+ # Changes to the default_options Hash effect only Application objects
921
+ # created after the changes are made. For this reason, modifications to
922
+ # the default_options Hash typically occur at the very beginning of a
923
+ # file, before any DSL related functions are invoked.
924
+ def self.default_options
925
+ return @default_options unless @default_options.nil?
926
+ root = File.expand_path(File.dirname($0))
927
+ @default_options = {
928
+ :run => true,
929
+ :port => 4567,
930
+ :host => '0.0.0.0',
931
+ :env => (ENV['RACK_ENV'] || :development).to_sym,
932
+ :root => root,
933
+ :views => root + '/views',
934
+ :public => root + '/public',
935
+ :sessions => false,
936
+ :logging => true,
937
+ :app_file => $0,
938
+ :raise_errors => false
939
+ }
940
+ load_default_options_from_command_line!
941
+ @default_options
942
+ end
943
+
944
+ # Search ARGV for command line arguments and update the
945
+ # Sinatra::default_options Hash accordingly. This method is
946
+ # invoked the first time the default_options Hash is accessed.
947
+ # NOTE: Ignores --name so unit/spec tests can run individually
948
+ def self.load_default_options_from_command_line! #:nodoc:
949
+ # fixes issue with: gem install --test sinatra
950
+ return if ARGV.empty? || File.basename($0) =~ /gem/
951
+ require 'optparse'
952
+ OptionParser.new do |op|
953
+ op.on('-p port') { |port| default_options[:port] = port }
954
+ op.on('-e env') { |env| default_options[:env] = env.to_sym }
955
+ op.on('-x') { default_options[:mutex] = true }
956
+ op.on('-s server') { |server| default_options[:server] = server }
957
+ end.parse!(ARGV.dup.select { |o| o !~ /--name/ })
958
+ end
959
+
960
+ # Determine whether the application is in the process of being
961
+ # reloaded.
962
+ def reloading?
963
+ @reloading == true
964
+ end
965
+
966
+ # Yield to the block for configuration if the current environment
967
+ # matches any included in the +envs+ list. Always yield to the block
968
+ # when no environment is specified.
969
+ #
970
+ # NOTE: configuration blocks are not executed during reloads.
971
+ def configures(*envs, &b)
972
+ return if reloading?
973
+ yield self if envs.empty? || envs.include?(options.env)
974
+ end
975
+
976
+ alias :configure :configures
977
+
978
+ # When both +option+ and +value+ arguments are provided, set the option
979
+ # specified. With a single Hash argument, set all options specified in
980
+ # Hash. Options are available via the Application#options object.
981
+ #
982
+ # Setting individual options:
983
+ # set :port, 80
984
+ # set :env, :production
985
+ # set :views, '/path/to/views'
986
+ #
987
+ # Setting multiple options:
988
+ # set :port => 80,
989
+ # :env => :production,
990
+ # :views => '/path/to/views'
991
+ #
992
+ def set(option, value=self)
993
+ if value == self && option.kind_of?(Hash)
994
+ option.each { |key,val| set(key, val) }
995
+ else
996
+ options.send("#{option}=", value)
997
+ end
998
+ end
999
+
1000
+ alias :set_option :set
1001
+ alias :set_options :set
1002
+
1003
+ # Enable the options specified by setting their values to true. For
1004
+ # example, to enable sessions and logging:
1005
+ # enable :sessions, :logging
1006
+ def enable(*opts)
1007
+ opts.each { |key| set(key, true) }
1008
+ end
1009
+
1010
+ # Disable the options specified by setting their values to false. For
1011
+ # example, to disable logging and automatic run:
1012
+ # disable :logging, :run
1013
+ def disable(*opts)
1014
+ opts.each { |key| set(key, false) }
1015
+ end
1016
+
1017
+ # Define an event handler for the given request method and path
1018
+ # spec. The block is executed when a request matches the method
1019
+ # and spec.
1020
+ #
1021
+ # NOTE: The #get, #post, #put, and #delete helper methods should
1022
+ # be used to define events when possible.
1023
+ def event(method, path, options = {}, &b)
1024
+ events[method].push(Event.new(path, options, &b)).last
1025
+ end
1026
+
1027
+ # Define an event handler for GET requests.
1028
+ def get(path, options={}, &b)
1029
+ event(:get, path, options, &b)
1030
+ end
1031
+
1032
+ # Define an event handler for POST requests.
1033
+ def post(path, options={}, &b)
1034
+ event(:post, path, options, &b)
1035
+ end
1036
+
1037
+ # Define an event handler for HEAD requests.
1038
+ def head(path, options={}, &b)
1039
+ event(:head, path, options, &b)
1040
+ end
1041
+
1042
+ # Define an event handler for PUT requests.
1043
+ #
1044
+ # NOTE: PUT events are triggered when the HTTP request method is
1045
+ # PUT and also when the request method is POST and the body includes a
1046
+ # "_method" parameter set to "PUT".
1047
+ def put(path, options={}, &b)
1048
+ event(:put, path, options, &b)
1049
+ end
1050
+
1051
+ # Define an event handler for DELETE requests.
1052
+ #
1053
+ # NOTE: DELETE events are triggered when the HTTP request method is
1054
+ # DELETE and also when the request method is POST and the body includes a
1055
+ # "_method" parameter set to "DELETE".
1056
+ def delete(path, options={}, &b)
1057
+ event(:delete, path, options, &b)
1058
+ end
1059
+
1060
+ # Visits and invokes each handler registered for the +request_method+ in
1061
+ # definition order until a Result response is produced. If no handler
1062
+ # responds with a Result, the NotFound error handler is invoked.
1063
+ #
1064
+ # When the request_method is "HEAD" and no valid Result is produced by
1065
+ # the set of handlers registered for HEAD requests, an attempt is made to
1066
+ # invoke the GET handlers to generate the response before resorting to the
1067
+ # default error handler.
1068
+ def lookup(request)
1069
+ method = request.request_method.downcase.to_sym
1070
+ events[method].eject(&[:invoke, request]) ||
1071
+ (events[:get].eject(&[:invoke, request]) if method == :head) ||
1072
+ errors[NotFound].invoke(request)
1073
+ end
1074
+
1075
+ # Define a named template. The template may be referenced from
1076
+ # event handlers by passing the name as a Symbol to rendering
1077
+ # methods. The block is executed each time the template is rendered
1078
+ # and the resulting object is passed to the template handler.
1079
+ #
1080
+ # The following example defines a HAML template named hello and
1081
+ # invokes it from an event handler:
1082
+ #
1083
+ # template :hello do
1084
+ # "h1 Hello World!"
1085
+ # end
1086
+ #
1087
+ # get '/' do
1088
+ # haml :hello
1089
+ # end
1090
+ #
1091
+ def template(name, &b)
1092
+ templates[name] = b
1093
+ end
1094
+
1095
+ # Define a layout template.
1096
+ def layout(name=:layout, &b)
1097
+ template(name, &b)
1098
+ end
1099
+
1100
+ # Define a custom error handler for the exception class +type+. The block
1101
+ # is invoked when the specified exception type is raised from an error
1102
+ # handler and can manipulate the response as needed:
1103
+ #
1104
+ # error MyCustomError do
1105
+ # status 500
1106
+ # 'So what happened was...' + request.env['sinatra.error'].message
1107
+ # end
1108
+ #
1109
+ # The Sinatra::ServerError handler is used by default when an exception
1110
+ # occurs and no matching error handler is found.
1111
+ def error(type=ServerError, options = {}, &b)
1112
+ errors[type] = Error.new(type, options, &b)
1113
+ end
1114
+
1115
+ # Define a custom error handler for '404 Not Found' responses. This is a
1116
+ # shorthand for:
1117
+ # error NotFound do
1118
+ # ..
1119
+ # end
1120
+ def not_found(options={}, &b)
1121
+ error NotFound, options, &b
1122
+ end
1123
+
1124
+ # Define a request filter. When <tt>type</tt> is <tt>:before</tt>, execute the
1125
+ # block in the context of each request before matching event handlers.
1126
+ def filter(type, &b)
1127
+ filters[type] << b
1128
+ end
1129
+
1130
+ # Invoke the block in the context of each request before invoking
1131
+ # matching event handlers.
1132
+ def before(&b)
1133
+ filter :before, &b
1134
+ end
1135
+
1136
+ # True when environment is :development.
1137
+ def development? ; options.env == :development ; end
1138
+
1139
+ # True when environment is :test.
1140
+ def test? ; options.env == :test ; end
1141
+
1142
+ # True when environment is :production.
1143
+ def production? ; options.env == :production ; end
1144
+
1145
+ # Clear all events, templates, filters, and error handlers
1146
+ # and then reload the application source file. This occurs
1147
+ # automatically before each request is processed in development.
1148
+ def reload!
1149
+ clearables.each(&:clear)
1150
+ load_default_configuration!
1151
+ load_development_configuration! if development?
1152
+ @pipeline = nil
1153
+ @reloading = true
1154
+ Kernel.load options.app_file
1155
+ @reloading = false
1156
+ end
1157
+
1158
+ # Determine whether the application is in the process of being
1159
+ # reloaded.
1160
+ def reloading?
1161
+ @reloading == true
1162
+ end
1163
+
1164
+ # Mutex instance used for thread synchronization.
1165
+ def mutex
1166
+ @@mutex ||= Mutex.new
1167
+ end
1168
+
1169
+ # Yield to the block with thread synchronization
1170
+ def run_safely
1171
+ if development? || options.mutex
1172
+ mutex.synchronize { yield }
1173
+ else
1174
+ yield
1175
+ end
1176
+ end
1177
+
1178
+ # Add a piece of Rack middleware to the pipeline leading to the
1179
+ # application.
1180
+ def use(klass, *args, &block)
1181
+ fail "#{klass} must respond to 'new'" unless klass.respond_to?(:new)
1182
+ @pipeline = nil
1183
+ @middleware.push([ klass, args, block ]).last
1184
+ end
1185
+
1186
+ private
1187
+
1188
+ # Rack middleware derived from current state of application options.
1189
+ # These components are plumbed in at the very beginning of the
1190
+ # pipeline.
1191
+ def optional_middleware
1192
+ [
1193
+ ([ Rack::CommonLogger, [], nil ] if options.logging),
1194
+ ([ Rack::Session::Cookie, [], nil ] if options.sessions)
1195
+ ].compact
1196
+ end
1197
+
1198
+ # Rack middleware explicitly added to the application with #use. These
1199
+ # components are plumbed into the pipeline downstream from
1200
+ # #optional_middle.
1201
+ def explicit_middleware
1202
+ @middleware
1203
+ end
1204
+
1205
+ # All Rack middleware used to construct the pipeline.
1206
+ def middleware
1207
+ optional_middleware + explicit_middleware
1208
+ end
1209
+
1210
+ public
1211
+
1212
+ # An assembled pipeline of Rack middleware that leads eventually to
1213
+ # the Application#invoke method. The pipeline is built upon first
1214
+ # access. Defining new middleware with Application#use or manipulating
1215
+ # application options may cause the pipeline to be rebuilt.
1216
+ def pipeline
1217
+ @pipeline ||=
1218
+ middleware.inject(method(:dispatch)) do |app,(klass,args,block)|
1219
+ klass.new(app, *args, &block)
1220
+ end
1221
+ end
1222
+
1223
+ # Rack compatible request invocation interface.
1224
+ def call(env)
1225
+ run_safely do
1226
+ reload! if development? && (options.reload != false)
1227
+ pipeline.call(env)
1228
+ end
1229
+ end
1230
+
1231
+ # Request invocation handler - called at the end of the Rack pipeline
1232
+ # for each request.
1233
+ #
1234
+ # 1. Create Rack::Request, Rack::Response helper objects.
1235
+ # 2. Lookup event handler based on request method and path.
1236
+ # 3. Create new EventContext to house event handler evaluation.
1237
+ # 4. Invoke each #before filter in context of EventContext object.
1238
+ # 5. Invoke event handler in context of EventContext object.
1239
+ # 6. Return response to Rack.
1240
+ #
1241
+ # See the Rack specification for detailed information on the
1242
+ # +env+ argument and return value.
1243
+ def dispatch(env)
1244
+ request = Rack::Request.new(env)
1245
+ context = EventContext.new(request, Rack::Response.new([], 200), {})
1246
+ begin
1247
+ returned =
1248
+ catch(:halt) do
1249
+ filters[:before].each { |f| context.instance_eval(&f) }
1250
+ result = lookup(context.request)
1251
+ context.route_params = result.params
1252
+ context.response.status = result.status
1253
+ context.reset!
1254
+ [:complete, context.instance_eval(&result.block)]
1255
+ end
1256
+ body = returned.to_result(context)
1257
+ rescue => e
1258
+ msg = "#{e.class.name} - #{e.message}:"
1259
+ msg << "\n #{e.backtrace.join("\n ")}"
1260
+ request.env['rack.errors'] << msg
1261
+
1262
+ request.env['sinatra.error'] = e
1263
+ context.status(500)
1264
+ raise if options.raise_errors && e.class != NotFound
1265
+ result = (errors[e.class] || errors[ServerError]).invoke(request)
1266
+ returned =
1267
+ catch(:halt) do
1268
+ [:complete, context.instance_eval(&result.block)]
1269
+ end
1270
+ body = returned.to_result(context)
1271
+ end
1272
+ body = '' unless body.respond_to?(:each)
1273
+ body = '' if request.env["REQUEST_METHOD"].upcase == 'HEAD'
1274
+ context.body = body.kind_of?(String) ? [*body] : body
1275
+ context.finish
1276
+ end
1277
+
1278
+ private
1279
+
1280
+ # Called immediately after the application is initialized or reloaded to
1281
+ # register default events, templates, and error handlers.
1282
+ def load_default_configuration!
1283
+ events[:get] << Static.new(self)
1284
+ configure do
1285
+ error do
1286
+ '<h1>Internal Server Error</h1>'
1287
+ end
1288
+ not_found { '<h1>Not Found</h1>'}
1289
+ end
1290
+ end
1291
+
1292
+ # Called before reloading to perform development specific configuration.
1293
+ def load_development_configuration!
1294
+ get '/sinatra_custom_images/:image.png' do
1295
+ content_type :png
1296
+ File.read(File.dirname(__FILE__) + "/../images/#{params[:image]}.png")
1297
+ end
1298
+
1299
+ not_found do
1300
+ (<<-HTML).gsub(/^ {8}/, '')
1301
+ <!DOCTYPE html>
1302
+ <html>
1303
+ <head>
1304
+ <style type="text/css">
1305
+ body {text-align:center;color:#888;font-family:arial;font-size:22px;margin:20px;}
1306
+ #content {margin:0 auto;width:500px;text-align:left}
1307
+ </style>
1308
+ </head>
1309
+ <body>
1310
+ <h2>Sinatra doesn't know this ditty.</h2>
1311
+ <img src='/sinatra_custom_images/404.png'>
1312
+ <div id="content">
1313
+ Try this:
1314
+ <pre>#{request.request_method.downcase} "#{request.path_info}" do\n .. do something ..\nend<pre>
1315
+ </div>
1316
+ </body>
1317
+ </html>
1318
+ HTML
1319
+ end
1320
+
1321
+ error do
1322
+ @error = request.env['sinatra.error']
1323
+ (<<-HTML).gsub(/^ {8}/, '')
1324
+ <!DOCTYPE html>
1325
+ <html>
1326
+ <head>
1327
+ <style type="text/css" media="screen">
1328
+ body {font-family:verdana;color:#333}
1329
+ #content {width:700px;margin-left:20px}
1330
+ #content h1 {width:99%;color:#1D6B8D;font-weight:bold}
1331
+ #stacktrace {margin-top:-20px}
1332
+ #stacktrace pre {font-size:12px;border-left:2px solid #ddd;padding-left:10px}
1333
+ #stacktrace img {margin-top:10px}
1334
+ </style>
1335
+ </head>
1336
+ <body>
1337
+ <div id="content">
1338
+ <img src="/sinatra_custom_images/500.png">
1339
+ <div class="info">
1340
+ Params: <pre>#{params.inspect}</pre>
1341
+ </div>
1342
+ <div id="stacktrace">
1343
+ <h1>#{escape_html(@error.class.name + ' - ' + @error.message.to_s)}</h1>
1344
+ <pre><code>#{escape_html(@error.backtrace.join("\n"))}</code></pre>
1345
+ </div>
1346
+ </div>
1347
+ </body>
1348
+ </html>
1349
+ HTML
1350
+ end
1351
+ end
1352
+
1353
+ end
1354
+
1355
+ end
1356
+
1357
+ # Delegate DSLish methods to the currently active Sinatra::Application
1358
+ # instance.
1359
+ Sinatra::Application::FORWARD_METHODS.each do |method|
1360
+ eval(<<-EOS, binding, '(__DSL__)', 1)
1361
+ def #{method}(*args, &b)
1362
+ Sinatra.application.#{method}(*args, &b)
1363
+ end
1364
+ EOS
1365
+ end
1366
+
1367
+ def helpers(&b)
1368
+ Sinatra::EventContext.class_eval(&b)
1369
+ end
1370
+
1371
+ def use_in_file_templates!
1372
+ require 'stringio'
1373
+ templates = IO.read(caller.first.split(':').first).split('__END__').last
1374
+ data = StringIO.new(templates)
1375
+ current_template = nil
1376
+ data.each do |line|
1377
+ if line =~ /^@@\s?(.*)/
1378
+ current_template = $1.to_sym
1379
+ Sinatra.application.templates[current_template] = ''
1380
+ elsif current_template
1381
+ Sinatra.application.templates[current_template] << line
1382
+ end
1383
+ end
1384
+ end
1385
+
1386
+ def mime(ext, type)
1387
+ Rack::File::MIME_TYPES[ext.to_s] = type
1388
+ end
1389
+
1390
+ ### Misc Core Extensions
1391
+
1392
+ module Kernel
1393
+ def silence_warnings
1394
+ old_verbose, $VERBOSE = $VERBOSE, nil
1395
+ yield
1396
+ ensure
1397
+ $VERBOSE = old_verbose
1398
+ end
1399
+ end
1400
+
1401
+ class Symbol
1402
+ def to_proc
1403
+ Proc.new { |*args| args.shift.__send__(self, *args) }
1404
+ end
1405
+ end
1406
+
1407
+ class Array
1408
+ def to_hash
1409
+ self.inject({}) { |h, (k, v)| h[k] = v; h }
1410
+ end
1411
+ def to_proc
1412
+ Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
1413
+ end
1414
+ end
1415
+
1416
+ module Enumerable
1417
+ def eject(&block)
1418
+ find { |e| result = block[e] and break result }
1419
+ end
1420
+ end
1421
+
1422
+ ### Core Extension results for throw :halt
1423
+
1424
+ class Proc
1425
+ def to_result(cx, *args)
1426
+ cx.instance_eval(&self)
1427
+ args.shift.to_result(cx, *args)
1428
+ end
1429
+ end
1430
+
1431
+ class String
1432
+ def to_result(cx, *args)
1433
+ args.shift.to_result(cx, *args)
1434
+ self
1435
+ end
1436
+ end
1437
+
1438
+ class Array
1439
+ def to_result(cx, *args)
1440
+ self.shift.to_result(cx, *self)
1441
+ end
1442
+ end
1443
+
1444
+ class Symbol
1445
+ def to_result(cx, *args)
1446
+ cx.send(self, *args)
1447
+ end
1448
+ end
1449
+
1450
+ class Fixnum
1451
+ def to_result(cx, *args)
1452
+ cx.status self
1453
+ args.shift.to_result(cx, *args)
1454
+ end
1455
+ end
1456
+
1457
+ class NilClass
1458
+ def to_result(cx, *args)
1459
+ ''
1460
+ end
1461
+ end
1462
+
1463
+ at_exit do
1464
+ raise $! if $!
1465
+ Sinatra.run if Sinatra.application.options.run
1466
+ end
1467
+
1468
+ mime :xml, 'application/xml'
1469
+ mime :js, 'application/javascript'
1470
+ mime :png, 'image/png'