pulling send file out for now
[sinatra.git] / lib / sinatra.rb
blobe02e0a357e427cc2668edf6bac2da5c7621bb3e3
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 default_config
164     @default_config ||= {
165       :run => true,
166       :port => 4567,
167       :raise_errors => false,
168       :env => :development,
169       :root => File.dirname($0),
170       :default_static_mime_type => 'text/plain',
171       :default_params => { :format => 'html' }
172     }
173   end
174   
175   def determine_route(verb, path)
176     routes[verb].eject { |r| r.match(path) } || routes[404]
177   end
178   
179   def content_type_for(path)
180     ext = File.extname(path)[1..-1]
181     Sinatra.mime_types[ext] || config[:default_static_mime_type]
182   end
183   
184   def serve_static_file(path)
185     path = Sinatra.config[:root] + '/public' + path
186     if File.file?(path)
187       headers = {
188         'Content-Type' => Array(content_type_for(path)),
189         'Content-Length' => Array(File.size(path))
190       }
191       [200, headers, File.read(path)]
192     end
193   end
194   
195   def call(env)
196     request = Rack::Request.new(env)
198     if found = serve_static_file(request.path_info)
199       return found
200     end
201         
202     response = Rack::Response.new
203     route = determine_route(
204       request.request_method.downcase.to_sym, 
205       request.path_info
206     )
207     context = EventContext.new(request, response, route.params)
208     context.status = nil
209     begin
210       context = handle_with_filters(context, &route.block)
211       context.status ||= route.default_status
212       context.finish
213     rescue => e
214       raise e if config[:raise_errors]
215       route = Sinatra.routes[500]
216       context.status 500
217       context.body Array(context.instance_eval(&route.block))
218       context.finish
219     end
220   end
221   
222   def define_route(verb, path, &b)
223     routes[verb] << route = Route.new(path, &b)
224     route
225   end
226   
227   def define_error(code, &b)
228     routes[code] = Error.new(code, &b)
229   end
230   
231   def define_filter(type, &b)
232     filters[type] << b
233   end
234   
235   def reset!
236     routes.clear
237     config = nil
238     setup_default_events!
239   end
240   
241   protected
243     def handle_with_filters(cx, &b)
244       caught = catch(:halt) do
245         filters[:before].each { |x| cx.instance_eval(&x) }
246         [:complete, b]
247       end
248       caught = catch(:halt) do
249         caught.to_result(cx)
250       end
251       result = caught.to_result(cx) if caught
252       filters[:after].each { |x| cx.instance_eval(&x) }
253       cx.body Array(result.to_s)
254       cx
255     end
256   
257   class Route
258         
259     URI_CHAR = '[^/?:,&#]'.freeze unless defined?(URI_CHAR)
260     PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
261     
262     Result = Struct.new(:path, :block, :params, :default_status)
263     
264     attr_reader :block, :path
265     
266     def initialize(path, &b)
267       @path, @block = path, b
268       @param_keys = []
269       regex = path.to_s.gsub(PARAM) do
270         @param_keys << $1.intern
271         "(#{URI_CHAR}+)"
272       end
273       if path =~ /:format$/
274         @pattern = /^#{regex}$/
275       else
276         @param_keys << :format
277         @pattern = /^#{regex}(?:\.(#{URI_CHAR}+))?$/
278       end
279     end
280         
281     def match(path)
282       return nil unless path =~ @pattern
283       params = @param_keys.zip($~.captures.compact.map(&:from_param)).to_hash
284       Result.new(@path, @block, include_format(params), 200)
285     end
286     
287     def include_format(h)
288       h.delete(:format) unless h[:format]
289       Sinatra.config[:default_params].merge(h)
290     end
291     
292   end
293   
294   class Error
295     
296     attr_reader :block
297     
298     def initialize(code, &b)
299       @code, @block = code, b
300     end
301     
302     def default_status
303       @code
304     end
305     
306     def params; {}; end
307   end
308       
311 def get(*paths, &b)
312   paths.map { |path| Sinatra.define_route(:get, path, &b) }
315 def post(*paths, &b)
316   paths.map { |path| Sinatra.define_route(:post, path, &b) }
319 def put(*paths, &b)
320   paths.map { |path| Sinatra.define_route(:put, path, &b) }
323 def delete(*paths, &b)
324   paths.map { |path| Sinatra.define_route(:delete, path, &b) }
327 def error(*codes, &b)
328   raise 'You must specify a block to assciate with an error' if b.nil?
329   codes.each { |code| Sinatra.define_error(code, &b) }
332 def before(&b)
333   Sinatra.define_filter(:before, &b)
336 def after(&b)
337   Sinatra.define_filter(:after, &b)
340 def mime_type(content_type, *exts)
341   exts.each { |ext| Sinatra::MIME_TYPES.merge(ext.to_s, content_type) }
344 def helpers(&b)
345   Sinatra::EventContext.class_eval(&b)
348 def configures(*envs)
349   yield if (envs.include?(Sinatra.config[:env]) || 
350     envs.empty?) && 
351     !Sinatra.config[:reloading]
354 Sinatra.setup_default_events!
356 at_exit do
357   raise $! if $!
358   Sinatra.run if Sinatra.config[:run]