simple options
[sinatra.git] / lib / sinatra.rb
blobf20fc0fe24818b82519afbcbd8ab358d9f820686
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
264     
265     def self.default_options
266       @@default_options ||= {
267         :run => true,
268         :port => 4567,
269         :env => :development,
270         :root => Dir.pwd,
271         :public => Dir.pwd + '/public'
272       }
273     end
274     
275     def default_options
276       self.class.default_options
277     end
279     def load_options!
280       require 'optparse'
281       OptionParser.new do |op|
282         op.on('-p port') { |port| default_options[:port] = port }
283         op.on('-e env') { |env| default_options[:env] = env }
284       end.parse!(ARGV.dup)
285     end
286         
287     def initialize
288       @events = Hash.new { |hash, key| hash[key] = [] }
289       @layouts = Hash.new
290       load_options!
291     end
292     
293     def define_event(method, path, options = {}, &b)
294       events[method] << event = Event.new(path, options, &b)
295       event
296     end
297     
298     def define_layout(name=:layout, &b)
299       layouts[name] = b
300     end
301     
302     def define_error(code, options = {}, &b)
303       events[:errors][code] = Error.new(code, &b)
304     end
305     
306     def static
307       @static ||= Static.new
308     end
309     
310     def lookup(env)
311       method = env['REQUEST_METHOD'].downcase.to_sym
312       e = static.invoke(env) 
313       e ||= events[method].eject(&[:invoke, env])
314       e ||= (events[:errors][404] || basic_not_found).invoke(env)
315       e
316     end
317     
318     def basic_not_found
319       Error.new(404) do
320         '<h1>Not Found</h1>'
321       end
322     end
323     
324     def basic_error
325       Error.new(500) do
326         '<h1>Internal Server Error</h1>'
327       end
328     end
330     def options
331       @options ||= OpenStruct.new(default_options)
332     end
333     
334     def call(env)
335       result = lookup(env)
336       context = EventContext.new(
337         Rack::Request.new(env), 
338         Rack::Response.new,
339         result.params
340       )
341       begin
342         context.status(result.status)
343         returned = catch(:halt) do
344           [:complete, context.instance_eval(&result.block)]
345         end
346         body = returned.to_result(context)
347         context.body = String === body ? [*body] : body
348         context.finish
349       rescue => e
350         raise e if options.raise_errors
351         env['sinatra.error'] = e
352         result = (events[:errors][500] || basic_error).invoke(env)
353         returned = catch(:halt) do
354           [:complete, context.instance_eval(&result.block)]
355         end
356         body = returned.to_result(context)
357         context.status(500)
358         context.body = String === body ? [*body] : body
359         context.finish
360       end
361     end
362     
363   end
364   
367 def get(path, options ={}, &b)
368   Sinatra.application.define_event(:get, path, options, &b)
371 def post(path, options ={}, &b)
372   Sinatra.application.define_event(:post, path, options, &b)
375 def put(path, options ={}, &b)
376   Sinatra.application.define_event(:put, path, options, &b)
379 def delete(path, options ={}, &b)
380   Sinatra.application.define_event(:delete, path, options, &b)
383 def helpers(&b)
384   Sinatra::EventContext.class_eval(&b)
387 def error(code, options = {}, &b)
388   Sinatra.application.define_error(code, options, &b)
391 def layout(name = :layout, &b)
392   Sinatra.application.define_layout(name, &b)
395 def configures(*envs, &b)
396   yield if  envs.include?(Sinatra.application.options.env) ||
397             envs.empty?
399 alias :configure :configures
401 ### Misc Core Extensions
403 module Kernel
405   def silence_warnings
406     old_verbose, $VERBOSE = $VERBOSE, nil
407     yield
408   ensure
409     $VERBOSE = old_verbose
410   end
414 class String
416   # Converts +self+ to an escaped URI parameter value
417   #   'Foo Bar'.to_param # => 'Foo%20Bar'
418   def to_param
419     URI.escape(self)
420   end
421   
422   # Converts +self+ from an escaped URI parameter value
423   #   'Foo%20Bar'.from_param # => 'Foo Bar'
424   def from_param
425     URI.unescape(self)
426   end
427   
430 class Hash
431   
432   def to_params
433     map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
434   end
435   
436   def symbolize_keys
437     self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
438   end
439   
440   def pass(*keys)
441     reject { |k,v| !keys.include?(k) }
442   end
443   
446 class Symbol
447   
448   def to_proc 
449     Proc.new { |*args| args.shift.__send__(self, *args) }
450   end
451   
454 class Array
455   
456   def to_hash
457     self.inject({}) { |h, (k, v)|  h[k] = v; h }
458   end
459   
460   def to_proc
461     Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
462   end
463   
466 module Enumerable
467   
468   def eject(&block)
469     find { |e| result = block[e] and break result }
470   end
471   
474 ### Core Extension results for throw :halt
476 class Proc
477   def to_result(cx, *args)
478     cx.instance_eval(&self)
479   end
482 class String
483   def to_result(cx, *args)
484     cx.body = self
485   end
488 class Array
489   def to_result(cx, *args)
490     self.shift.to_result(cx, *self)
491   end
494 class Symbol
495   def to_result(cx, *args)
496     cx.send(self, *args)
497   end
500 class Fixnum
501   def to_result(cx, *args)
502     cx.status self
503     cx.body args.first
504   end
507 class NilClass
508   def to_result(cx, *args)
509     cx.body = ''
510     # log warning here
511   end
514 at_exit do
515   raise $! if $!
516   Sinatra.run if Sinatra.application.options.run
519 ENV['SINATRA_ENV'] = 'test' if $0 =~ /_test\.rb$/
520 Sinatra::Application.default_options.merge!(
521   :env => (ENV['SINATRA_ENV'] || 'development').to_sym
524 configures :development do
525   
526   get '/sinatra_custom_images/:image.png' do
527     File.read(File.dirname(__FILE__) + "/../images/#{params[:image]}.png")
528   end
529   
530   error 404 do
531     %Q(
532     <html>
533       <body style='text-align: center; color: #888; font-family: Arial; font-size: 22px; margin: 20px'>
534       <h2>Sinatra doesn't know this diddy.</h2>
535       <img src='/sinatra_custom_images/404.png'></img>
536       </body>
537     </html>
538     )
539   end
540   
541   error 500 do
542     @error = request.env['sinatra.error']
543     %Q(
544     <html>
545         <body>
546                 <style type="text/css" media="screen">
547                         body {
548                                 font-family: Verdana;
549                                 color: #333;
550                         }
552                         #content {
553                                 width: 700px;
554                                 margin-left: 20px;
555                         }
557                         #content h1 {
558                                 width: 99%;
559                                 color: #1D6B8D;
560                                 font-weight: bold;
561                         }
562                         
563                         #stacktrace {
564                           margin-top: -20px;
565                         }
567                         #stacktrace pre {
568                                 font-size: 12px;
569                                 border-left: 2px solid #ddd;
570                                 padding-left: 10px;
571                         }
573                         #stacktrace img {
574                                 margin-top: 10px;
575                         }
576                 </style>
577                 <div id="content">
578                 <img src="/sinatra_custom_images/500.png" />
579                         <div id="stacktrace">
580                                 <h1>#{@error.message}</h1>
581                                 <pre><code>#{@error.backtrace.join("\n")}</code></pre>
582                 </div>
583         </body>
584     </html>
585     )
586   end
587