static files
[sinatra.git] / lib / sinatra.rb
blobd377a6d27eb5fe335c4fb775fa8fa1b73f7b86bb
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       rescue => e
331         raise e if options.env == :test
332         env['sinatra.error'] = e
333         result = (events[:errors][500] || basic_error).invoke(env)
334         returned = catch(:halt) do
335           [:complete, context.instance_eval(&result.block)]
336         end
337         body = returned.to_result(context)
338         context.status(500)
339       end
340       context.body = String === body ? [*body] : body
341       context.finish
342     end
343     
344   end
345   
348 def get(path, &b)
349   Sinatra.application.define_event(:get, path, &b)
352 def post(path, &b)
353   Sinatra.application.define_event(:post, path, &b)
356 def put(path, &b)
357   Sinatra.application.define_event(:put, path, &b)
360 def delete(path, &b)
361   Sinatra.application.define_event(:delete, path, &b)
364 def helpers(&b)
365   Sinatra::EventContext.class_eval(&b)
368 def error(code, &b)
369   Sinatra.application.define_error(code, &b)
372 def layout(name = :layout, &b)
373   Sinatra.application.define_layout(name, &b)
376 def configures(*envs, &b)
377   yield if  envs.include?(Sinatra.application.options.env) ||
378             envs.empty?
380 alias :configure :configures
382 ### Misc Core Extensions
384 module Kernel
386   def silence_warnings
387     old_verbose, $VERBOSE = $VERBOSE, nil
388     yield
389   ensure
390     $VERBOSE = old_verbose
391   end
395 class String
397   # Converts +self+ to an escaped URI parameter value
398   #   'Foo Bar'.to_param # => 'Foo%20Bar'
399   def to_param
400     URI.escape(self)
401   end
402   
403   # Converts +self+ from an escaped URI parameter value
404   #   'Foo%20Bar'.from_param # => 'Foo Bar'
405   def from_param
406     URI.unescape(self)
407   end
408   
411 class Hash
412   
413   def to_params
414     map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
415   end
416   
417   def symbolize_keys
418     self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
419   end
420   
421   def pass(*keys)
422     reject { |k,v| !keys.include?(k) }
423   end
424   
427 class Symbol
428   
429   def to_proc 
430     Proc.new { |*args| args.shift.__send__(self, *args) }
431   end
432   
435 class Array
436   
437   def to_hash
438     self.inject({}) { |h, (k, v)|  h[k] = v; h }
439   end
440   
441   def to_proc
442     Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
443   end
444   
447 module Enumerable
448   
449   def eject(&block)
450     find { |e| result = block[e] and break result }
451   end
452   
455 ### Core Extension results for throw :halt
457 class Proc
458   def to_result(cx, *args)
459     cx.instance_eval(&self)
460   end
463 class String
464   def to_result(cx, *args)
465     cx.body = self
466   end
469 class Array
470   def to_result(cx, *args)
471     self.shift.to_result(cx, *self)
472   end
475 class Symbol
476   def to_result(cx, *args)
477     cx.send(self, *args)
478   end
481 class Fixnum
482   def to_result(cx, *args)
483     cx.status self
484     cx.body args.first
485   end
488 class NilClass
489   def to_result(cx, *args)
490     cx.body = ''
491     # log warning here
492   end
495 at_exit do
496   raise $! if $!
497   Sinatra.run if Sinatra.application.options.run
500 ENV['SINATRA_ENV'] = 'test' if $0 =~ /_test\.rb$/
501 Sinatra::Application.default_options.merge!(
502   :env => (ENV['SINATRA_ENV'] || 'development').to_sym
505 configures :development do
506   
507   get '/sinatra_custom_images/:image.png' do
508     File.read(File.dirname(__FILE__) + "/../images/#{params[:image]}.png")
509   end
510   
511   error 404 do
512     %Q(
513     <html>
514       <body style='text-align: center; color: #888; font-family: Arial; font-size: 22px; margin: 20px'>
515       <h2>Sinatra doesn't know this diddy.</h2>
516       <img src='/sinatra_custom_images/404.png'></img>
517       </body>
518     </html>
519     )
520   end
521   
522   error 500 do
523     @error = request.env['sinatra.error']
524     %Q(
525     <html>
526         <body>
527                 <style type="text/css" media="screen">
528                         body {
529                                 font-family: Verdana;
530                                 color: #333;
531                         }
533                         #content {
534                                 width: 700px;
535                                 margin-left: 20px;
536                         }
538                         #content h1 {
539                                 width: 99%;
540                                 color: #1D6B8D;
541                                 font-weight: bold;
542                         }
543                         
544                         #stacktrace {
545                           margin-top: -20px;
546                         }
548                         #stacktrace pre {
549                                 font-size: 12px;
550                                 border-left: 2px solid #ddd;
551                                 padding-left: 10px;
552                         }
554                         #stacktrace img {
555                                 margin-top: 10px;
556                         }
557                 </style>
558                 <div id="content">
559                 <img src="/sinatra_custom_images/500.png" />
560                         <div id="stacktrace">
561                                 <h1>#{@error.message}</h1>
562                                 <pre><code>#{@error.backtrace.join("\n")}</code></pre>
563                 </div>
564         </body>
565     </html>
566     )
567   end
568