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