this should be stop and body= should go to @response
[sinatra.git] / lib / sinatra.rb
blob9c66eec400c07af581e25fff7980b42ea4b9b925
1 require 'rubygems'
2 require 'rack'
4 class Class
5   def dslify_writter(*syms)
6     syms.each do |sym|
7       class_eval <<-end_eval
8         def #{sym}(v=nil)
9           self.send "#{sym}=", v if v
10           v
11         end
12       end_eval
13     end
14   end
15 end
17 module Sinatra
18   extend self
20   Result = Struct.new(:block, :params)
21   
22   def application
23     @app ||= Application.new
24   end
25   
26   def application=(app)
27     @app = app
28   end
29       
30   class Event
32     URI_CHAR = '[^/?:,&#]'.freeze unless defined?(URI_CHAR)
33     PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
34     
35     attr_reader :path, :block, :param_keys, :pattern
36     
37     def initialize(path, &b)
38       @path = path
39       @block = b
40       @param_keys = []
41       regex = @path.to_s.gsub(PARAM) do
42         @param_keys << $1.intern
43         "(#{URI_CHAR}+)"
44       end
45       @pattern = /^#{regex}$/
46     end
47         
48     def invoke(env)
49       return unless pattern =~ env['PATH_INFO'].squeeze('/')
50       params = param_keys.zip($~.captures.map(&:from_param)).to_hash
51       Result.new(block, params)
52     end
53     
54   end
55   
56   class EventContext
57     
58     module ResponseHelpers
60       def redirect(path)
61         throw :halt, Redirect.new(path)
62       end
64     end
65     include ResponseHelpers
66     
67     module RenderingHelpers
68       
69       def render(content, options={})
70         template = resolve_template(content, options)
71         @content = _evaluate_render(template)
72         layout = resolve_layout(options[:layout], options)
73         @content = _evaluate_render(layout) if layout
74         @content
75       end
76       
77       private
78         
79         def _evaluate_render(content, options={})
80           case content
81           when String
82             instance_eval(%Q{"#{content}"})
83           when Proc
84             instance_eval(&content)
85           when File
86             instance_eval(%Q{"#{content.read}"})
87           end
88         end
89         
90         def resolve_template(content, options={})
91           case content
92           when String
93             content
94           when Symbol
95             File.new(filename_for(content, options))
96           end
97         end
98       
99         def resolve_layout(name, options={})
100           return if name == false
101           if layout = layouts[name || :layout]
102             return layout
103           end
104           if File.file?(filename = filename_for(name, options))
105             File.new(filename)
106           end
107         end
108         
109         def filename_for(name, options={})
110           (options[:views_directory] || 'views') + "/#{name}.#{ext}"
111         end
112                 
113         def ext
114           :html
115         end
117         def layouts
118           Sinatra.application.layouts
119         end
120       
121     end
122     include RenderingHelpers
123     
124     
125     attr_accessor :request, :response
126     
127     dslify_writter :status, :body
128     
129     def initialize(request, response, route_params)
130       @request = request
131       @response = response
132       @route_params = route_params
133       @response.body = nil
134     end
135     
136     def params
137       @params ||= @route_params.merge(@request.params).symbolize_keys
138     end
139     
140     def stop(content)
141       throw :halt, content
142     end
143     
144     def complete(returned)
145       @response.body || returned
146     end
147     
148     private
150       def method_missing(name, *args, &b)
151         @response.send(name, *args, &b)
152       end
153     
154   end
155   
156   class Redirect
157     def initialize(path)
158       @path = path
159     end
160     
161     def to_result(cx, *args)
162       cx.status(302)
163       cx.header.merge!('Location' => @path)
164       cx.body = ''
165     end
166   end
167     
168   class Application
169     
170     attr_reader :events, :layouts
171     
172     def initialize
173       @events = Hash.new { |hash, key| hash[key] = [] }
174       @layouts = Hash.new
175     end
176     
177     def define_event(method, path, &b)
178       events[method] << event = Event.new(path, &b)
179       event
180     end
181     
182     def define_layout(name=:layout, &b)
183       layouts[name] = b
184     end
185     
186     def lookup(env)
187       events[env['REQUEST_METHOD'].downcase.to_sym].eject(&[:invoke, env])
188     end
189     
190     def call(env)
191       return [404, {}, 'Not Found'] unless result = lookup(env)
192       context = EventContext.new(
193         Rack::Request.new(env), 
194         Rack::Response.new,
195         result.params
196       )
197       returned = catch(:halt) do
198         [:complete, context.instance_eval(&result.block)]
199       end
200       result = returned.to_result(context)
201       context.body = String === result ? [*result] : result
202       context.finish
203     end
204         
205   end
206   
209 def get(path, &b)
210   Sinatra.application.define_event(:get, path, &b)
213 def post(path, &b)
214   Sinatra.application.define_event(:post, path, &b)
217 def put(path, &b)
218   Sinatra.application.define_event(:put, path, &b)
221 def delete(path, &b)
222   Sinatra.application.define_event(:delete, path, &b)
225 def helpers(&b)
226   Sinatra::EventContext.class_eval(&b)
229 def layout(name = :layout, &b)
230   Sinatra.application.define_layout(name, &b)
233 ### Misc Core Extensions
235 module Kernel
237   def silence_warnings
238     old_verbose, $VERBOSE = $VERBOSE, nil
239     yield
240   ensure
241     $VERBOSE = old_verbose
242   end
246 class String
248   # Converts +self+ to an escaped URI parameter value
249   #   'Foo Bar'.to_param # => 'Foo%20Bar'
250   def to_param
251     URI.escape(self)
252   end
253   
254   # Converts +self+ from an escaped URI parameter value
255   #   'Foo%20Bar'.from_param # => 'Foo Bar'
256   def from_param
257     URI.unescape(self)
258   end
259   
262 class Hash
263   
264   def to_params
265     map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
266   end
267   
268   def symbolize_keys
269     self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
270   end
271   
272   def pass(*keys)
273     reject { |k,v| !keys.include?(k) }
274   end
275   
278 class Symbol
279   
280   def to_proc 
281     Proc.new { |*args| args.shift.__send__(self, *args) }
282   end
283   
286 class Array
287   
288   def to_hash
289     self.inject({}) { |h, (k, v)|  h[k] = v; h }
290   end
291   
292   def to_proc
293     Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
294   end
295   
298 module Enumerable
299   
300   def eject(&block)
301     find { |e| result = block[e] and break result }
302   end
303   
306 ### Core Extension results for throw :halt
308 class Proc
309   def to_result(cx, *args)
310     cx.instance_eval(&self)
311   end
314 class String
315   def to_result(cx, *args)
316     cx.body = self
317   end
320 class Array
321   def to_result(cx, *args)
322     self.shift.to_result(cx, *self)
323   end
326 class Symbol
327   def to_result(cx, *args)
328     cx.send(self, *args)
329   end
332 class Fixnum
333   def to_result(cx, *args)
334     cx.status self
335     cx.body args.first
336   end
339 class NilClass
340   def to_result(cx, *args)
341     cx.body = ''
342     # log warning here
343   end