agent testing!
[sinatra.git] / lib / sinatra.rb
blobf17ff701adce4ba8ef65e7ce61b2f2bb94a97fae
1 require 'rubygems'
2 require 'metaid'
4 if ENV['SWIFT']
5  require 'swiftcore/swiftiplied_mongrel'
6  puts "Using Swiftiplied Mongrel"
7 elsif ENV['EVENT']
8   require 'swiftcore/evented_mongrel' 
9   puts "Using Evented Mongrel"
10 end
12 require 'rack'
13 require 'ostruct'
15 class Class
16   def dslify_writter(*syms)
17     syms.each do |sym|
18       class_eval <<-end_eval
19         def #{sym}(v=nil)
20           self.send "#{sym}=", v if v
21           v
22         end
23       end_eval
24     end
25   end
26 end
28 module Sinatra
29   extend self
31   Result = Struct.new(:block, :params, :status) unless defined?(Result)
32   
33   def application
34     @app ||= Application.new
35   end
36   
37   def application=(app)
38     @app = app
39   end
40   
41   def port
42     application.options.port
43   end
44   
45   def env
46     application.options.env
47   end
48   
49   def build_application
50     Rack::CommonLogger.new(application)
51   end
52   
53   def run
54     
55     begin
56       puts "== Sinatra has taken the stage on port #{port} for #{env}"
57       require 'pp'
58       Rack::Handler::Mongrel.run(build_application, :Port => port) do |server|
59         trap(:INT) do
60           server.stop
61           puts "\n== Sinatra has ended his set (crowd applauds)"
62         end
63       end
64     rescue Errno::EADDRINUSE => e
65       puts "== Someone is already performing on port #{port}!"
66     end
67     
68   end
69       
70   class Event
72     URI_CHAR = '[^/?:,&#\.]'.freeze unless defined?(URI_CHAR)
73     PARAM = /:(#{URI_CHAR}+)/.freeze unless defined?(PARAM)
74     SPLAT = /(.*?)/
75     attr_reader :path, :block, :param_keys, :pattern, :options
76     
77     def initialize(path, options = {}, &b)
78       @path = path
79       @block = b
80       @param_keys = []
81       @options = options
82       regex = @path.to_s.gsub(PARAM) do
83         @param_keys << $1.intern
84         "(#{URI_CHAR}+)"
85       end
86       
87       regex.gsub!('*', SPLAT.to_s)
88       
89       @pattern = /^#{regex}$/
90     end
91         
92     def invoke(env)
93       if options[:agent] 
94         return unless env['HTTP_USER_AGENT'] =~ options[:agent]
95       end
96       return unless pattern =~ env['PATH_INFO'].squeeze('/')
97       params = param_keys.zip($~.captures.map(&:from_param)).to_hash
98       Result.new(block, params, 200)
99     end
100     
101   end
102   
103   class Error
104     
105     attr_reader :code, :block
106     
107     def initialize(code, &b)
108       @code, @block = code, b
109     end
110     
111     def invoke(env)
112       Result.new(block, {}, 404)
113     end
114     
115   end
116   
117   class Static
118             
119     def invoke(env)
120       return unless File.file?(
121         Sinatra.application.options.public + env['PATH_INFO']
122       )
123       Result.new(block, {}, 200)
124     end
125     
126     def block
127       Proc.new do
128         send_file Sinatra.application.options.public + 
129           request.env['PATH_INFO']
130       end
131     end
132     
133   end
134   
135   module ResponseHelpers
137     def redirect(path)
138       throw :halt, Redirect.new(path)
139     end
140     
141     def send_file(filename)
142       throw :halt, SendFile.new(filename)
143     end
145   end
146   
147   module RenderingHelpers
148     
149     def render(content, options={})
150       template = resolve_template(content, options)
151       @content = _evaluate_render(template)
152       layout = resolve_layout(options[:layout], options)
153       @content = _evaluate_render(layout) if layout
154       @content
155     end
156     
157     private
158       
159       def _evaluate_render(content, options={})
160         case content
161         when String
162           instance_eval(%Q{"#{content}"})
163         when Proc
164           instance_eval(&content)
165         when File
166           instance_eval(%Q{"#{content.read}"})
167         end
168       end
169       
170       def resolve_template(content, options={})
171         case content
172         when String
173           content
174         when Symbol
175           File.new(filename_for(content, options))
176         end
177       end
178     
179       def resolve_layout(name, options={})
180         return if name == false
181         if layout = layouts[name || :layout]
182           return layout
183         end
184         if File.file?(filename = filename_for(name, options))
185           File.new(filename)
186         end
187       end
188       
189       def filename_for(name, options={})
190         (options[:views_directory] || 'views') + "/#{name}.#{ext}"
191       end
192               
193       def ext
194         :html
195       end
197       def layouts
198         Sinatra.application.layouts
199       end
200     
201   end
203   class EventContext
204     
205     include ResponseHelpers
206     include RenderingHelpers
207     
208     attr_accessor :request, :response
209     
210     dslify_writter :status, :body
211     
212     def initialize(request, response, route_params)
213       @request = request
214       @response = response
215       @route_params = route_params
216       @response.body = nil
217     end
218     
219     def params
220       @params ||= @route_params.merge(@request.params).symbolize_keys
221     end
222     
223     def stop(content)
224       throw :halt, content
225     end
226     
227     def complete(returned)
228       @response.body || returned
229     end
230     
231     private
233       def method_missing(name, *args, &b)
234         @response.send(name, *args, &b)
235       end
236     
237   end
238   
239   class Redirect
240     def initialize(path)
241       @path = path
242     end
243     
244     def to_result(cx, *args)
245       cx.status(302)
246       cx.header.merge!('Location' => @path)
247       cx.body = ''
248     end
249   end
250     
251   class SendFile
252     def initialize(filename)
253       @filename = filename
254     end
255     
256     def to_result(cx, *args)
257       cx.body = File.read(@filename)
258     end
259   end
260     
261   class Application
262     
263     attr_reader :events, :layouts, :default_options
264     
265     def self.default_options
266       @@default_options ||= {
267         :run => true,
268         :port => 4567,
269         :env => :development,
270         :root => Dir.pwd,
271         :public => Dir.pwd + '/public'
272       }
273     end
274     
275     def default_options
276       self.class.default_options
277     end
278         
279     def initialize
280       @events = Hash.new { |hash, key| hash[key] = [] }
281       @layouts = Hash.new
282     end
283     
284     def define_event(method, path, options = {}, &b)
285       events[method] << event = Event.new(path, options, &b)
286       event
287     end
288     
289     def define_layout(name=:layout, &b)
290       layouts[name] = b
291     end
292     
293     def define_error(code, options = {}, &b)
294       events[:errors][code] = Error.new(code, &b)
295     end
296     
297     def static
298       @static ||= Static.new
299     end
300     
301     def lookup(env)
302       method = env['REQUEST_METHOD'].downcase.to_sym
303       e = static.invoke(env) 
304       e ||= events[method].eject(&[:invoke, env])
305       e ||= (events[:errors][404] || basic_not_found).invoke(env)
306       e
307     end
308     
309     def basic_not_found
310       Error.new(404) do
311         '<h1>Not Found</h1>'
312       end
313     end
314     
315     def basic_error
316       Error.new(500) do
317         '<h1>Internal Server Error</h1>'
318       end
319     end
321     def options
322       @options ||= OpenStruct.new(default_options)
323     end
324     
325     def call(env)
326       result = lookup(env)
327       context = EventContext.new(
328         Rack::Request.new(env), 
329         Rack::Response.new,
330         result.params
331       )
332       begin
333         context.status(result.status)
334         returned = catch(:halt) do
335           [:complete, context.instance_eval(&result.block)]
336         end
337         body = returned.to_result(context)
338         context.body = String === body ? [*body] : body
339         context.finish
340       rescue => e
341         raise e if options.raise_errors
342         env['sinatra.error'] = e
343         result = (events[:errors][500] || basic_error).invoke(env)
344         returned = catch(:halt) do
345           [:complete, context.instance_eval(&result.block)]
346         end
347         body = returned.to_result(context)
348         context.status(500)
349         context.body = String === body ? [*body] : body
350         context.finish
351       end
352     end
353     
354   end
355   
358 def get(path, options ={}, &b)
359   Sinatra.application.define_event(:get, path, options, &b)
362 def post(path, options ={}, &b)
363   Sinatra.application.define_event(:post, path, options, &b)
366 def put(path, options ={}, &b)
367   Sinatra.application.define_event(:put, path, options, &b)
370 def delete(path, options ={}, &b)
371   Sinatra.application.define_event(:delete, path, options, &b)
374 def helpers(&b)
375   Sinatra::EventContext.class_eval(&b)
378 def error(code, options = {}, &b)
379   Sinatra.application.define_error(code, options, &b)
382 def layout(name = :layout, &b)
383   Sinatra.application.define_layout(name, &b)
386 def configures(*envs, &b)
387   yield if  envs.include?(Sinatra.application.options.env) ||
388             envs.empty?
390 alias :configure :configures
392 ### Misc Core Extensions
394 module Kernel
396   def silence_warnings
397     old_verbose, $VERBOSE = $VERBOSE, nil
398     yield
399   ensure
400     $VERBOSE = old_verbose
401   end
405 class String
407   # Converts +self+ to an escaped URI parameter value
408   #   'Foo Bar'.to_param # => 'Foo%20Bar'
409   def to_param
410     URI.escape(self)
411   end
412   
413   # Converts +self+ from an escaped URI parameter value
414   #   'Foo%20Bar'.from_param # => 'Foo Bar'
415   def from_param
416     URI.unescape(self)
417   end
418   
421 class Hash
422   
423   def to_params
424     map { |k,v| "#{k}=#{URI.escape(v)}" }.join('&')
425   end
426   
427   def symbolize_keys
428     self.inject({}) { |h,(k,v)| h[k.to_sym] = v; h }
429   end
430   
431   def pass(*keys)
432     reject { |k,v| !keys.include?(k) }
433   end
434   
437 class Symbol
438   
439   def to_proc 
440     Proc.new { |*args| args.shift.__send__(self, *args) }
441   end
442   
445 class Array
446   
447   def to_hash
448     self.inject({}) { |h, (k, v)|  h[k] = v; h }
449   end
450   
451   def to_proc
452     Proc.new { |*args| args.shift.__send__(self[0], *(args + self[1..-1])) }
453   end
454   
457 module Enumerable
458   
459   def eject(&block)
460     find { |e| result = block[e] and break result }
461   end
462   
465 ### Core Extension results for throw :halt
467 class Proc
468   def to_result(cx, *args)
469     cx.instance_eval(&self)
470   end
473 class String
474   def to_result(cx, *args)
475     cx.body = self
476   end
479 class Array
480   def to_result(cx, *args)
481     self.shift.to_result(cx, *self)
482   end
485 class Symbol
486   def to_result(cx, *args)
487     cx.send(self, *args)
488   end
491 class Fixnum
492   def to_result(cx, *args)
493     cx.status self
494     cx.body args.first
495   end
498 class NilClass
499   def to_result(cx, *args)
500     cx.body = ''
501     # log warning here
502   end
505 at_exit do
506   raise $! if $!
507   Sinatra.run if Sinatra.application.options.run
510 ENV['SINATRA_ENV'] = 'test' if $0 =~ /_test\.rb$/
511 Sinatra::Application.default_options.merge!(
512   :env => (ENV['SINATRA_ENV'] || 'development').to_sym
515 configures :development do
516   
517   get '/sinatra_custom_images/:image.png' do
518     File.read(File.dirname(__FILE__) + "/../images/#{params[:image]}.png")
519   end
520   
521   error 404 do
522     %Q(
523     <html>
524       <body style='text-align: center; color: #888; font-family: Arial; font-size: 22px; margin: 20px'>
525       <h2>Sinatra doesn't know this diddy.</h2>
526       <img src='/sinatra_custom_images/404.png'></img>
527       </body>
528     </html>
529     )
530   end
531   
532   error 500 do
533     @error = request.env['sinatra.error']
534     %Q(
535     <html>
536         <body>
537                 <style type="text/css" media="screen">
538                         body {
539                                 font-family: Verdana;
540                                 color: #333;
541                         }
543                         #content {
544                                 width: 700px;
545                                 margin-left: 20px;
546                         }
548                         #content h1 {
549                                 width: 99%;
550                                 color: #1D6B8D;
551                                 font-weight: bold;
552                         }
553                         
554                         #stacktrace {
555                           margin-top: -20px;
556                         }
558                         #stacktrace pre {
559                                 font-size: 12px;
560                                 border-left: 2px solid #ddd;
561                                 padding-left: 10px;
562                         }
564                         #stacktrace img {
565                                 margin-top: 10px;
566                         }
567                 </style>
568                 <div id="content">
569                 <img src="/sinatra_custom_images/500.png" />
570                         <div id="stacktrace">
571                                 <h1>#{@error.message}</h1>
572                                 <pre><code>#{@error.backtrace.join("\n")}</code></pre>
573                 </div>
574         </body>
575     </html>
576     )
577   end
578