Errors when testing
[sinatra.git] / lib / sinatra.rb
blobe2bf3c83f43396c544bd0a671544ea4fc0506a66
1 require 'rubygems'
2 require 'metaid'
4 if ENV['SWIFT']
5  require 'swiftcore/swiftiplied_mongrel'
6  puts "Using Swiftiplied Mongrel"
7 elsif ENV['EVENT']
8   require 'swiftcore/evented_mongrel' 
9   puts "Using Evented Mongrel"
10 end
12 require 'rack'
13 require 'ostruct'
15 class Class
16   def dslify_writter(*syms)
17     syms.each do |sym|
18       class_eval <<-end_eval
19         def #{sym}(v=nil)
20           self.send "#{sym}=", v if v
21           v
22         end
23       end_eval
24     end
25   end
26 end
28 module Sinatra
29   extend self
31   Result = Struct.new(:block, :params, :status) unless defined?(Result)
32   
33   def application
34     @app ||= Application.new
35   end
36   
37   def application=(app)
38     @app = app
39   end
40   
41   def port
42     application.options.port
43   end
44   
45   def env
46     application.options.env
47   end
48   
49   def build_application
50     Rack::CommonLogger.new(application)
51   end
52   
53   def run
54     
55     begin
56       puts "== Sinatra has taken the stage on port #{port} for #{env}"
57       require 'pp'
58       Rack::Handler::Mongrel.run(build_application, :Port => port) do |server|
59         trap(:INT) do
60           server.stop
61           puts "\n== Sinatra has ended his set (crowd applauds)"
62         end
63       end
64     rescue Errno::EADDRINUSE => e
65       puts "== Someone is already performing on port #{port}!"
66     end
67     
68   end
69       
70   class Event
72     URI_CHAR = '[^/?:,&#\.]'.freeze unless defined?(URI_CHAR)
73     PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
74     SPLAT = /(.*?)/
75     attr_reader :path, :block, :param_keys, :pattern, :options
76     
77     def initialize(path, options = {}, &b)
78       @path = path
79       @block = b
80       @param_keys = []
81       @options = options
82       regex = @path.to_s.gsub(PARAM) do
83         @param_keys << $1.intern
84         "(#{URI_CHAR}+)"
85       end
86       
87       regex.gsub!('*', SPLAT.to_s)
88       
89       @pattern = /^#{regex}$/
90     end
91         
92     def invoke(env)
93       if options[:agent] 
94         return unless env['HTTP_USER_AGENT'] =~ options[:agent]
95       end
96       return unless pattern =~ env['PATH_INFO'].squeeze('/')
97       params = param_keys.zip($~.captures.map(&:from_param)).to_hash
98       Result.new(block, params, 200)
99     end
100     
101   end
102   
103   class Error
104     
105     attr_reader :code, :block
106     
107     def initialize(code, &b)
108       @code, @block = code, b
109     end
110     
111     def invoke(env)
112       Result.new(block, {}, 404)
113     end
114     
115   end
116   
117   class Static
118             
119     def invoke(env)
120       return unless File.file?(
121         Sinatra.application.options.public + env['PATH_INFO']
122       )
123       Result.new(block, {}, 200)
124     end
125     
126     def block
127       Proc.new do
128         send_file Sinatra.application.options.public + 
129           request.env['PATH_INFO']
130       end
131     end
132     
133   end
134   
135   module ResponseHelpers
137     def redirect(path)
138       throw :halt, Redirect.new(path)
139     end
140     
141     def send_file(filename)
142       throw :halt, SendFile.new(filename)
143     end
145   end
146   
147   module RenderingHelpers
148     
149     def render(content, options={})
150       template = resolve_template(content, options)
151       @content = _evaluate_render(template)
152       layout = resolve_layout(options[:layout], options)
153       @content = _evaluate_render(layout) if layout
154       @content
155     end
156     
157     private
158       
159       def _evaluate_render(content, options={})
160         case content
161         when String
162           instance_eval(%Q{"#{content}"})
163         when Proc
164           instance_eval(&content)
165         when File
166           instance_eval(%Q{"#{content.read}"})
167         end
168       end
169       
170       def resolve_template(content, options={})
171         case content
172         when String
173           content
174         when Symbol
175           File.new(filename_for(content, options))
176         end
177       end
178     
179       def resolve_layout(name, options={})
180         return if name == false
181         if layout = layouts[name || :layout]
182           return layout
183         end
184         if File.file?(filename = filename_for(name, options))
185           File.new(filename)
186         end
187       end
188       
189       def filename_for(name, options={})
190         (options[:views_directory] || 'views') + "/#{name}.#{ext}"
191       end
192               
193       def ext
194         :html
195       end
197       def layouts
198         Sinatra.application.layouts
199       end
200     
201   end
203   class EventContext
204     
205     include ResponseHelpers
206     include RenderingHelpers
207     
208     attr_accessor :request, :response
209     
210     dslify_writter :status, :body
211     
212     def initialize(request, response, route_params)
213       @request = request
214       @response = response
215       @route_params = route_params
216       @response.body = nil
217     end
218     
219     def params
220       @params ||= @route_params.merge(@request.params).symbolize_keys
221     end
222     
223     def stop(content)
224       throw :halt, content
225     end
226     
227     def complete(returned)
228       @response.body || returned
229     end
230     
231     private
233       def method_missing(name, *args, &b)
234         @response.send(name, *args, &b)
235       end
236     
237   end
238   
239   class Redirect
240     def initialize(path)
241       @path = path
242     end
243     
244     def to_result(cx, *args)
245       cx.status(302)
246       cx.header.merge!('Location' => @path)
247       cx.body = ''
248     end
249   end
250     
251   class SendFile
252     def initialize(filename)
253       @filename = filename
254     end
255     
256     def to_result(cx, *args)
257       cx.body = File.read(@filename)
258     end
259   end
260     
261   class Application
262     
263     attr_reader :events, :layouts, :default_options, :filters
264     attr_writer :options
265     
266     def self.default_options
267       @@default_options ||= {
268         :run => true,
269         :port => 4567,
270         :env => :development,
271         :root => Dir.pwd,
272         :public => Dir.pwd + '/public'
273       }
274     end
275     
276     def default_options
277       self.class.default_options
278     end
280     def load_options!
281       require 'optparse'
282       OptionParser.new do |op|
283         op.on('-p port') { |port| default_options[:port] = port }
284         op.on('-e env') { |env| default_options[:env] = env }
285       end.parse!(ARGV.dup)
286     end
287         
288     def initialize
289       @events = Hash.new { |hash, key| hash[key] = [] }
290       @filters = Hash.new { |hash, key| hash[key] = [] }
291       @layouts = Hash.new
292       load_options!
293     end
294     
295     def define_event(method, path, options = {}, &b)
296       events[method] << event = Event.new(path, options, &b)
297       event
298     end
299     
300     def define_layout(name=:layout, &b)
301       layouts[name] = b
302     end
303     
304     def define_error(code, options = {}, &b)
305       events[:errors][code] = Error.new(code, &b)
306     end
307     
308     def define_filter(type, &b)
309       filters[:before] << b
310     end
311     
312     def static
313       @static ||= Static.new
314     end
315     
316     def lookup(env)
317       method = env['REQUEST_METHOD'].downcase.to_sym
318       e = static.invoke(env) 
319       e ||= events[method].eject(&[:invoke, env])
320       e ||= (events[:errors][404] || basic_not_found).invoke(env)
321       e
322     end
323     
324     def basic_not_found
325       Error.new(404) do
326         '<h1>Not Found</h1>'
327       end
328     end
329     
330     def basic_error
331       Error.new(500) do
332         '<h1>Internal Server Error</h1>'
333       end
334     end
336     def options
337       @options ||= OpenStruct.new(default_options)
338     end
339         
340     def call(env)
341       result = lookup(env)
342       context = EventContext.new(
343         Rack::Request.new(env), 
344         Rack::Response.new,
345         result.params
346       )
347       begin
348         context.status(result.status)
349         returned = catch(:halt) do
350           filters[:before].each { |f| context.instance_eval(&f) }
351           [:complete, context.instance_eval(&result.block)]
352         end
353         body = returned.to_result(context)
354         context.body = String === body ? [*body] : body
355         context.finish
356       rescue => e
357         raise e if options.raise_errors
358         env['sinatra.error'] = e
359         result = (events[:errors][500] || basic_error).invoke(env)
360         returned = catch(:halt) do
361           [:complete, context.instance_eval(&result.block)]
362         end
363         body = returned.to_result(context)
364         context.status(500)
365         context.body = String === body ? [*body] : body
366         context.finish
367       end
368     end
369     
370   end
371   
374 def get(path, options ={}, &b)
375   Sinatra.application.define_event(:get, path, options, &b)
378 def post(path, options ={}, &b)
379   Sinatra.application.define_event(:post, path, options, &b)
382 def put(path, options ={}, &b)
383   Sinatra.application.define_event(:put, path, options, &b)
386 def delete(path, options ={}, &b)
387   Sinatra.application.define_event(:delete, path, options, &b)
390 def before(&b)
391   Sinatra.application.define_filter(:before, &b)
394 def helpers(&b)
395   Sinatra::EventContext.class_eval(&b)
398 def error(code, options = {}, &b)
399   Sinatra.application.define_error(code, options, &b)
402 def layout(name = :layout, &b)
403   Sinatra.application.define_layout(name, &b)
406 def configures(*envs, &b)
407   yield if  envs.include?(Sinatra.application.options.env) ||
408             envs.empty?
410 alias :configure :configures
412 ### Misc Core Extensions
414 module Kernel
416   def silence_warnings
417     old_verbose, $VERBOSE = $VERBOSE, nil
418     yield
419   ensure
420     $VERBOSE = old_verbose
421   end
425 class String
427   # Converts +self+ to an escaped URI parameter value
428   #   'Foo Bar'.to_param # => 'Foo%20Bar'
429   def to_param
430     URI.escape(self)
431   end
432   
433   # Converts +self+ from an escaped URI parameter value
434   #   'Foo%20Bar'.from_param # => 'Foo Bar'
435   def from_param
436     URI.unescape(self)
437   end
438   
441 class Hash
442   
443   def to_params
444     map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
445   end
446   
447   def symbolize_keys
448     self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
449   end
450   
451   def pass(*keys)
452     reject { |k,v| !keys.include?(k) }
453   end
454   
457 class Symbol
458   
459   def to_proc 
460     Proc.new { |*args| args.shift.__send__(self, *args) }
461   end
462   
465 class Array
466   
467   def to_hash
468     self.inject({}) { |h, (k, v)|  h[k] = v; h }
469   end
470   
471   def to_proc
472     Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
473   end
474   
477 module Enumerable
478   
479   def eject(&block)
480     find { |e| result = block[e] and break result }
481   end
482   
485 ### Core Extension results for throw :halt
487 class Proc
488   def to_result(cx, *args)
489     cx.instance_eval(&self)
490   end
493 class String
494   def to_result(cx, *args)
495     cx.body = self
496   end
499 class Array
500   def to_result(cx, *args)
501     self.shift.to_result(cx, *self)
502   end
505 class Symbol
506   def to_result(cx, *args)
507     cx.send(self, *args)
508   end
511 class Fixnum
512   def to_result(cx, *args)
513     cx.status self
514     cx.body args.first
515   end
518 class NilClass
519   def to_result(cx, *args)
520     cx.body = ''
521     # log warning here
522   end
525 at_exit do
526   raise $! if $!
527   Sinatra.run if Sinatra.application.options.run
530 configures :development do
531   
532   get '/sinatra_custom_images/:image.png' do
533     File.read(File.dirname(__FILE__) + "/../images/#{params[:image]}.png")
534   end
535   
536   error 404 do
537     %Q(
538     <html>
539       <body style='text-align: center; color: #888; font-family: Arial; font-size: 22px; margin: 20px'>
540       <h2>Sinatra doesn't know this diddy.</h2>
541       <img src='/sinatra_custom_images/404.png'></img>
542       </body>
543     </html>
544     )
545   end
546   
547   error 500 do
548     @error = request.env['sinatra.error']
549     %Q(
550     <html>
551         <body>
552                 <style type="text/css" media="screen">
553                         body {
554                                 font-family: Verdana;
555                                 color: #333;
556                         }
558                         #content {
559                                 width: 700px;
560                                 margin-left: 20px;
561                         }
563                         #content h1 {
564                                 width: 99%;
565                                 color: #1D6B8D;
566                                 font-weight: bold;
567                         }
568                         
569                         #stacktrace {
570                           margin-top: -20px;
571                         }
573                         #stacktrace pre {
574                                 font-size: 12px;
575                                 border-left: 2px solid #ddd;
576                                 padding-left: 10px;
577                         }
579                         #stacktrace img {
580                                 margin-top: 10px;
581                         }
582                 </style>
583                 <div id="content">
584                 <img src="/sinatra_custom_images/500.png" />
585                         <div id="stacktrace">
586                                 <h1>#{@error.message}</h1>
587                                 <pre><code>#{@error.backtrace.join("\n")}</code></pre>
588                 </div>
589         </body>
590     </html>
591     )
592   end
593