camping 1.4.2 → 1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,7 +28,7 @@
28
28
  # http://rubyforge.org/projects/mongrel Mongrel comes with examples
29
29
  # in its <tt>examples/camping</tt> directory.
30
30
  #
31
- %w[rubygems active_record markaby metaid tempfile uri].each { |lib| require lib }
31
+ %w[active_support markaby tempfile uri].each { |lib| require lib }
32
32
 
33
33
  # == Camping
34
34
  #
@@ -42,28 +42,25 @@
42
42
  #
43
43
  # * Camping::Helpers which can be used in controllers and views.
44
44
  #
45
- # == The postamble
45
+ # == The Camping Server
46
46
  #
47
- # Most Camping applications contain the entire application in a single script.
48
- # The script begins by requiring Camping, then fills each of the three modules
49
- # described above with classes and methods. Finally, a postamble puts the wheels
50
- # in motion.
47
+ # How do you run Camping apps? Oh, uh... The Camping Server!
51
48
  #
52
- # if __FILE__ == $0
53
- # Camping::Models::Base.establish_connection :adapter => 'sqlite3',
54
- # :database => 'blog3.db'
55
- # Camping::Models::Base.logger = Logger.new('camping.log')
56
- # Camping.create if Camping.respond_to? :create
57
- # puts Camping.run
58
- # end
49
+ # The Camping Server is, firstly and thusly, a set of rules. At the very least, The Camping Server must:
59
50
  #
60
- # In the postamble, your job is to setup Camping::Models::Base (see: ActiveRecord::Base)
61
- # and call Camping::run in a request loop. The above postamble is for a standard
62
- # CGI setup, where the web server manages the request loop and calls the script once
63
- # for every request.
51
+ # * Load all Camping apps in a directory.
52
+ # * Load new apps that appear in that directory.
53
+ # * Mount those apps according to their filename. (e.g. blog.rb is mounted at /blog.)
54
+ # * Run each app's <tt>create</tt> method upon startup.
55
+ # * Reload the app if its modification time changes.
56
+ # * Reload the app if it requires any files under the same directory and one of their modification times changes.
57
+ # * Support the X-Sendfile header.
64
58
  #
65
- # For other configurations, see
66
- # http://code.whytheluckystiff.net/camping/wiki/PostAmbles
59
+ # In fact, Camping comes with its own little The Camping Server.
60
+ #
61
+ # At a command prompt, run: <tt>camping examples/</tt> and the entire <tt>examples/</tt> directory will be served.
62
+ #
63
+ # Configurations also exist for Apache and Lighttpd. See http://code.whytheluckystiff.net/camping/wiki/TheCampingServer.
67
64
  #
68
65
  # == The <tt>create</tt> method
69
66
  #
@@ -86,9 +83,17 @@
86
83
  #
87
84
  # For more tips, see http://code.whytheluckystiff.net/camping/wiki/GiveUsTheCreateMethod.
88
85
  module Camping
86
+ # Stores an +Array+ of all Camping applications modules. Modules are added
87
+ # automatically by +Camping.goes+.
88
+ #
89
+ # Camping.goes :Blog
90
+ # Camping.goes :Tepee
91
+ # Camping::Apps # => [Blog, Tepee]
92
+ #
93
+ Apps = []
89
94
  C = self
90
- F = __FILE__
91
- S = IO.read(F).gsub(/_+FILE_+/,F.dump)
95
+ S = IO.read(__FILE__).sub(/^ S = I.+$/,'')
96
+ P="Cam\ping Problem!"
92
97
 
93
98
  H = HashWithIndifferentAccess
94
99
  # An object-like Hash, based on ActiveSupport's HashWithIndifferentAccess.
@@ -113,15 +118,16 @@ module Camping
113
118
  # to get the value for the <tt>page</tt> query variable.
114
119
  #
115
120
  # Use the <tt>@cookies</tt> variable in the same fashion to access cookie variables.
121
+ # Also, the <tt>@env</tt> variable is an H containing the HTTP headers and server info.
116
122
  class H
