* fixed reloading and warnings.
[sinatra.git] / lib / sinatra.rb
blob6c314dedfe6fc5bed4521af37f32ba819b63c310
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"
13 require File.dirname(__FILE__) + '/sinatra/mime_types'
14 require File.dirname(__FILE__) + '/sinatra/halt_results'
15 require File.dirname(__FILE__) + '/sinatra/logger'
17 def silence_warnings
18   old_verbose, $VERBOSE = $VERBOSE, nil
19   yield
20 ensure
21   $VERBOSE = old_verbose
22 end
24 class String
25   def to_param
26     URI.escape(self)
27   end
28   
29   def from_param
30     URI.unescape(self)
31   end
32 end
34 class Hash
35   def to_params
36     map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
37   end
38   
39   def symbolize_keys
40     self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
41   end
42   
43   def pass(*keys)
44     reject { |k,v| !keys.include?(k) }
45   end
46 end
48 class Symbol
49   def to_proc 
50     Proc.new { |*args| args.shift.__send__(self, *args) }
51   end
52 end
54 class Array
55   def to_hash
56     self.inject({}) { |h, (k, v)|  h[k] = v; h }
57   end
58   
59   def to_proc
60     Proc.new { |*args| args.shift.__send__(self[0], args + self[1..-1]) }
61   end
62 end
64 class Proc
65   def block
66     self
67   end
68 end
70 module Enumerable
71   def eject(&block)
72     find { |e| result = block[e] and break result }
73   end
74 end
76 module Sinatra
77   extend self
79   attr_accessor :logger
81   def run
82     
83     begin
84       puts "== Sinatra has taken the stage on port #{Sinatra.config[:port]} for #{Sinatra.config[:env]}"
85       require 'pp'
86       Rack::Handler::Mongrel.run(Sinatra, :Port => Sinatra.config[:port]) do |server|
87         trap(:INT) do
88           server.stop
89           puts "\n== Sinatra has ended his set (crowd applauds)"
90         end
91       end
92     rescue Errno::EADDRINUSE => e
93       puts "== Someone is already performing on port #{Sinatra.config[:port]}!"
94     end
95     
96   end
98   class EventContext
99     
100     attr_reader :request, :response, :route_params
101     
102     def logger
103       Sinatra.logger
104     end
105     
106     def initialize(request, response, route_params)
107       @request, @response, @route_params = 
108         request, response, route_params
109     end
110     
111     def params
112       @params ||= request.params.merge(route_params).symbolize_keys
113     end
114     
115     def complete(b)
116       self.instance_eval(&b)
117     end
118     
119     # redirect to another url It can be like /foo/bar
120     # for redirecting within your same app. Or it can
121     # be a fully qualified url to another site.
122     def redirect(url)
123       logger.info "Redirecting to: #{url}"
124       status(302)
125       headers.merge!('Location' => url)
126       return ''
127     end
128     
129     def method_missing(name, *args)
130       if args.size == 1 && response.respond_to?("#{name}=")
131         response.send("#{name}=", args.first)
132       else
133         response.send(name, *args)
134       end
135     end
136     
137   end
139   def setup_logger
140     self.logger = Sinatra::Logger.new(
141       config[:root] + "/#{Sinatra.config[:env]}.log"
142     )
143   end
144   
145   def setup_default_events!
146     error 500 do
147       "<h2>#{$!.message}</h2>#{$!.backtrace.join("<br/>")}"
148     end
150     error 404 do
151       "<h1>Not Found</h1>"
152     end
153   end
154   
155   def request_types
156     @request_types ||= [:get, :put, :post, :delete]
157   end
158   
159   def routes
160     @routes ||= Hash.new do |hash, key|
161       hash[key] = [] if request_types.include?(key)
162     end
163   end
164   
165   def filters
166     @filters ||= Hash.new { |hash, key| hash[key] = [] }
167   end
168   
169   def config
170     @config ||= default_config.dup
171   end
172   
173   def config=(c)
174     @config = c
175   end
176   
177   def development?
178     config[:env] == :development
179   end
180   
181   def default_config
182     @default_config ||= {
183       :run => true,
184       :port => 4567,
185       :raise_errors => false,
186       :env => :development,
187       :root => Dir.pwd,
188       :default_static_mime_type => 'text/plain',
189       :default_params => { :format => 'html' }
190     }
191   end
192   
193   def determine_route(verb, path)
194     routes[verb].eject { |r| r.match(path) } || routes[404]
195   end
196   
197   def content_type_for(path)
198     ext = File.extname(path)[1..-1]
199     Sinatra.mime_types[ext] || config[:default_static_mime_type]
200   end
201   
202   def serve_static_file(path)
203     path = Sinatra.config[:root] + '/public' + path
204     if File.file?(path)
205       headers = {
206         'Content-Type' => Array(content_type_for(path)),
207         'Content-Length' => Array(File.size(path))
208       }
209       [200, headers, File.read(path)]
210     end
211   end
212   
213   def call(env)
214     
215     reload! if Sinatra.development?
217     time = Time.now
218     
219     request = Rack::Request.new(env)
221     if found = serve_static_file(request.path_info)
222       log_request_and_response(time, request, Rack::Response.new(found))
223       return found
224     end
225         
226     response = Rack::Response.new
227     route = determine_route(
228       request.request_method.downcase.to_sym, 
229       request.path_info
230     )
231     context = EventContext.new(request, response, route.params)
232     context.status = nil
233     begin
234       context = handle_with_filters(context, &route.block)
235       context.status ||= route.default_status
236     rescue => e
237       raise e if config[:raise_errors]
238       route = Sinatra.routes[500]
239       context.status 500
240       context.body Array(context.instance_eval(&route.block))
241     ensure
242       log_request_and_response(time, request, response)
243       logger.flush
244     end
245     
246     context.finish
247   end
248     
249   def define_route(verb, path, &b)
250     routes[verb] << route = Route.new(path, &b)
251     route
252   end
253   
254   def define_error(code, &b)
255     routes[code] = Error.new(code, &b)
256   end
257   
258   def define_filter(type, &b)
259     filters[type] << b
260   end
261   
262   def reset!
263     self.config = nil
264     routes.clear
265     filters.clear
266     setup_default_events!
267   end
268   
269   def reload!
270     reset!
271     self.config[:reloading] = true
272     load $0
273     self.config[:reloading] = false
274   end
275   
276   protected
277   
278     def log_request_and_response(time, request, response)
279       now = Time.now
281       # Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
282       # lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 -
283       #             %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
284       logger.info %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n} %
285         [
286           request.env["REMOTE_ADDR"] || "-",
287           request.env["REMOTE_USER"] || "-",
288           now.strftime("%d/%b/%Y %H:%M:%S"),
289           request.env["REQUEST_METHOD"],
290           request.env["PATH_INFO"],
291           request.env["QUERY_STRING"].empty? ? 
292             "" : 
293             "?" + request.env["QUERY_STRING"],
294           request.env["HTTP_VERSION"],
295           response.status.to_s[0..3].to_i,
296           (response.body.length.zero? ? "-" : response.body.length.to_s),
297           now - time
298         ]
299     end
301     def handle_with_filters(cx, &b)
302       caught = catch(:halt) do
303         filters[:before].each { |x| cx.instance_eval(&x) }
304         [:complete, b]
305       end
306       caught = catch(:halt) do
307         caught.to_result(cx)
308       end
309       result = caught.to_result(cx) if caught
310       filters[:after].each { |x| cx.instance_eval(&x) }
311       cx.body Array(result.to_s)
312       cx
313     end
314   
315   class Route
316         
317     URI_CHAR = '[^/?:,&#]'.freeze unless defined?(URI_CHAR)
318     PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
319     
320     attr_reader :block, :path
321     
322     def initialize(path, &b)
323       @path, @block = path, b
324       @param_keys = []
325       @struct = Struct.new(:path, :block, :params, :default_status)
326       regex = path.to_s.gsub(PARAM) do
327         @param_keys << $1.intern
328         "(#{URI_CHAR}+)"
329       end
330       if path =~ /:format$/
331         @pattern = /^#{regex}$/
332       else
333         @param_keys << :format
334         @pattern = /^#{regex}(?:\.(#{URI_CHAR}+))?$/
335       end
336     end
337         
338     def match(path)
339       return nil unless path =~ @pattern
340       params = @param_keys.zip($~.captures.compact.map(&:from_param)).to_hash
341       @struct.new(@path, @block, include_format(params), 200)
342     end
343     
344     def include_format(h)
345       h.delete(:format) unless h[:format]
346       Sinatra.config[:default_params].merge(h)
347     end
348     
349     def pretty_print(pp)
350       pp.text "{Route: #{@pattern} : [#{@param_keys.map(&:inspect).join(",")}] }"
351     end
352     
353   end
354   
355   class Error
356     
357     attr_reader :block
358     
359     def initialize(code, &b)
360       @code, @block = code, b
361     end
362     
363     def default_status
364       @code
365     end
366     
367     def params; {}; end
368   end
369       
372 def get(*paths, &b)
373   paths.map { |path| Sinatra.define_route(:get, path, &b) }
376 def post(*paths, &b)
377   paths.map { |path| Sinatra.define_route(:post, path, &b) }
380 def put(*paths, &b)
381   paths.map { |path| Sinatra.define_route(:put, path, &b) }
384 def delete(*paths, &b)
385   paths.map { |path| Sinatra.define_route(:delete, path, &b) }
388 def error(*codes, &b)
389   raise 'You must specify a block to assciate with an error' if b.nil?
390   codes.each { |code| Sinatra.define_error(code, &b) }
393 def before(&b)
394   Sinatra.define_filter(:before, &b)
397 def after(&b)
398   Sinatra.define_filter(:after, &b)
401 def mime_type(content_type, *exts)
402   exts.each { |ext| Sinatra::MIME_TYPES.merge(ext.to_s, content_type) }
405 def helpers(&b)
406   Sinatra::EventContext.class_eval(&b)
409 def configures(*envs)
410   return if Sinatra.config[:reloading]
411   yield if (envs.include?(Sinatra.config[:env]) || envs.empty?)
413 alias :configure :configures
415 Sinatra.setup_default_events!
417 at_exit do
418   raise $! if $!
419   Sinatra.setup_logger
420   Sinatra.run if Sinatra.config[:run]