camping 1.4.2 → 1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+