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