8585118b1769c8209738ba7914d66a8db3419476
[sinatra.git] / lib / sinatra.rb
blob8585118b1769c8209738ba7914d66a8db3419476
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'
12 require 'ostruct'
14 class Class
15   def dslify_writter(*syms)
16     syms.each do |sym|
17       class_eval <<-end_eval
18         def #{sym}(v=nil)
19           self.send "#{sym}=", v if v
20           v
21         end
22       end_eval
23     end
24   end
25 end
27 module Sinatra
28   extend self
30   Result = Struct.new(:block, :params, :status) unless defined?(Result)
31   
32   def application
33     @app ||= Application.new
34   end
35   
36   def application=(app)
37     @app = app
38   end
39   
40   def port
41     application.options.port
42   end
43   
44   def env
45     application.options.env
46   end
47   
48   def run
49     
50     begin
51       puts "== Sinatra has taken the stage on port #{port} for #{env}"
52       require 'pp'
53       Rack::Handler::Mongrel.run(application, :Port => port) do |server|
54         trap(:INT) do
55           server.stop
56           puts "\n== Sinatra has ended his set (crowd applauds)"
57         end
58       end
59     rescue Errno::EADDRINUSE => e
60       puts "== Someone is already performing on port #{port}!"
61     end
62     
63   end
64       
65   class Event
67     URI_CHAR = '[^/?:,&#\.]'.freeze unless defined?(URI_CHAR)
68     PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
69     
70     attr_reader :path, :block, :param_keys, :pattern
71     
72     def initialize(path, &b)
73       @path = path
74       @block = b
75       @param_keys = []
76       regex = @path.to_s.gsub(PARAM) do
77         @param_keys << $1.intern
78         "(#{URI_CHAR}+)"
79       end
80       @pattern = /^#{regex}$/
81     end
82         
83     def invoke(env)
84       return unless pattern =~ env['PATH_INFO'].squeeze('/')
85       params = param_keys.zip($~.captures.map(&:from_param)).to_hash
86       Result.new(block, params, 200)
87     end
88     
89   end
90   
91   class Error
92     
93     attr_reader :code, :block
94     
95     def initialize(code, &b)
96       @code, @block = code, b
97     end
98     
99     def invoke(env)
100       Result.new(block, {}, 404)
101     end
102     
103   end
104   
105   module ResponseHelpers
107     def redirect(path)
108       throw :halt, Redirect.new(path)
109     end
111   end
112   
113   module RenderingHelpers
114     
115     def render(content, options={})
116       template = resolve_template(content, options)
117       @content = _evaluate_render(template)
118       layout = resolve_layout(options[:layout], options)
119       @content = _evaluate_render(layout) if layout
120       @content
121     end
122     
123     private
124       
125       def _evaluate_render(content, options={})
126         case content
127         when String
128           instance_eval(%Q{"#{content}"})
129         when Proc
130           instance_eval(&content)
131         when File
132           instance_eval(%Q{"#{content.read}"})
133         end
134       end
135       
136       def resolve_template(content, options={})
137         case content
138         when String
139           content
140         when Symbol
141           File.new(filename_for(content, options))
142         end
143       end
144     
145       def resolve_layout(name, options={})
146         return if name == false
147         if layout = layouts[name || :layout]
148           return layout
149         end
150         if File.file?(filename = filename_for(name, options))
151           File.new(filename)
152         end
153       end
154       
155       def filename_for(name, options={})
156         (options[:views_directory] || 'views') + "/#{name}.#{ext}"
157       end
158               
159       def ext
160         :html
161       end
163       def layouts
164         Sinatra.application.layouts
165       end
166     
167   end
169   class EventContext
170     
171     include ResponseHelpers
172     include RenderingHelpers
173     
174     attr_accessor :request, :response
175     
176     dslify_writter :status, :body
177     
178     def initialize(request, response, route_params)
179       @request = request
180       @response = response
181       @route_params = route_params
182       @response.body = nil
183     end
184     
185     def params
186       @params ||= @route_params.merge(@request.params).symbolize_keys
187     end
188     
189     def stop(content)
190       throw :halt, content
191     end
192     
193     def complete(returned)
194       @response.body || returned
195     end
196     
197     private
199       def method_missing(name, *args, &b)
200         @response.send(name, *args, &b)
201       end
202     
203   end
204   
205   class Redirect
206     def initialize(path)
207       @path = path
208     end
209     
210     def to_result(cx, *args)
211       cx.status(302)
212       cx.header.merge!('Location' => @path)
213       cx.body = ''
214     end
215   end
216     
217   class Application
218     
219     attr_reader :events, :layouts, :default_options
220     
221     def self.default_options
222       @@default_options ||= {
223         :run => true,
224         :port => 4567,
225         :env => :development
226       }
227     end
228     
229     def default_options
230       self.class.default_options
231     end
232         
233     def initialize
234       @events = Hash.new { |hash, key| hash[key] = [] }
235       @layouts = Hash.new
236     end
237     
238     def define_event(method, path, &b)
239       events[method] << event = Event.new(path, &b)
240       event
241     end
242     
243     def define_layout(name=:layout, &b)
244       layouts[name] = b
245     end
246     
247     def define_error(code, &b)
248       events[:errors][code] = Error.new(code, &b)
249     end
250     
251     def lookup(env)
252       e = events[env['REQUEST_METHOD'].downcase.to_sym].eject(&[:invoke, env])
253       e ||= (events[:errors][404] || basic_not_found).invoke(env)
254     end
255     
256     def basic_not_found
257       Error.new(404) do
258         '<h1>Not Found</h1>'
259       end
260     end
261     
262     def basic_error
263       Error.new(500) do
264         '<h1>Internal Server Error</h1>'
265       end
266     end
268     def options
269       @options ||= OpenStruct.new(default_options)
270     end
271     
272     def call(env)
273       body = nil
274       begin
275         result = lookup(env)
276         context = EventContext.new(
277           Rack::Request.new(env), 
278           Rack::Response.new,
279           result.params
280         )
281         context.status(result.status)
282         returned = catch(:halt) do
283           [:complete, context.instance_eval(&result.block)]
284         end
285         body = returned.to_result(context)
286       rescue => e
287         env['sinatra.error'] = e
288         result = (events[:errors][500] || basic_error).invoke(env)
289         returned = catch(:halt) do
290           [:complete, context.instance_eval(&result.block)]
291         end
292         body = returned.to_result(context)
293         context.status(500)
294       end
295       context.body = String === body ? [*body] : body
296       context.finish
297     end
298     
299   end
300   
303 def get(path, &b)
304   Sinatra.application.define_event(:get, path, &b)
307 def post(path, &b)
308   Sinatra.application.define_event(:post, path, &b)
311 def put(path, &b)
312   Sinatra.application.define_event(:put, path, &b)
315 def delete(path, &b)
316   Sinatra.application.define_event(:delete, path, &b)
319 def helpers(&b)
320   Sinatra::EventContext.class_eval(&b)
323 def error(code, &b)
324   Sinatra.application.define_error(code, &b)
327 def layout(name = :layout, &b)
328   Sinatra.application.define_layout(name, &b)
331 def configures(*envs, &b)
332   yield if  envs.include?(Sinatra.application.options.env) ||
333             envs.empty?
336 ### Misc Core Extensions
338 module Kernel
340   def silence_warnings
341     old_verbose, $VERBOSE = $VERBOSE, nil
342     yield
343   ensure
344     $VERBOSE = old_verbose
345   end
349 class String
351   # Converts +self+ to an escaped URI parameter value
352   #   'Foo Bar'.to_param # => 'Foo%20Bar'
353   def to_param
354     URI.escape(self)
355   end
356   
357   # Converts +self+ from an escaped URI parameter value
358   #   'Foo%20Bar'.from_param # => 'Foo Bar'
359   def from_param
360     URI.unescape(self)
361   end
362   
365 class Hash
366   
367   def to_params
368     map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
369   end
370   
371   def symbolize_keys
372     self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
373   end
374   
375   def pass(*keys)
376     reject { |k,v| !keys.include?(k) }
377   end
378   
381 class Symbol
382   
383   def to_proc 
384     Proc.new { |*args| args.shift.__send__(self, *args) }
385   end
386   
389 class Array
390   
391   def to_hash
392     self.inject({}) { |h, (k, v)|  h[k] = v; h }
393   end
394   
395   def to_proc
396     Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
397   end
398   
401 module Enumerable
402   
403   def eject(&block)
404     find { |e| result = block[e] and break result }
405   end
406   
409 ### Core Extension results for throw :halt
411 class Proc
412   def to_result(cx, *args)
413     cx.instance_eval(&self)
414   end
417 class String
418   def to_result(cx, *args)
419     cx.body = self
420   end
423 class Array
424   def to_result(cx, *args)
425     self.shift.to_result(cx, *self)
426   end
429 class Symbol
430   def to_result(cx, *args)
431     cx.send(self, *args)
432   end
435 class Fixnum
436   def to_result(cx, *args)
437     cx.status self
438     cx.body args.first
439   end
442 class NilClass
443   def to_result(cx, *args)
444     cx.body = ''
445     # log warning here
446   end
449 at_exit do
450   raise $! if $!
451   Sinatra.run if Sinatra.application.options.run
454 ENV['SINATRA_ENV'] = 'test' if $0 =~ /_test\.rb$/
455 Sinatra::Application.default_options.merge!(
456   :env => (ENV['SINATRA_ENV'] || 'development').to_sym
459 configures :development do
460   
461   get '/sinatra_custom_images/:image.png' do
462     File.read(File.dirname(__FILE__) + "/../images/#{params[:image]}.png")
463   end
464   
465   error 404 do
466     %Q(
467     <html>
468       <body style='text-align: center; color: #888; font-family: Arial; font-size: 22px; margin: 20px'>
469       <h2>Sinatra doesn't know this diddy.</h2>
470       <img src='/sinatra_custom_images/404.png'></img>
471       </body>
472     </html>
473     )
474   end
475   
476   error 500 do
477     @error = request.env['sinatra.error']
478     %Q(
479     <html>
480         <body>
481                 <style type="text/css" media="screen">
482                         body {
483                                 font-family: Verdana;
484                                 color: #333;
485                         }
487                         #content {
488                                 width: 700px;
489                                 margin-left: 20px;
490                         }
492                         #content h1 {
493                                 width: 99%;
494                                 color: #1D6B8D;
495                                 font-weight: bold;
496                         }
497                         
498                         #stacktrace {
499                           margin-top: -20px;
500                         }
502                         #stacktrace pre {
503                                 font-size: 12px;
504                                 border-left: 2px solid #ddd;
505                                 padding-left: 10px;
506                         }
508                         #stacktrace img {
509                                 margin-top: 10px;
510                         }
511                 </style>
512                 <div id="content">
513                 <img src="/sinatra_custom_images/500.png" />
514                         <div id="stacktrace">
515                                 <h1>#{@error.message}</h1>
516                                 <pre><code>#{@error.backtrace.join("\n")}</code></pre>
517                 </div>
518         </body>
519     </html>
520     )
521   end
522