9e94ef611c690b839c873d3cf32309b7df595482
[sinatra.git] / lib / sinatra.rb
blob9e94ef611c690b839c873d3cf32309b7df595482
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     
74     attr_reader :path, :block, :param_keys, :pattern
75     
76     def initialize(path, &b)
77       @path = path
78       @block = b
79       @param_keys = []
80       regex = @path.to_s.gsub(PARAM) do
81         @param_keys << $1.intern
82         "(#{URI_CHAR}+)"
83       end
84       @pattern = /^#{regex}$/
85     end
86         
87     def invoke(env)
88       return unless pattern =~ env['PATH_INFO'].squeeze('/')
89       params = param_keys.zip($~.captures.map(&:from_param)).to_hash
90       Result.new(block, params, 200)
91     end
92     
93   end
94   
95   class Error
96     
97     attr_reader :code, :block
98     
99     def initialize(code, &b)
100       @code, @block = code, b
101     end
102     
103     def invoke(env)
104       Result.new(block, {}, 404)
105     end
106     
107   end
108   
109   class Static
110             
111     def invoke(env)
112       return unless File.file?(
113         Sinatra.application.options.public + env['PATH_INFO']
114       )
115       Result.new(block, {}, 200)
116     end
117     
118     def block
119       Proc.new do
120         send_file Sinatra.application.options.public + 
121           request.env['PATH_INFO']
122       end
123     end
124     
125   end
126   
127   module ResponseHelpers
129     def redirect(path)
130       throw :halt, Redirect.new(path)
131     end
132     
133     def send_file(filename)
134       throw :halt, SendFile.new(filename)
135     end
137   end
138   
139   module RenderingHelpers
140     
141     def render(content, options={})
142       template = resolve_template(content, options)
143       @content = _evaluate_render(template)
144       layout = resolve_layout(options[:layout], options)
145       @content = _evaluate_render(layout) if layout
146       @content
147     end
148     
149     private
150       
151       def _evaluate_render(content, options={})
152         case content
153         when String
154           instance_eval(%Q{"#{content}"})
155         when Proc
156           instance_eval(&content)
157         when File
158           instance_eval(%Q{"#{content.read}"})
159         end
160       end
161       
162       def resolve_template(content, options={})
163         case content
164         when String
165           content
166         when Symbol
167           File.new(filename_for(content, options))
168         end
169       end
170     
171       def resolve_layout(name, options={})
172         return if name == false
173         if layout = layouts[name || :layout]
174           return layout
175         end
176         if File.file?(filename = filename_for(name, options))
177           File.new(filename)
178         end
179       end
180       
181       def filename_for(name, options={})
182         (options[:views_directory] || 'views') + "/#{name}.#{ext}"
183       end
184               
185       def ext
186         :html
187       end
189       def layouts
190         Sinatra.application.layouts
191       end
192     
193   end
195   class EventContext
196     
197     include ResponseHelpers
198     include RenderingHelpers
199     
200     attr_accessor :request, :response
201     
202     dslify_writter :status, :body
203     
204     def initialize(request, response, route_params)
205       @request = request
206       @response = response
207       @route_params = route_params
208       @response.body = nil
209     end
210     
211     def params
212       @params ||= @route_params.merge(@request.params).symbolize_keys
213     end
214     
215     def stop(content)
216       throw :halt, content
217     end
218     
219     def complete(returned)
220       @response.body || returned
221     end
222     
223     private
225       def method_missing(name, *args, &b)
226         @response.send(name, *args, &b)
227       end
228     
229   end
230   
231   class Redirect
232     def initialize(path)
233       @path = path
234     end
235     
236     def to_result(cx, *args)
237       cx.status(302)
238       cx.header.merge!('Location' => @path)
239       cx.body = ''
240     end
241   end
242     
243   class SendFile
244     def initialize(filename)
245       @filename = filename
246     end
247     
248     def to_result(cx, *args)
249       cx.body = File.read(@filename)
250     end
251   end
252     
253   class Application
254     
255     attr_reader :events, :layouts, :default_options
256     
257     def self.default_options
258       @@default_options ||= {
259         :run => true,
260         :port => 4567,
261         :env => :development,
262         :root => Dir.pwd,
263         :public => Dir.pwd + '/public'
264       }
265     end
266     
267     def default_options
268       self.class.default_options
269     end
270         
271     def initialize
272       @events = Hash.new { |hash, key| hash[key] = [] }
273       @layouts = Hash.new
274     end
275     
276     def define_event(method, path, &b)
277       events[method] << event = Event.new(path, &b)
278       event
279     end
280     
281     def define_layout(name=:layout, &b)
282       layouts[name] = b
283     end
284     
285     def define_error(code, &b)
286       events[:errors][code] = Error.new(code, &b)
287     end
288     
289     def static
290       @static ||= Static.new
291     end
292     
293     def lookup(env)
294       method = env['REQUEST_METHOD'].downcase.to_sym
295       e = static.invoke(env) 
296       e ||= events[method].eject(&[:invoke, env])
297       e ||= (events[:errors][404] || basic_not_found).invoke(env)
298       e
299     end
300     
301     def basic_not_found
302       Error.new(404) do
303         '<h1>Not Found</h1>'
304       end
305     end
306     
307     def basic_error
308       Error.new(500) do
309         '<h1>Internal Server Error</h1>'
310       end
311     end
313     def options
314       @options ||= OpenStruct.new(default_options)
315     end
316     
317     def call(env)
318       result = lookup(env)
319       context = EventContext.new(
320         Rack::Request.new(env), 
321         Rack::Response.new,
322         result.params
323       )
324       body = begin
325         context.status(result.status)
326         returned = catch(:halt) do
327           [:complete, context.instance_eval(&result.block)]
328         end
329         body = returned.to_result(context)
330         context.body = String === body ? [*body] : body
331         context.finish
332       rescue => e
333         raise e if options.raise_errors
334         env['sinatra.error'] = e
335         result = (events[:errors][500] || basic_error).invoke(env)
336         returned = catch(:halt) do
337           [:complete, context.instance_eval(&result.block)]
338         end
339         body = returned.to_result(context)
340         context.status(500)
341         context.body = String === body ? [*body] : body
342         context.finish
343       end
344     end
345     
346   end
347   
350 def get(path, &b)
351   Sinatra.application.define_event(:get, path, &b)
354 def post(path, &b)
355   Sinatra.application.define_event(:post, path, &b)
358 def put(path, &b)
359   Sinatra.application.define_event(:put, path, &b)
362 def delete(path, &b)
363   Sinatra.application.define_event(:delete, path, &b)
366 def helpers(&b)
367   Sinatra::EventContext.class_eval(&b)
370 def error(code, &b)
371   Sinatra.application.define_error(code, &b)
374 def layout(name = :layout, &b)
375   Sinatra.application.define_layout(name, &b)
378 def configures(*envs, &b)
379   yield if  envs.include?(Sinatra.application.options.env) ||
380             envs.empty?
382 alias :configure :configures
384 ### Misc Core Extensions
386 module Kernel
388   def silence_warnings
389     old_verbose, $VERBOSE = $VERBOSE, nil
390     yield
391   ensure
392     $VERBOSE = old_verbose
393   end
397 class String
399   # Converts +self+ to an escaped URI parameter value
400   #   'Foo Bar'.to_param # => 'Foo%20Bar'
401   def to_param
402     URI.escape(self)
403   end
404   
405   # Converts +self+ from an escaped URI parameter value
406   #   'Foo%20Bar'.from_param # => 'Foo Bar'
407   def from_param
408     URI.unescape(self)
409   end
410   
413 class Hash
414   
415   def to_params
416     map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
417   end
418   
419   def symbolize_keys
420     self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
421   end
422   
423   def pass(*keys)
424     reject { |k,v| !keys.include?(k) }
425   end
426   
429 class Symbol
430   
431   def to_proc 
432     Proc.new { |*args| args.shift.__send__(self, *args) }
433   end
434   
437 class Array
438   
439   def to_hash
440     self.inject({}) { |h, (k, v)|  h[k] = v; h }
441   end
442   
443   def to_proc
444     Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
445   end
446   
449 module Enumerable
450   
451   def eject(&block)
452     find { |e| result = block[e] and break result }
453   end
454   
457 ### Core Extension results for throw :halt
459 class Proc
460   def to_result(cx, *args)
461     cx.instance_eval(&self)
462   end
465 class String
466   def to_result(cx, *args)
467     cx.body = self
468   end
471 class Array
472   def to_result(cx, *args)
473     self.shift.to_result(cx, *self)
474   end
477 class Symbol
478   def to_result(cx, *args)
479     cx.send(self, *args)
480   end
483 class Fixnum
484   def to_result(cx, *args)
485     cx.status self
486     cx.body args.first
487   end
490 class NilClass
491   def to_result(cx, *args)
492     cx.body = ''
493     # log warning here
494   end
497 at_exit do
498   raise $! if $!
499   Sinatra.run if Sinatra.application.options.run
502 ENV['SINATRA_ENV'] = 'test' if $0 =~ /_test\.rb$/
503 Sinatra::Application.default_options.merge!(
504   :env => (ENV['SINATRA_ENV'] || 'development').to_sym
507 configures :development do
508   
509   get '/sinatra_custom_images/:image.png' do
510     File.read(File.dirname(__FILE__) + "/../images/#{params[:image]}.png")
511   end
512   
513   error 404 do
514     %Q(
515     <html>
516       <body style='text-align: center; color: #888; font-family: Arial; font-size: 22px; margin: 20px'>
517       <h2>Sinatra doesn't know this diddy.</h2>
518       <img src='/sinatra_custom_images/404.png'></img>
519       </body>
520     </html>
521     )
522   end
523   
524   error 500 do
525     @error = request.env['sinatra.error']
526     %Q(
527     <html>
528         <body>
529                 <style type="text/css" media="screen">
530                         body {
531                                 font-family: Verdana;
532                                 color: #333;
533                         }
535                         #content {
536                                 width: 700px;
537                                 margin-left: 20px;
538                         }
540                         #content h1 {
541                                 width: 99%;
542                                 color: #1D6B8D;
543                                 font-weight: bold;
544                         }
545                         
546                         #stacktrace {
547                           margin-top: -20px;
548                         }
550                         #stacktrace pre {
551                                 font-size: 12px;
552                                 border-left: 2px solid #ddd;
553                                 padding-left: 10px;
554                         }
556                         #stacktrace img {
557                                 margin-top: 10px;
558                         }
559                 </style>
560                 <div id="content">
561                 <img src="/sinatra_custom_images/500.png" />
562                         <div id="stacktrace">
563                                 <h1>#{@error.message}</h1>
564                                 <pre><code>#{@error.backtrace.join("\n")}</code></pre>
565                 </div>
566         </body>
567     </html>
568     )
569   end
570