root / tags / 1.2 / lib / camping-unabridged.rb

Revision 33, 16.8 kB (checked in by why, 3 years ago)

camping.gemspec: generate rdoc.
lib/camping-unabridged.rb: subtle ommissions for Helpers::/ and Base#redirect.

Line 
1%w[rubygems active_record markaby metaid ostruct tempfile].each { |lib| require lib }
2
3# == Camping
4#
5# The camping module contains three modules for separating your application:
6#
7# * Camping::Models for storing classes derived from ActiveRecord::Base.
8# * Camping::Controllers for storing controller classes, which map URLs to code.
9# * Camping::Views for storing methods which generate HTML.
10#
11# Of use to you is also one module for storing helpful additional methods:
12#
13# * Camping::Helpers which can be used in controllers and views.
14#
15# == The postamble
16#
17# Most Camping applications contain the entire application in a single script.
18# The script begins by requiring Camping, then fills each of the three modules
19# described above with classes and methods.  Finally, a postamble puts the wheels
20# in motion.
21#
22#   if __FILE__ == $0
23#     Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
24#     Camping::Models::Base.logger = Logger.new('camping.log')
25#     Camping.run
26#   end
27#
28# In the postamble, your job is to setup Camping::Models::Base (see: ActiveRecord::Base)
29# and call Camping::run in a request loop.  The above postamble is for a standard
30# CGI setup, where the web server manages the request loop and calls the script once
31# for every request.
32#
33# For other configurations, see
34# http://code.whytheluckystiff.net/camping/wiki/PostAmbles
35module Camping
36  C = self
37  S = File.read(__FILE__).gsub(/_{2}FILE_{2}/,__FILE__.dump)
38
39  # Helpers contains methods available in your controllers and views.
40  module Helpers
41    # From inside your controllers and views, you will often need to figure out
42    # the route used to get to a certain controller +c+.  Pass the controller class
43    # and any arguments into the R method, a string containing the route will be
44    # returned to you.
45    #
46    # Assuming you have a specific route in an edit controller:
47    #
48    #   class Edit < R '/edit/(\d+)'
49    #
50    # A specific route to the Edit controller can be built with:
51    #
52    #   R(Edit, 1)
53    #
54    # Which outputs: <tt>/edit/1</tt>.
55    #
56    # You may also pass in a model object and the ID of the object will be used.
57    #
58    # If a controller has many routes, the route will be selected if it is the
59    # first in the routing list to have the right number of arguments.
60    #
61    # Keep in mind that this route doesn't include the root path.  Occassionally
62    # you will need to use <tt>/</tt> (the slash method above).
63    def R(c,*args)
64      p = /\(.+?\)/
65      args.inject(c.urls.detect{|x|x.scan(p).size==args.size}.dup){|str,a|
66        str.sub(p,(a.method(a.class.primary_key)[] rescue a).to_s)
67      }
68    end
69    # Shows AR validation errors for the object passed.
70    # There is no output if there are no errors.
71    #
72    # An example might look like:
73    #
74    #   errors_for @post
75    #
76    # Might (depending on actual data) render something like this in Markaby:
77    #
78    #   ul.errors do
79    #     li "Body can't be empty"
80    #     li "Title must be unique"
81    #   end
82    #
83    # Add a simple ul.errors {color:red; font-weight:bold;} CSS rule and you
84    # have built-in, usable error checking in only one line of code. :-)
85    #
86    # See AR validation documentation for details on validations.
87    def errors_for(o); ul.errors { o.errors.each_full { |er| li er } } unless o.errors.empty?; end
88    # Simply builds the complete URL from a relative or absolute path +p+.  If your
89    # application is running from <tt>/blog</tt>:
90    #
91    #   self / "/view/1"    #=> "/blog/view/1"
92    #   self / "styles.css" #=> "styles.css"
93    #   self / R(Edit, 1)   #=> "/blog/edit/1"
94    #
95    def /(p); p[/^\//]?@root+p:p end
96  end
97
98  # Controllers is a module for placing classes which handle URLs.  This is done
99  # by defining a route to each class using the Controllers::R method.
100  #
101  #   module Camping::Controllers
102  #     class Edit < R '/edit/(\d+)'
103  #       def get; end
104  #       def post; end
105  #     end
106  #   end
107  #
108  # If no route is set, Camping will guess the route from the class name.
109  # The rule is very simple: the route becomes a slash followed by the lowercased
110  # class name.  See Controllers::D for the complete rules of dispatch.
111  #
112  # == Special classes
113  #
114  # There are two special classes used for handling 404 and 500 errors.  The
115  # NotFound class handles URLs not found.  The ServerError class handles exceptions
116  # uncaught by your application.
117  module Controllers
118    # Controllers::Base is built into each controller by way of the generic routing
119    # class Controllers::R.  In some ways, this class is trying to do too much, but
120    # it saves code for all the glue to stay in one place.
121    #
122    # Forgivable, considering that it's only really a handful of methods and accessors.
123    #
124    # == Treating controller methods like Response objects
125    #
126    # Camping originally came with a barebones Response object, but it's often much more readable
127    # to just use your controller as the response.
128    #
129    # Go ahead and alter the status, cookies, headers and body instance variables as you
130    # see fit in order to customize the response.
131    #
132    #   module Camping::Controllers
133    #     class SoftLink
134    #       def get
135    #         redirect "/"
136    #       end
137    #     end
138    #   end
139    #
140    # Is equivalent to:
141    #
142    #   module Camping::Controllers
143    #     class SoftLink
144    #       def get
145    #         @status = 302
146    #         @headers['Location'] = "/"
147    #       end
148    #     end
149    #   end
150    #
151    module Base
152      include Helpers
153      attr_accessor :input, :cookies, :headers, :body, :status, :root
154      # Display a view, calling it by its method name +m+.  If a <tt>layout</tt>
155      # method is found in Camping::Views, it will be used to wrap the HTML.
156      #
157      #   module Camping::Controllers
158      #     class Show
159      #       def get
160      #         @posts = Post.find :all
161      #         render :index
162      #       end
163      #     end
164      #   end
165      #
166      def render(m); end; undef_method :render
167
168      # Any stray method calls will be passed to Markaby.  This means you can reply
169      # with HTML directly from your controller for quick debugging.
170      #
171      #   module Camping::Controllers
172      #     class Info
173      #       def get; code ENV.inspect end
174      #     end
175      #   end
176      #
177      # If you have a <tt>layout</tt> method in Camping::Views, it will be used to
178      # wrap the HTML.
179      def method_missing(m, *args, &blk)
180        str = m==:render ? markaview(*args, &blk):eval("markaby.#{m}(*args, &blk)")
181        str = markaview(:layout) { str } rescue nil
182        r(200, str.to_s)
183      end
184
185      # Formulate a redirect response: a 302 status with <tt>Location</tt> header
186      # and a blank body.  If +c+ is a string, the root path will be added.  If
187      # +c+ is a controller class, Helpers::R will be used to route the redirect
188      # and the root path will be added.
189      #
190      # So, given a root of <tt>/articles</tt>:
191      #
192      #   redirect "view/12"    # redirects to "/articles/view/12"
193      #   redirect View, 12     # redirects to "/articles/view/12"
194      #
195      def redirect(c, *args)
196        c = R(c,*args) if c.respond_to? :urls
197        r(302, '', 'Location' => self/c)
198      end
199
200      # A quick means of setting this controller's status, body and headers.
201      # Used internally by Camping, but... by all means...
202      #
203      #   r(302, '', 'Location' => self / "/view/12")
204      #
205      # Is equivalent to:
206      #
207      #   redirect "/view/12"
208      #
209      def r(s, b, h = {}); @status = s; @headers.merge!(h); @body = b; end
210
211      def service(r, e, m, a) #:nodoc:
212        @status, @headers, @root = 200, {}, e['SCRIPT_NAME']
213        cook = C.cookie_parse(e['HTTP_COOKIE'] || e['COOKIE'])
214        qs = C.qs_parse(e['QUERY_STRING'])
215        if "POST" == m
216          inp = r.read(e['CONTENT_LENGTH'].to_i)
217          if %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)|n.match(e['CONTENT_TYPE'])
218            b = "--#$1"
219            inp.split(/(?:\r?\n|\A)#{ Regexp::quote( b ) }(?:--)?\r\n/m).each { |pt|
220              h,v=pt.split("\r\n\r\n",2);fh={}
221              [:name, :filename].each { |x|
222                fh[x] = $1 if h =~ /^Content-Disposition: form-data;.*(?:\s#{x}="([^"]+)")/m
223              }
224              fn = fh[:name]
225              if fh[:filename]
226                fh[:type]=$1 if h =~ /^Content-Type: (.+?)(\r\n|\Z)/m
227                fh[:tempfile]=Tempfile.new("#{C}").instance_eval {binmode;write v;rewind;self}
228              else
229                fh=v
230              end
231              qs[fn]=fh if fn
232            }
233          else
234            qs.merge!(C.qs_parse(inp))
235          end
236        end
237        @cookies, @input = [cook, qs].map{|_|OpenStruct.new(_)}
238
239        @body = method( m.downcase ).call(*a)
240        @headers['Set-Cookie'] = @cookies.marshal_dump.map { |k,v| "#{k}=#{C.escape(v)}; path=/" if v != cook[k] }.compact
241        self
242      end
243      def to_s #:nodoc:
244        "Status: #{@status}\n#{{'Content-Type'=>'text/html'}.merge(@headers).map{|k,v|v.to_a.map{|v2|"#{k}: #{v2}"}}.flatten.join("\n")}\n\n#{@body}"
245      end
246      private
247      def markaby
248          Mab.new( instance_variables.map { |iv|
249            [iv[1..-1], instance_variable_get(iv)] }, {} )
250      end
251      def markaview(m, *args, &blk)
252        b=markaby
253        b.method(m).call(*args, &blk)
254        b.to_s
255      end
256    end
257
258    # The R class is the parent class for all controllers and ensures they all get the Base mixin.
259    class R; include Base end
260
261    # The NotFound class is a special controller class for handling 404 errors, in case you'd
262    # like to alter the appearance of the 404.  The path is passed in as +p+.
263    #
264    #   module Camping::Controllers
265    #     class NotFound
266    #       def get(p)
267    #         @status = 404
268    #         div do
269    #           h1 'Camping Problem!'
270    #           h2 "#{p} not found"
271    #         end
272    #       end
273    #     end
274    #   end
275    #
276    class NotFound; def get(p); r(404, div{h1("#{C} Problem!")+h2("#{p} not found")}); end end
277
278    # The ServerError class is a special controller class for handling many (but not all) 500 errors.
279    # If there is a parse error in Camping or in your application's source code, it will not be caught
280    # by Camping.  The controller class +k+ and request method +m+ (GET, POST, etc.) where the error
281    # took place are passed in, along with the Exception +e+ which can be mined for useful info.
282    #
283    #   module Camping::Controllers
284    #     class ServerError
285    #       def get(k,m,e)
286    #         @status = 500
287    #         div do
288    #           h1 'Camping Problem!'
289    #           h2 "in #{k}.#{m}"
290    #           h3 "#{e.class} #{e.message}:"
291    #           ul do
292    #             e.backtrace.each do |bt|
293    #               li bt
294    #             end
295    #           end
296    #         end
297    #       end
298    #     end
299    #   end
300    #
301    class ServerError; include Base; def get(k,m,e); r(500, markaby.div{ h1 "#{C} Problem!"; h2 "#{k}.#{m}"; h3 "#{e.class} #{e.message}:"; ul { e.backtrace.each { |bt| li bt } } }) end end
302
303    class << self
304      # Add routes to a controller class by piling them into the R method.
305      #
306      #   module Camping::Controllers
307      #     class Edit < R '/edit/(\d+)', '/new'
308      #       def get(id)
309      #         if id   # edit
310      #         else    # new
311      #         end
312      #       end
313      #     end
314      #   end
315      #
316      # You will need to use routes in either of these cases:
317      #
318      # * You want to assign multiple routes to a controller.
319      # * You want your controller to receive arguments.
320      #
321      # Most of the time the rules inferred by dispatch method Controllers::D will get you
322      # by just fine.
323      def R(*urls); Class.new(R) { meta_def(:inherited) { |c| c.meta_def(:urls) { urls } } }; end
324
325      # Dispatch routes to controller classes.  Classes are searched in no particular order.
326      # For each class, routes are checked for a match based on their order in the routing list
327      # given to Controllers::R.  If no routes were given, the dispatcher uses a slash followed
328      # by the name of the controller lowercased.
329      def D(path)
330        constants.inject(nil) do |d,c|
331            k = const_get(c)
332            k.meta_def(:urls){["/#{c.downcase}"]}if !(k<R)
333            d||([k, $~[1..-1]] if k.urls.find { |x| path =~ /^#{x}\/?$/ })
334        end||[NotFound, [path]]
335      end
336    end
337  end
338
339  class << self
340    # When you are running many applications, you may want to create independent
341    # modules for each Camping application.  Namespaces for each.  Camping::goes
342    # defines a toplevel constant with the whole MVC rack inside.
343    #
344    #   require 'camping'
345    #   Camping.goes :Blog
346    #
347    #   module Blog::Controllers; ... end
348    #   module Blog::Models;      ... end
349    #   module Blog::Views;       ... end
350    #
351    def goes(m)
352        eval(S.gsub(/Camping/,m.to_s),TOPLEVEL_BINDING)
353    end
354
355    # URL escapes a string.
356    #
357    #   Camping.escape("I'd go to the museum straightway!") 
358    #     #=> "I%27d+go+to+the+museum+straightway%21"
359    #
360
361    def escape(s); s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n){'%'+$1.unpack('H2'*$1.size).join('%').upcase}.tr(' ', '+') end
362    # Unescapes a URL-encoded string.
363    #
364    #   Camping.unescape("I%27d+go+to+the+museum+straightway%21")
365    #     #=> "I'd go to the museum straightway!"
366    #
367    def unescape(s); s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%')].pack('H*')} end
368
369    # Parses a query string into an OpenStruct object.
370    #
371    #   input = Camping.qs_parse("name=Philarp+Tremain&hair=sandy+blonde")
372    #   input.name
373    #     #=> "Philarp Tremaine"
374    #
375    def qs_parse(qs, d = '&;'); (qs||'').split(/[#{d}] */n).
376        inject({}){|hsh, p|k, v = p.split('=',2).map {|v| unescape(v)}; hsh[k] = v unless v.blank?; hsh} end
377
378    # Parses a string of cookies from the <tt>Cookie</tt> header.
379    def cookie_parse(s); c = qs_parse(s, ';,'); end
380
381    # Fields a request through Camping.  For traditional CGI applications, the method can be
382    # executed without arguments.
383    #
384    #   if __FILE__ == $0
385    #     Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
386    #     Camping::Models::Base.logger = Logger.new('camping.log')
387    #     Camping.run
388    #   end
389    #
390    # For FastCGI and Webrick-loaded applications, you will need to use a request loop, with <tt>run</tt>
391    # at the center, passing in the read +r+ and write +w+ streams.  You will also need to mimick or
392    # replace <tt>ENV</tt> as part of your wrapper.
393    #
394    #   if __FILE__ == $0
395    #     require 'fcgi'
396    #       Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
397    #       Camping::Models::Base.logger = Logger.new('camping.log')
398    #       FCGI.each do |req|
399    #         ENV.replace req.env
400    #         Camping.run req.in, req.out
401    #         req.finish
402    #       end
403    #     end
404    #   end
405    #
406    def run(r=$stdin,w=$stdout)
407      w <<
408        begin
409          k, a = Controllers.D "/#{ENV['PATH_INFO']}".gsub(%r!/+!,'/')
410          m = ENV['REQUEST_METHOD']||"GET"
411          k.class_eval { include C; include Controllers::Base; include Models }
412          o = k.new
413          o.service(r, ENV, m, a)
414        rescue => e
415          Controllers::ServerError.new.service(r, ENV, "GET", [k,m,e])
416        end
417    end
418  end
419
420  # Models is an empty Ruby module for housing model classes derived
421  # from ActiveRecord::Base.  As a shortcut, you may derive from Base
422  # which is an alias for ActiveRecord::Base.
423  #
424  #   module Camping::Models
425  #     class Post < Base; belongs_to :user end
426  #     class User < Base; has_many :posts end
427  #   end
428  #
429  # == Where Models are Used
430  #
431  # Models are used in your controller classes.  However, if your model class
432  # name conflicts with a controller class name, you will need to refer to it
433  # using the Models module.
434  #
435  #   module Camping::Controllers
436  #     class Post < R '/post/(\d+)'
437  #       def get(post_id)
438  #         @post = Models::Post.find post_id
439  #         render :index
440  #       end
441  #     end
442  #   end
443  #
444  # Models cannot be referred to in Views at this time.
445  module Models; end
446
447  # Views is an empty module for storing methods which create HTML.  The HTML is described
448  # using the Markaby language.
449  #
450  # == Using the layout method
451  #
452  # If your Views module has a <tt>layout</tt> method defined, it will be called with a block
453  # which will insert content from your view.
454  module Views; include Controllers; include Helpers end
455  Models::Base = ActiveRecord::Base
456 
457  # The Mab class wraps Markaby, allowing it to run methods from Camping::Views
458  # and also to replace :href and :action attributes in tags by prefixing the root
459  # path.
460  class Mab < Markaby::Builder
461      include Views
462      def tag!(*g,&b)
463          h=g[-1]
464          [:href,:action].each{|a|(h[a]=self/h[a])rescue 0}
465          super
466      end
467  end
468end
Note: See TracBrowser for help on using the browser.