123
+ # Gets or sets keys in the hash.
124
+ #
125
+ # @cookies.my_favorite = :macadamian
126
+ # @cookies.my_favorite
127
+ # => :macadamian
128
+ #
117
129
  def method_missing(m,*a)
118
- if m.to_s =~ /=$/
119
- self[$`] = a[0]
120
- elsif a.empty?
121
- self[m]
122
- else
123
- raise NoMethodError, "#{m}"
124
- end
130
+ m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m]:raise(NoMethodError,"#{m}")
125
131
  end
126
132
  alias_method :u, :regular_update
127
133
  end
@@ -190,18 +196,19 @@ module Camping
190
196
  # is assigned to route <tt>/logout</tt>. The HTML will come out as:
191
197
  #
192
198
  # <div id="menu">
193
- # <a href="http://localhost:3301/frodo/">Home</a>
199
+ # <a href="//localhost:3301/frodo/">Home</a>
194
200
  # <a href="/frodo/profile">Profile</a>
195
201
  # <a href="/frodo/logout">Logout</a>
196
202
  # <a href="http://google.com">Google</a>
197
203
  # </div>
198
204
  #
199
- def R(c,*args)
200
- p = /\(.+?\)/
201
- args.inject(c.urls.find{|x|x.scan(p).size==args.size}.dup){|str,a|
202
- str.sub(p,(a.__send__(a.class.primary_key) rescue a).to_s)
205
+ def R(c,*g)
206
+ p=/\(.+?\)/
207
+ g.inject(c.urls.find{|x|x.scan(p).size==g.size}.dup){|s,a|
208
+ s.sub p,C.escape((a[a.class.primary_key]rescue a))
203
209
  }
204
210
  end
211
+
205
212
  # Shows AR validation errors for the object passed.
206
213
  # There is no output if there are no errors.
207
214
  #
@@ -221,8 +228,8 @@ module Camping
221
228
  #
222
229
  # See AR validation documentation for details on validations.
223
230
  def errors_for(o); ul.errors { o.errors.each_full { |er| li er } } if o.errors.any?; end
224
- # Simply builds the complete URL from a relative or absolute path +p+. If your
225
- # application is running from <tt>/blog</tt>:
231
+ # Simply builds a complete path from a path +p+ within the app. If your application is
232
+ # mounted at <tt>/blog</tt>:
226
233
  #
227
234
  # self / "/view/1" #=> "/blog/view/1"
228
235
  # self / "styles.css" #=> "styles.css"
@@ -231,18 +238,23 @@ module Camping
231
238
  def /(p); p[/^\//]?@root+p:p end
232
239
  # Builds a URL route to a controller or a path, returning a URI object.
233
240
  # This way you'll get the hostname and the port number, a complete URL.
241
+ # No scheme is given (http or https).
234
242
  #
235
243
  # You can use this to grab URLs for controllers using the R-style syntax.
236
244
  # So, if your application is mounted at <tt>http://test.ing/blog/</tt>
237
245
  # and you have a View controller which routes as <tt>R '/view/(\d+)'</tt>:
238
246
  #
239
- # URL(View, @post.id) #=> #<URI:http://test.ing/blog/view/12>
247
+ # URL(View, @post.id) #=> #<URL://test.ing/blog/view/12>
240
248
  #
241
249
  # Or you can use the direct path:
242
250
  #
243
- # self.URL #=> #<URI:http://test.ing/blog/>
244
- # self.URL + "view/12" #=> #<URI:http://test.ing/blog/view/12>
245
- # URL("/view/12") #=> #<URI:http://test.ing/blog/view/12>
251
+ # self.URL #=> #<URL://test.ing/blog/>
252
+ # self.URL + "view/12" #=> #<URL://test.ing/blog/view/12>
253
+ # URL("/view/12") #=> #<URL://test.ing/blog/view/12>
254
+ #
255
+ # Since no scheme is given, you will need to add the scheme yourself:
256
+ #
257
+ # "http" + URL("/view/12") #=> "http://test.ing/blog/view/12"
246
258
  #
247
259
  # It's okay to pass URL strings through this method as well:
248
260
  #
@@ -294,6 +306,8 @@ module Camping
294
306
  module Base
295
307
  include Helpers
296
308
  attr_accessor :input, :cookies, :env, :headers, :body, :status, :root
309
+ Z = "\r\n"
310
+
297
311
  # Display a view, calling it by its method name +m+. If a <tt>layout</tt>
298
312
  # method is found in Camping::Views, it will be used to wrap the HTML.
299
313
  #
@@ -319,10 +333,12 @@ module Camping
319
333
  #
320
334
  # If you have a <tt>layout</tt> method in Camping::Views, it will be used to
321
335
  # wrap the HTML.
322
- def method_missing(m, *a, &b)
323
- str = m==:render ? markaview(*a, &b):eval("markaby.#{m}(*a, &b)")
324
- str = markaview(:layout) { str } if Views.method_defined? :layout
325
- r(200, str.to_s)
336
+ def method_missing(*a,&b)
337
+ a.shift if a[0]==:render
338
+ m=Mab.new({},self)
339
+ s=m.capture{send(*a,&b)}
340
+ s=m.layout{s} if /^_/!~a[0].to_s and m.respond_to?:layout
341
+ s
326
342
  end
327
343
 
328
344
  # Formulate a redirect response: a 302 status with <tt>Location</tt> header
@@ -331,9 +347,12 @@ module Camping
331
347
  #
332
348
  # So, given a root of <tt>http://localhost:3301/articles</tt>:
333
349
  #
334
- # redirect "view/12" # redirects to "http://localhost:3301/articles/view/12"
335
- # redirect View, 12 # redirects to "http://localhost:3301/articles/view/12"
350
+ # redirect "view/12" # redirects to "//localhost:3301/articles/view/12"
351
+ # redirect View, 12 # redirects to "//localhost:3301/articles/view/12"
336
352
  #
353
+ # <b>NOTE:</b> This method doesn't magically exit your methods and redirect.
354
+ # You'll need to <tt>return redirect(...)</tt> if this isn't the last statement
355
+ # in your code.
337
356
  def redirect(*a)
338
357
  r(302,'','Location'=>URL(*a))
339
358
  end
@@ -362,7 +381,7 @@ module Camping
362
381
  fh=H[]
363
382
  for l in @in
364
383
  case l
365
- when "\r\n": break
384
+ when Z: break
366
385
  when /^Content-Disposition: form-data;/
367
386
  fh.u H[*$'.scan(/(?:\s(\w+)="([^"]+)")/).flatten]
368
387
  when /^Content-Type: (.+?)(\r$|\Z)/m
@@ -394,27 +413,24 @@ module Camping
394
413
  @cookies, @input = @k.dup, qs.dup
395
414
  end
396
415
 
397
- def service(*a) #:nodoc:
416
+ # All requests pass through this method before going to the controller. Some magic
417
+ # in Camping can be performed by overriding this method.
418
+ #
419
+ # See http://code.whytheluckystiff.net/camping/wiki/BeforeAndAfterOverrides for more
420
+ # on before and after overrides with Camping.
421
+ def service(*a)
398
422
  @body = send(@method, *a) if respond_to? @method
399
- @headers['Set-Cookie'] = @cookies.map { |k,v| "#{k}=#{C.escape(v)}; path=#{self/"/"}" if v != @k[k] }.compact
423
+ @headers['Set-Cookie'] = @cookies.map { |k,v| "#{k}=#{C.escape(v)}; path=#{self/"/"}" if v != @k[k] } - [nil]
400
424
  self
401
425
  end
402
- def to_s #:nodoc:
403
- "Status: #{@status}\n#{@headers.map{|k,v|[*v].map{|x|"#{k}: #{x}"}*"\n"}*"\n"}\n\n#{@body}"
404
- end
405
- def markaby #:nodoc:
406
- Mab.new( instance_variables.map { |iv|
407
- [iv[1..-1], instance_variable_get(iv)] } )
408
- end
409
- def markaview(m, *a, &b) #:nodoc:
410
- h=markaby
411
- h.send(m, *a, &b)
412
- h.to_s
426
+
427
+ # Used by the web server to convert the current request to a string. If you need to
428
+ # alter the way Camping builds HTTP headers, consider overriding this method.
429
+ def to_s
430
+ "Status: #{@status}#{Z+@headers.map{|k,v|[*v].map{|x|"#{k}: #{x}"}}*Z+Z*2+@body}"
413
431
  end
414
- end
415
432
 
416
- # The R class is the parent class for all routed controllers.
417
- class R; include Base end
433
+ end
418
434
 
419
435
  # Controllers is a module for placing classes which handle URLs. This is done
420
436
  # by defining a route to each class using the Controllers::R method.
@@ -436,6 +452,79 @@ module Camping
436
452
  # NotFound class handles URLs not found. The ServerError class handles exceptions
437
453
  # uncaught by your application.
438
454
  module Controllers
455
+ @r = []
456
+ class << self
457
+ def r #:nodoc:
458
+ @r
459
+ end
460
+ # Add routes to a controller class by piling them into the R method.
461
+ #
462
+ # module Camping::Controllers
463
+ # class Edit < R '/edit/(\d+)', '/new'
464
+ # def get(id)
465
+ # if id # edit
466
+ # else # new
467
+ # end
468
+ # end
469
+ # end
470
+ # end
471
+ #
472
+ # You will need to use routes in either of these cases:
473
+ #
474
+ # * You want to assign multiple routes to a controller.
475
+ # * You want your controller to receive arguments.
476
+ #
477
+ # Most of the time the rules inferred by dispatch method Controllers::D will get you
478
+ # by just fine.
479
+ def R *u
480
+ r=@r
481
+ Class.new {
482
+ meta_def(:urls){u}
483
+ meta_def(:inherited){|x|r<<x}
484
+ }
485
+ end
486
+
487
+ # Dispatch routes to controller classes.
488
+ # For each class, routes are checked for a match based on their order in the routing list
489
+ # given to Controllers::R. If no routes were given, the dispatcher uses a slash followed
490
+ # by the name of the controller lowercased.
491
+ #
492
+ # Controllers are searched in this order:
493
+ #
494
+ # # Classes without routes, since they refer to a very specific URL.
495
+ # # Classes with routes are searched in order of their creation.
496
+ #
497
+ # So, define your catch-all controllers last.
498
+ def D(path)
499
+ r.map { |k|
500
+ k.urls.map { |x|
501
+ return k, $~[1..-1] if path =~ /^#{x}\/?$/
502
+ }
503
+ }
504
+ [NotFound, [path]]
505
+ end
506
+
507
+ # The route maker, this is called by Camping internally, you shouldn't need to call it.
508
+ #
509
+ # Still, it's worth know what this method does. Since Ruby doesn't keep track of class
510
+ # creation order, we're keeping an internal list of the controllers which inherit from R().
511
+ # This method goes through and adds all the remaining routes to the beginning of the list
512
+ # and ensures all the controllers have the right mixins.
513
+ #
514
+ # Anyway, if you are calling the URI dispatcher from outside of a Camping server, you'll
515
+ # definitely need to call this at least once to set things up.
516
+ def M
517
+ def M #:nodoc:
518
+ end
519
+ constants.map { |c|
520
+ k=const_get(c)
521
+ k.send:include,C,Base,Models
522
+ r[0,0]=k if !r.include?k
523
+ k.meta_def(:urls){["/#{c.downcase}"]}if !k.respond_to?:urls
524
+ }
525
+ end
526
+ end
527
+
439
528
  # The NotFound class is a special controller class for handling 404 errors, in case you'd
440
529
  # like to alter the appearance of the 404. The path is passed in as +p+.
441
530
  #
@@ -451,7 +540,11 @@ module Camping
451
540
  # end
452
541
  # end
453
542
  #
454
- class NotFound; def get(p); r(404, div{h1("Cam\ping Problem!");h2("#{p} not found")}); end end
543
+ class NotFound < R()
544
+ def get(p)
545
+ r(404, Mab.new{h1(P);h2("#{p} not found")})
546
+ end
547
+ end
455
548
 
456
549
  # The ServerError class is a special controller class for handling many (but not all) 500 errors.
457
550
  # If there is a parse error in Camping or in your application's source code, it will not be caught
@@ -476,43 +569,18 @@ module Camping
476
569
  # end
477
570
  # end
478
571
  #
479
- class ServerError; include Base; def get(k,m,e); r(500, Mab.new { h1 "Cam\ping Problem!"; h2 "#{k}.#{m}"; h3 "#{e.class} #{e.message}:"; ul { e.backtrace.each { |bt| li bt } } }.to_s) end end
480
-
481
- class << self
482
- # Add routes to a controller class by piling them into the R method.
483
- #
484
- # module Camping::Controllers
485
- # class Edit < R '/edit/(\d+)', '/new'
486
- # def get(id)
487
- # if id # edit
488
- # else # new
489
- # end
490
- # end
491
- # end
492
- # end
493
- #
494
- # You will need to use routes in either of these cases:
495
- #
496
- # * You want to assign multiple routes to a controller.
497
- # * You want your controller to receive arguments.
498
- #
499
- # Most of the time the rules inferred by dispatch method Controllers::D will get you
500
- # by just fine.
501
- def R(*urls); Class.new(R) { meta_def(:urls) { urls } }; end
502
-
503
- # Dispatch routes to controller classes. Classes are searched in no particular order.
504
- # For each class, routes are checked for a match based on their order in the routing list
505
- # given to Controllers::R. If no routes were given, the dispatcher uses a slash followed
506
- # by the name of the controller lowercased.
507
- def D(path)
508
- constants.inject(nil) do |d,c|
509
- k = const_get(c)
510
- k.meta_def(:urls){["/#{c.downcase}"]}if !(k<R)
511
- d||([k, $~[1..-1]] if k.urls.find { |x| path =~ /^#{x}\/?$/ })
512
- end||[NotFound, [path]]
572
+ class ServerError < R()
573
+ def get(k,m,e)
574
+ r(500, Mab.new {
575
+ h1(P)
576
+ h2 "#{k}.#{m}"
577
+ h3 "#{e.class} #{e.message}:"
578
+ ul { e.backtrace.each { |bt| li bt } }
579
+ }.to_s)
513
580
  end
514
581
  end
515
582
  end
583
+ X = Controllers
516
584
 
517
585
  class << self
518
586
  # When you are running many applications, you may want to create independent
@@ -527,7 +595,7 @@ module Camping
527
595
  # module Blog::Views; ... end
528
596
  #
529
597
  def goes(m)
530
- eval(S.gsub(/Camping/,m.to_s),TOPLEVEL_BINDING)
598
+ eval S.gsub(/Camping/,m.to_s).gsub("A\pps = []","Cam\ping::Apps<<self"), TOPLEVEL_BINDING
531
599
  end
532
600
 
533
601
  # URL escapes a string.
@@ -535,14 +603,14 @@ module Camping
535
603
  # Camping.escape("I'd go to the museum straightway!")
536
604
  # #=> "I%27d+go+to+the+museum+straightway%21"
537
605
  #
606
+ def escape(s); s.to_s.gsub(/[^ \w.-]+/n){'%'+($&.unpack('H2'*$&.size)*'%').upcase}.tr(' ', '+') end
538
607
 
539
- def escape(s); s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n){'%'+$1.unpack('H2'*$1.size).join('%').upcase}.tr(' ', '+') end
540
608
  # Unescapes a URL-encoded string.
541
609
  #
542
610
  # Camping.un("I%27d+go+to+the+museum+straightway%21")
543
611
  # #=> "I'd go to the museum straightway!"
544
612
  #
545
- def un(s); s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%')].pack('H*')} end
613
+ def un(s); s.tr('+', ' ').gsub(/%([\da-f]{2})/in){[$1].pack('H*')} end
546
614
 
547
615
  # Parses a query string into an Camping::H object.
548
616
  #
@@ -587,24 +655,40 @@ module Camping
587
655
  # at the center, passing in the read +r+ and write +w+ streams. You will also need to mimick or
588
656
  # pass in the <tt>ENV</tt> replacement as part of your wrapper.
589
657
  #
590
- # if __FILE__ == $0
591
- # require 'fcgi'
592
- # Camping::Models::Base.establish_connection :adapter => 'sqlite3',
593
- # :database => 'blog3.db'
594
- # Camping::Models::Base.logger = Logger.new('camping.log')
595
- # FCGI.each do |req|
596
- # req.out << Camping.run req.in, req.env
597
- # req.finish
598
- # end
599
- # end
600
- # end
658
+ # See Camping::FastCGI and Camping::WEBrick for examples.
601
659
  #
602
660
  def run(r=$stdin,e=ENV)
603
- k, a = Controllers.D un("/#{e['PATH_INFO']}".gsub(%r!/+!,'/'))
604
- k.send :include, C, Base, Models
605
- k.new(r,e,(m=e['REQUEST_METHOD']||"GET")).service(*a)
606
- rescue => x
607
- Controllers::ServerError.new(r,e,'get').service(k,m,x)
661
+ X.M
662
+ k,a=X.D un("/#{e['PATH_INFO']}".gsub(/\/+/,'/'))
663
+ k.new(r,e,(m=e['REQUEST_METHOD']||"GET")).Y.service *a
664
+ rescue Exception=>x
665
+ X::ServerError.new(r,e,'get').service(k,m,x)
666
+ end
667
+
668
+ # The Camping scriptable dispatcher. Any unhandled method call to the app module will
669
+ # be sent to a controller class, specified as an argument.
670
+ #
671
+ # Blog.get(:Index)
672
+ # #=> #<Blog::Controllers::Index ... >
673
+ #
674
+ # The controller object contains all the @cookies, @body, @headers, etc. formulated by
675
+ # the response.
676
+ #
677
+ # You can also feed environment variables and query variables as a hash, the final
678
+ # argument.
679
+ #
680
+ # Blog.post(:Login, :input => {'username' => 'admin', 'password' => 'camping'})
681
+ # #=> #<Blog::Controllers::Login @user=... >
682
+ #
683
+ # Blog.get(:Info, :env => {:HTTP_HOST => 'wagon'})
684
+ # #=> #<Blog::Controllers::Info @env={'HTTP_HOST'=>'wagon'} ...>
685
+ #
686
+ def method_missing(m, c, *a)
687
+ X.M
688
+ k = X.const_get(c).new(StringIO.new,
689
+ H['HTTP_HOST','','SCRIPT_NAME','','HTTP_COOKIE',''],m.to_s)
690
+ H.new(a.pop).each { |e,f| k.send("#{e}=",f) } if Hash === a[-1]
691
+ k.service *a
608
692
  end
609
693
  end
610
694
 
@@ -634,21 +718,8 @@ module Camping
634
718
  #
635
719
  # Models cannot be referred to in Views at this time.
636
720
  module Models
637
- A = ActiveRecord
638
- # Base is an alias for ActiveRecord::Base. The big warning I'm going to give you
639
- # about this: *Base overloads table_name_prefix.* This means that if you have a
640
- # model class Blog::Models::Post, it's table name will be <tt>blog_posts</tt>.
641
- Base = A::Base
642
-
643
- # The default prefix for Camping model classes is the topmost module name lowercase
644
- # and followed with an underscore.
645
- #
646
- # Tepee::Models::Page.table_name_prefix
647
- # #=> "tepee_pages"
648
- #
649
- def Base.table_name_prefix
650
- "#{name[/^(\w+)/,1]}_".downcase.sub(/^(#{A}|camping)_/i,'')
651
- end
721
+ autoload:Base,'camping/db'
722
+ def Y;self;end
652
723
  end
653
724
 
654
725
  # Views is an empty module for storing methods which create HTML. The HTML is described
@@ -672,3 +743,4 @@ module Camping
672
743
  end
673
744
  end
674
745
  end
746
+