a little refactoring
[sinatra.git] / lib / sinatra.rb
blob9cb1c473c682e684901c36243c7bda40eeb400c9
1 require "rubygems"
2 require "rack"
4 require 'sinatra/mime_types'
6 def silence_warnings
7   old_verbose, $VERBOSE = $VERBOSE, nil
8   yield
9 ensure
10   $VERBOSE = old_verbose
11 end
13 class String
14   def to_param
15     URI.escape(self)
16   end
17   
18   def from_param
19     URI.unescape(self)
20   end
21 end
23 class Hash
24   def to_params
25     map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
26   end
27   
28   def symbolize_keys
29     self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
30   end
31   
32   def pass(*keys)
33     reject { |k,v| !keys.include?(k) }
34   end
35 end
37 class Symbol
38   def to_proc 
39     Proc.new { |*args| args.shift.__send__(self, *args) }
40   end
41 end
43 class Array
44   def to_hash
45     self.inject({}) { |h, (k, v)|  h[k] = v; h }
46   end
47   
48   def to_proc
49     Proc.new { |*args| args.shift.__send__(self[0], args + self[1..-1]) }
50   end
51 end
53 class Proc
54   def block
55     self
56   end
57 end
59 module Enumerable
60   def eject(&block)
61     find { |e| result = block[e] and break result }
62   end
63 end
65 module Sinatra
66   extend self
68   EventContext = Struct.new(:request, :response) do
69     def method_missing(name, *args)
70       if args.size == 1 && response.respond_to?("#{name}=")
71         response.send("#{name}=", args.first)
72       else
73         response.send(name, *args)
74       end
75     end
76   end
77   
78   def setup_default_events!
79     error 500 do
80       "#{$!.message}\n\t#{$!.backtrace.join("\n\t")}"
81     end
83     error 404 do
84       "<h1>Not Found</h1>"
85     end
86   end
87   
88   def request_types
89     @request_types ||= [:get, :put, :post, :delete]
90   end
91   
92   def routes
93     @routes ||= Hash.new do |hash, key|
94       hash[key] = [] if request_types.include?(key)
95     end
96   end
97   
98   def config
99     @config ||= @default_config.dup
100   end
101   
102   def config=(c)
103     @config = c
104   end
105   
106   def default_config
107     @default_config ||= {
108       :run => true,
109       :raise_errors => false,
110       :env => :development,
111       :root => File.dirname($0),
112       :default_static_mime_type => 'text/plain'
113     }
114   end
115   
116   def determine_route(verb, path)
117     routes[verb].eject { |r| r.match(path) } || routes[404]
118   end
119   
120   def content_type_for(path)
121     ext = File.extname(path)[1..-1]
122     Sinatra.mime_types[ext] || config[:default_static_mime_type]
123   end
124   
125   def serve_static_file(path)
126     path = Sinatra.config[:root] + '/public' + path
127     if File.file?(path)
128       headers = {
129         'Content-Type' => Array(content_type_for(path)),
130         'Content-Length' => Array(File.size(path))
131       }
132       [200, headers, File.read(path)]
133     end
134   end
135   
136   def call(env)
137     request = Rack::Request.new(env)
139     if found = serve_static_file(request.path_info)
140       return found
141     end
142         
143     response = Rack::Response.new
144     route = determine_route(
145       request.request_method.downcase.to_sym, 
146       request.path_info
147     )
148     context = EventContext.new(request, response)
149     context.status = nil
150     begin
151       result = context.instance_eval(&route.block)
152       context.status ||= route.default_status
153       context.body = Array(result.to_s)
154       context.finish
155     rescue => e
156       raise e if config[:raise_errors]
157       route = Sinatra.routes[500]
158       context.status 500
159       context.body Array(context.instance_eval(&route.block))
160       context.finish
161     end
162   end
163   
164   def define_route(verb, path, &b)
165     routes[verb] << route = Route.new(path, &b)
166     route
167   end
168   
169   def define_error(code, &b)
170     routes[code] = Error.new(code, &b)
171   end
172   
173   class Route
174         
175     URI_CHAR = '[^/?:,&#]'.freeze unless defined?(URI_CHAR)
176     PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
177     
178     Result = Struct.new(:path, :block, :params, :default_status)
179     
180     attr_reader :block, :path
181     
182     def initialize(path, &b)
183       @path, @block = path, b
184       @param_keys = []
185       regex = path.to_s.gsub(PARAM) do
186         @param_keys << $1.intern
187         "(#{URI_CHAR}+)"
188       end
189       @pattern = /^#{regex}$/
190     end
191         
192     def match(path)
193       return nil unless path =~ @pattern
194       params = @param_keys.zip($~.captures.map(&:from_param)).to_hash
195       Result.new(@path, @block, params, 200)
196     end
197     
198   end
199   
200   class Error
201     
202     attr_reader :block
203     
204     def initialize(code, &b)
205       @code, @block = code, b
206     end
207     
208     def default_status
209       @code
210     end
211         
212   end
213       
216 def get(*paths, &b)
217   paths.map { |path| Sinatra.define_route(:get, path, &b) }
220 def error(*codes, &b)
221   raise 'You must specify a block to assciate with an error' if b.nil?
222   codes.each { |code| Sinatra.define_error(code, &b) }
225 def mime_type(content_type, *exts)
226   exts.each { |ext| Sinatra::MIME_TYPES.merge(ext.to_s, content_type) }
229 Sinatra.setup_default_events!