format
[sinatra.git] / lib / sinatra.rb
blobe66a74918c80ea55017becaf8f0c0e413de9d769
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, :route_params) do
69     def params
70       @params ||= request.params.merge(route_params).symbolize_keys
71     end
72     
73     def method_missing(name, *args)
74       if args.size == 1 && response.respond_to?("#{name}=")
75         response.send("#{name}=", args.first)
76       else
77         response.send(name, *args)
78       end
79     end
80   end
81   
82   def setup_default_events!
83     error 500 do
84       "#{$!.message}\n\t#{$!.backtrace.join("\n\t")}"
85     end
87     error 404 do
88       "<h1>Not Found</h1>"
89     end
90   end
91   
92   def request_types
93     @request_types ||= [:get, :put, :post, :delete]
94   end
95   
96   def routes
97     @routes ||= Hash.new do |hash, key|
98       hash[key] = [] if request_types.include?(key)
99     end
100   end
101   
102   def config
103     @config ||= @default_config.dup
104   end
105   
106   def config=(c)
107     @config = c
108   end
109   
110   def default_config
111     @default_config ||= {
112       :run => true,
113       :raise_errors => false,
114       :env => :development,
115       :root => File.dirname($0),
116       :default_static_mime_type => 'text/plain',
117       :default_params => { :format => 'html' }
118     }
119   end
120   
121   def determine_route(verb, path)
122     routes[verb].eject { |r| r.match(path) } || routes[404]
123   end
124   
125   def content_type_for(path)
126     ext = File.extname(path)[1..-1]
127     Sinatra.mime_types[ext] || config[:default_static_mime_type]
128   end
129   
130   def serve_static_file(path)
131     path = Sinatra.config[:root] + '/public' + path
132     if File.file?(path)
133       headers = {
134         'Content-Type' => Array(content_type_for(path)),
135         'Content-Length' => Array(File.size(path))
136       }
137       [200, headers, File.read(path)]
138     end
139   end
140   
141   def call(env)
142     request = Rack::Request.new(env)
144     if found = serve_static_file(request.path_info)
145       return found
146     end
147         
148     response = Rack::Response.new
149     route = determine_route(
150       request.request_method.downcase.to_sym, 
151       request.path_info
152     )
153     context = EventContext.new(request, response, route.params)
154     context.status = nil
155     begin
156       result = context.instance_eval(&route.block)
157       context.status ||= route.default_status
158       context.body = Array(result.to_s)
159       context.finish
160     rescue => e
161       raise e if config[:raise_errors]
162       route = Sinatra.routes[500]
163       context.status 500
164       context.body Array(context.instance_eval(&route.block))
165       context.finish
166     end
167   end
168   
169   def define_route(verb, path, &b)
170     routes[verb] << route = Route.new(path, &b)
171     route
172   end
173   
174   def define_error(code, &b)
175     routes[code] = Error.new(code, &b)
176   end
177   
178   class Route
179         
180     URI_CHAR = '[^/?:,&#]'.freeze unless defined?(URI_CHAR)
181     PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
182     
183     Result = Struct.new(:path, :block, :params, :default_status)
184     
185     attr_reader :block, :path
186     
187     def initialize(path, &b)
188       @path, @block = path, b
189       @param_keys = []
190       regex = path.to_s.gsub(PARAM) do
191         @param_keys << $1.intern
192         "(#{URI_CHAR}+)"
193       end
194       @param_keys << :format
195       @pattern = /^#{regex}(?:\.(#{URI_CHAR}+))?$/
196     end
197         
198     def match(path)
199       return nil unless path =~ @pattern
200       params = @param_keys.zip($~.captures.compact.map(&:from_param)).to_hash
201       Result.new(@path, @block, include_format(params), 200)
202     end
203     
204     def include_format(h)
205       h.delete(:format) unless h[:format]
206       Sinatra.config[:default_params].merge(h)
207     end
208     
209   end
210   
211   class Error
212     
213     attr_reader :block
214     
215     def initialize(code, &b)
216       @code, @block = code, b
217     end
218     
219     def default_status
220       @code
221     end
222     
223     def params; {}; end
224   end
225       
228 def get(*paths, &b)
229   paths.map { |path| Sinatra.define_route(:get, path, &b) }
232 def error(*codes, &b)
233   raise 'You must specify a block to assciate with an error' if b.nil?
234   codes.each { |code| Sinatra.define_error(code, &b) }
237 def mime_type(content_type, *exts)
238   exts.each { |ext| Sinatra::MIME_TYPES.merge(ext.to_s, content_type) }
241 Sinatra.setup_default_events!