looking for layouts in files if not inline
[sinatra.git] / lib / sinatra.rb
blob88d254a7fc25368244bfc8561ee0a6129592ea8a
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         @content = _evaluate_render(%Q{"#{content}"})
71         layout = resolve_layout(options[:layout], options)
72         @content = _evaluate_render(layout) if layout
73         @content
74       end
75       
76       private
77         
78         def _evaluate_render(content, options={})
79           case content
80           when String
81             instance_eval(content)
82           when Proc
83             instance_eval(&content)
84           when File
85             instance_eval(%Q{"#{content.read}"})
86           end
87         end
88       
89         def resolve_layout(name, options={})
90           return if name == false
91           if layout = layouts[name || :layout]
92             return layout
93           end
94           filename = (options[:views_directory] || 'views') + "/#{name}.#{ext}"
95           if File.file?(filename)
96             File.new(filename)
97           end
98         end
99                 
100         def ext
101           :html
102         end
104         def layouts
105           Sinatra.application.layouts
106         end
107       
108     end
109     include RenderingHelpers
110     
111     
112     attr_accessor :request, :response
113     
114     dslify_writter :status
115     
116     def initialize(request, response, route_params)
117       @request = request
118       @response = response
119       @route_params = route_params
120       @response.body = nil
121     end
122     
123     def params
124       @params ||= @route_params.merge(@request.params).symbolize_keys
125     end
126     
127     def body(content)
128       throw :halt, content
129     end
130     
131     def complete(returned)
132       @response.body || returned
133     end
134     
135     private
137       def _body=(content)
138         @response.body = content
139       end
140     
141       def method_missing(name, *args, &b)
142         raise NoMethodError.new('body=') if name == :body=
143         @response.send(name, *args, &b)
144       end
145     
146   end
147   
148   class Redirect
149     def initialize(path)
150       @path = path
151     end
152     
153     def to_result(cx, *args)
154       cx.status(302)
155       cx.header.merge!('Location' => @path)
156       cx.send :_body=, ''
157     end
158   end
159     
160   class Application
161     
162     attr_reader :events, :layouts
163     
164     def initialize
165       @events = Hash.new { |hash, key| hash[key] = [] }
166       @layouts = Hash.new
167     end
168     
169     def define_event(method, path, &b)
170       events[method] << event = Event.new(path, &b)
171       event
172     end
173     
174     def define_layout(name=:layout, &b)
175       layouts[name] = b
176     end
177     
178     def lookup(env)
179       events[env['REQUEST_METHOD'].downcase.to_sym].eject(&[:invoke, env])
180     end
181     
182     def call(env)
183       return [404, {}, 'Not Found'] unless result = lookup(env)
184       context = EventContext.new(
185         Rack::Request.new(env), 
186         Rack::Response.new,
187         result.params
188       )
189       returned = catch(:halt) do
190         [:complete, context.instance_eval(&result.block)]
191       end
192       result = returned.to_result(context)
193       context.send :_body=, String === result ? [*result] : result
194       context.finish
195     end
196         
197   end
198   
201 def get(path, &b)
202   Sinatra.application.define_event(:get, path, &b)
205 def post(path, &b)
206   Sinatra.application.define_event(:post, path, &b)
209 def put(path, &b)
210   Sinatra.application.define_event(:put, path, &b)
213 def delete(path, &b)
214   Sinatra.application.define_event(:delete, path, &b)
217 def helpers(&b)
218   Sinatra::EventContext.class_eval(&b)
221 def layout(name = :layout, &b)
222   Sinatra.application.define_layout(name, &b)
225 ### Misc Core Extensions
227 module Kernel
229   def silence_warnings
230     old_verbose, $VERBOSE = $VERBOSE, nil
231     yield
232   ensure
233     $VERBOSE = old_verbose
234   end
238 class String
240   # Converts +self+ to an escaped URI parameter value
241   #   'Foo Bar'.to_param # => 'Foo%20Bar'
242   def to_param
243     URI.escape(self)
244   end
245   
246   # Converts +self+ from an escaped URI parameter value
247   #   'Foo%20Bar'.from_param # => 'Foo Bar'
248   def from_param
249     URI.unescape(self)
250   end
251   
254 class Hash
255   
256   def to_params
257     map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
258   end
259   
260   def symbolize_keys
261     self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
262   end
263   
264   def pass(*keys)
265     reject { |k,v| !keys.include?(k) }
266   end
267   
270 class Symbol
271   
272   def to_proc 
273     Proc.new { |*args| args.shift.__send__(self, *args) }
274   end
275   
278 class Array
279   
280   def to_hash
281     self.inject({}) { |h, (k, v)|  h[k] = v; h }
282   end
283   
284   def to_proc
285     Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
286   end
287   
290 module Enumerable
291   
292   def eject(&block)
293     find { |e| result = block[e] and break result }
294   end
295   
298 ### Core Extension results for throw :halt
300 class Proc
301   def to_result(cx, *args)
302     cx.instance_eval(&self)
303   end
306 class String
307   def to_result(cx, *args)
308     cx.send :_body=, self
309   end
312 class Array
313   def to_result(cx, *args)
314     self.shift.to_result(cx, *self)
315   end
318 class Symbol
319   def to_result(cx, *args)
320     cx.send(self, *args)
321   end
324 class Fixnum
325   def to_result(cx, *args)
326     cx.status self
327     cx.body args.first
328   end