up and running
[sinatra.git] / lib / sinatra.rb
blob9ad411dd1e28af27fb5e2725a896b19a43191452
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/send_file_mixin'
15 require 'sinatra/halt_results'
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   def run
80     
81     begin
82       puts "== Sinatra has taken the stage on port #{Sinatra.config[:port]} for #{Sinatra.config[:env]}"
83       require 'pp'
84       Rack::Handler::Mongrel.run(Sinatra, :Port => Sinatra.config[:port]) do |server|
85         trap(:INT) do
86           server.stop
87           puts "\n== Sinatra has ended his set (crowd applauds)"
88         end
89       end
90     rescue Errno::EADDRINUSE => e
91       puts "== Someone is already performing on port #{Sinatra.config[:port]}!"
92     end
93     
94   end
96   EventContext = Struct.new(:request, :response, :route_params) do
97     def params
98       @params ||= request.params.merge(route_params).symbolize_keys
99     end
100     
101     def complete(b)
102       self.instance_eval(&b)
103     end
104     
105     def method_missing(name, *args)
106       if args.size == 1 && response.respond_to?("#{name}=")
107         response.send("#{name}=", args.first)
108       else
109         response.send(name, *args)
110       end
111     end
112   end
113   
114   def setup_default_events!
115     error 500 do
116       "<h2>#{$!.message}</h2>#{$!.backtrace.join("<br/>")}"
117     end
119     error 404 do
120       "<h1>Not Found</h1>"
121     end
122   end
123   
124   def request_types
125     @request_types ||= [:get, :put, :post, :delete]
126   end
127   
128   def routes
129     @routes ||= Hash.new do |hash, key|
130       hash[key] = [] if request_types.include?(key)
131     end
132   end
133   
134   def filters
135     @filters ||= Hash.new { |hash, key| hash[key] = [] }
136   end
137   
138   def config
139     @config ||= default_config.dup
140   end
141   
142   def config=(c)
143     @config = c
144   end
145   
146   def default_config
147     @default_config ||= {
148       :run => true,
149       :port => 4567,
150       :raise_errors => false,
151       :env => :development,
152       :root => File.dirname($0),
153       :default_static_mime_type => 'text/plain',
154       :default_params => { :format => 'html' }
155     }
156   end
157   
158   def determine_route(verb, path)
159     routes[verb].eject { |r| r.match(path) } || routes[404]
160   end
161   
162   def content_type_for(path)
163     ext = File.extname(path)[1..-1]
164     Sinatra.mime_types[ext] || config[:default_static_mime_type]
165   end
166   
167   def serve_static_file(path)
168     path = Sinatra.config[:root] + '/public' + path
169     if File.file?(path)
170       headers = {
171         'Content-Type' => Array(content_type_for(path)),
172         'Content-Length' => Array(File.size(path))
173       }
174       [200, headers, File.read(path)]
175     end
176   end
177   
178   def call(env)
179     request = Rack::Request.new(env)
181     if found = serve_static_file(request.path_info)
182       return found
183     end
184         
185     response = Rack::Response.new
186     route = determine_route(
187       request.request_method.downcase.to_sym, 
188       request.path_info
189     )
190     context = EventContext.new(request, response, route.params)
191     context.status = nil
192     begin
193       context = handle_with_filters(context, &route.block)
194       context.status ||= route.default_status
195       context.finish
196     rescue => e
197       raise e if config[:raise_errors]
198       route = Sinatra.routes[500]
199       context.status 500
200       context.body Array(context.instance_eval(&route.block))
201       context.finish
202     end
203   end
204   
205   def define_route(verb, path, &b)
206     routes[verb] << route = Route.new(path, &b)
207     route
208   end
209   
210   def define_error(code, &b)
211     routes[code] = Error.new(code, &b)
212   end
213   
214   def define_filter(type, &b)
215     filters[type] << b
216   end
217   
218   def reset!
219     routes.clear
220     config = nil
221     setup_default_events!
222   end
223   
224   protected
226     def handle_with_filters(cx, &b)
227       caught = catch(:halt) do
228         filters[:before].each { |x| cx.instance_eval(&x) }
229         [:complete, b]
230       end
231       caught = catch(:halt) do
232         caught.to_result(cx)
233       end
234       result = caught.to_result(cx) if caught
235       filters[:after].each { |x| cx.instance_eval(&x) }
236       cx.body Array(result.to_s)
237       cx
238     end
239   
240   class Route
241         
242     URI_CHAR = '[^/?:,&#]'.freeze unless defined?(URI_CHAR)
243     PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
244     
245     Result = Struct.new(:path, :block, :params, :default_status)
246     
247     attr_reader :block, :path
248     
249     def initialize(path, &b)
250       @path, @block = path, b
251       @param_keys = []
252       regex = path.to_s.gsub(PARAM) do
253         @param_keys << $1.intern
254         "(#{URI_CHAR}+)"
255       end
256       if path =~ /:format$/
257         @pattern = /^#{regex}$/
258       else
259         @param_keys << :format
260         @pattern = /^#{regex}(?:\.(#{URI_CHAR}+))?$/
261       end
262     end
263         
264     def match(path)
265       return nil unless path =~ @pattern
266       params = @param_keys.zip($~.captures.compact.map(&:from_param)).to_hash
267       Result.new(@path, @block, include_format(params), 200)
268     end
269     
270     def include_format(h)
271       h.delete(:format) unless h[:format]
272       Sinatra.config[:default_params].merge(h)
273     end
274     
275   end
276   
277   class Error
278     
279     attr_reader :block
280     
281     def initialize(code, &b)
282       @code, @block = code, b
283     end
284     
285     def default_status
286       @code
287     end
288     
289     def params; {}; end
290   end
291       
294 def get(*paths, &b)
295   paths.map { |path| Sinatra.define_route(:get, path, &b) }
298 def post(*paths, &b)
299   paths.map { |path| Sinatra.define_route(:post, path, &b) }
302 def put(*paths, &b)
303   paths.map { |path| Sinatra.define_route(:put, path, &b) }
306 def delete(*paths, &b)
307   paths.map { |path| Sinatra.define_route(:delete, path, &b) }
310 def error(*codes, &b)
311   raise 'You must specify a block to assciate with an error' if b.nil?
312   codes.each { |code| Sinatra.define_error(code, &b) }
315 def before(&b)
316   Sinatra.define_filter(:before, &b)
319 def after(&b)
320   Sinatra.define_filter(:after, &b)
323 def mime_type(content_type, *exts)
324   exts.each { |ext| Sinatra::MIME_TYPES.merge(ext.to_s, content_type) }
327 def helpers(&b)
328   Sinatra::EventContext.class_eval(&b)
331 def configures(*envs)
332   yield if (envs.include?(Sinatra.config[:env]) || 
333     envs.empty?) && 
334     !Sinatra.config[:reloading]
337 Sinatra.setup_default_events!
339 Sinatra::EventContext.send :include, Sinatra::SendFileMixin
341 at_exit do
342   raise $! if $!
343   Sinatra.run if Sinatra.config[:run]