camping 1.5.180 → 2.0.rc0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/CHANGELOG +35 -0
  2. data/README +43 -68
  3. data/Rakefile +155 -86
  4. data/bin/camping +64 -246
  5. data/book/01_introduction +19 -0
  6. data/book/02_getting_started +443 -0
  7. data/book/51_upgrading +93 -0
  8. data/doc/api.html +1953 -0
  9. data/doc/book.html +73 -0
  10. data/doc/book/01_introduction.html +57 -0
  11. data/doc/book/02_getting_started.html +573 -0
  12. data/doc/book/51_upgrading.html +146 -0
  13. data/doc/created.rid +1 -0
  14. data/{extras → doc/images}/Camping.gif +0 -0
  15. data/doc/images/loadingAnimation.gif +0 -0
  16. data/{extras → doc/images}/permalink.gif +0 -0
  17. data/doc/index.html +148 -0
  18. data/doc/js/camping.js +79 -0
  19. data/doc/js/jquery.js +32 -0
  20. data/doc/rdoc.css +117 -0
  21. data/examples/blog.rb +280 -181
  22. data/extras/images/badge.gif +0 -0
  23. data/extras/images/boys-life.png +0 -0
  24. data/extras/images/deerputer.png +0 -0
  25. data/extras/images/diagram.png +0 -0
  26. data/extras/images/hill.png +0 -0
  27. data/extras/images/i-wish.png +0 -0
  28. data/extras/images/latl.png +0 -0
  29. data/extras/images/little-wheels.png +0 -0
  30. data/extras/images/square-badge.png +0 -0
  31. data/extras/images/uniform.png +0 -0
  32. data/extras/images/whale-bounce.png +0 -0
  33. data/extras/rdoc/generator/singledarkfish.rb +205 -0
  34. data/extras/rdoc/generator/template/flipbook/images/Camping.gif +0 -0
  35. data/extras/rdoc/generator/template/flipbook/images/loadingAnimation.gif +0 -0
  36. data/extras/rdoc/generator/template/flipbook/images/permalink.gif +0 -0
  37. data/extras/rdoc/generator/template/flipbook/js/camping.js +79 -0
  38. data/extras/rdoc/generator/template/flipbook/js/jquery.js +32 -0
  39. data/extras/rdoc/generator/template/flipbook/page.rhtml +30 -0
  40. data/extras/rdoc/generator/template/flipbook/rdoc.css +117 -0
  41. data/extras/rdoc/generator/template/flipbook/readme.rhtml +31 -0
  42. data/extras/rdoc/generator/template/flipbook/reference.rhtml +71 -0
  43. data/extras/rdoc/generator/template/flipbook/toc.rhtml +43 -0
  44. data/lib/camping-unabridged.rb +420 -481
  45. data/lib/camping.rb +40 -55
  46. data/lib/camping/{db.rb → ar.rb} +5 -8
  47. data/lib/camping/mab.rb +26 -0
  48. data/lib/camping/reloader.rb +175 -147
  49. data/lib/camping/server.rb +178 -0
  50. data/lib/camping/session.rb +34 -121
  51. data/test/apps/env_debug.rb +65 -0
  52. data/test/apps/forms.rb +95 -0
  53. data/test/apps/forward_to_other_controller.rb +60 -0
  54. data/test/apps/migrations.rb +97 -0
  55. data/test/apps/misc.rb +86 -0
  56. data/test/apps/sessions.rb +38 -0
  57. metadata +120 -80
  58. data/doc/camping.1.gz +0 -0
  59. data/examples/campsh.rb +0 -630
  60. data/examples/tepee.rb +0 -242
  61. data/extras/flipbook_rdoc.rb +0 -491
  62. data/lib/camping/fastcgi.rb +0 -244
  63. data/lib/camping/webrick.rb +0 -65
  64. data/test/test_xhtml_trans.rb +0 -55
data/lib/camping.rb CHANGED
@@ -1,55 +1,40 @@
1
- %w[active_support markaby tempfile uri].map{|l|require l}
2
- module Camping;Apps=[];C=self;S=IO.read(__FILE__).sub(/S=I.+$/,'')
3
- P="Cam\ping Problem!";module Helpers;def R(c,*g);p,h=/\(.+?\)/,g.grep(Hash)
4
- (g-=h).inject(c.urls.find{|x|x.scan(p).size==g.size}.dup){|s,a|s.sub p,C.
5
- escape((a[a.class.primary_key]rescue a))}+(h.any?? "?"+h[0].map{|x|x.map{|z|C.
6
- escape z}*"="}*"&": "")end;def URL c='/',*a;c=R(c,*a)if c.
7
- respond_to?:urls;c=self/c;c="//"+@env.HTTP_HOST+c if c[/^\//];URI(c)end;def/p
8
- p[/^\//]?@root+p : p end;def errors_for o;ul.errors{o.errors.each_full{|x|li x}
9
- }if o.errors.any?end end;module Base;include Helpers;attr_accessor:input,
10
- :cookies,:env,:headers,:body,:status,:root;def method_missing*a,&b
11
- a.shift if a[0]==:render;m=Mab.new({},self);s=m.capture{send(*a,&b)}
12
- s=m.capture{send(:layout){s}} if /^_/!~a[0].to_s and m.respond_to?:layout
13
- s end;def r s,b,h={};@status=s;@headers.merge!h;@body=b end
14
- def redirect*a;r 302,'','Location'=>URL(*a)end;Z="\r\n"
15
- def to_a;[@status,@body,@headers]end
16
- def initialize r,e,m;e=H[e.to_hash];@status,@method,@env,@headers,@root=200,m.
17
- downcase,e,{'Content-Type'=>"text/html"},e.SCRIPT_NAME.sub(/\/$/,'')
18
- @k=C.kp e.HTTP_COOKIE;q=C.qsp e.QUERY_STRING;@in=r
19
- if%r|\Amultipart/form-.*boundary=\"?([^\";,]+)|n.match e.CONTENT_TYPE
20
- b=/(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/;until@in.eof?;fh=H[];for l in@in
21
- case l;when Z;break;when/^Content-D.+?: form-data;/;fh.u H[*$'.
22
- scan(/(?:\s(\w+)="([^"]+)")/).flatten];when/^Content-Type: (.+?)(\r$|\Z)/m;fh[
23
- :type]=$1;end;end;fn=fh[:name];o=if fh[:filename];o=fh[:tempfile]=Tempfile.new(:C)
24
- o.binmode;else;fh=""end;while l=@in.read(16384);if l=~b;o<<$`.chomp;@in.seek(-$'.
25
- size,IO::SEEK_CUR);break;end;o<<l;end;C.qsp(fn,'&;',fh,q) if fn;fh[:tempfile].rewind if
26
- fh.is_a?H;end;elsif@method=="post";q.u C.qsp(@in.read)end;@cookies,@input=
27
- @k.dup,q.dup end;def service*a;@body=send(@method,*a)if respond_to?@method
28
- @headers["Set-Cookie"]=@cookies.map{|k,v|"#{k}=#{C.escape(v)}; path=#{self/'/'
29
- }"if v!=@k[k]}-[nil];self end;def to_s;a=[];@headers.map{|k,v|[*v].map{|x|a<<
30
- "#{k}: #{x}"}};"Status: #{@status}#{Z+a*Z+Z*2+@body}"end;end
31
- X=module Controllers;@r=[];class<<self;def r;@r;end;def R*u;r=@r;Class.new{
32
- meta_def(:urls){u};meta_def(:inherited){|x|r<<x}}end;def M;def M;end;constants.map{|c|
33
- k=const_get(c);k.send:include,C,Base,Models;r[0,0]=k if !r.include?k;k.meta_def(
34
- :urls){["/#{c.downcase}"]}if !k.respond_to?:urls}end;def D p;r.map{|k|k.urls.
35
- map{|x|return k,$~[1..-1]if p=~/^#{x}\/?$/}};[NotFound,[p]]end end;class
36
- NotFound<R();def get p;r(404,Mab.new{h1 P;h2 p+" not found"})end end;class
37
- ServerError<R();def get k,m,e;r(500,Mab.new{h1 P;h2"#{k}.#{m}";h3"#{e.class
38
- } #{e.message}:";ul{e.backtrace.each{|bt|li(bt)}}}.to_s)end end;self;end;class<<
39
- self;def goes m;eval S.gsub(/Camping/,m.to_s).gsub("A\pps=[]","Cam\ping::Apps<<\
40
- self"),TOPLEVEL_BINDING;end;def escape s;s.to_s.gsub(/[^ \w.-]+/n){'%'+($&.
41
- unpack('H2'*$&.size)*'%').upcase}.tr(' ','+')end;def un s;s.tr('+',' ').gsub(
42
- /%([\da-f]{2})/in){[$1].pack('H*')}end;def qsp q,d='&;',y=nil,z=H[];m=proc{|_,o,n|o.u(
43
- n,&m)rescue([*o]<<n)};q.to_s.split(/[#{d}] */n).inject((b,z=z,H[])[0]){|h,p|k,v=un(p).
44
- split('=',2);h.u k.split(/[\]\[]+/).reverse.inject(y||v){|x,i|H[i,x]},&m}end;def
45
- kp s;c=qsp(s,';,')end;def run r=$stdin,e=ENV;X.M;k,a=X.D un("/#{e[
46
- 'PATH_INFO']}".gsub(/\/+/,'/'));k.new(r,e,(m=e['REQUEST_METHOD']||"GET")).Y.
47
- service *a;rescue Object=>x;X::ServerError.new(r,e,'get').service(k,m,x)end
48
- def method_missing m,c,*a;X.M;k=X.const_get(c).new(StringIO.new,H['HTTP_HOST',
49
- '','SCRIPT_NAME','','HTTP_COOKIE',''],m.to_s);H.new(a.pop).each{|e,f|k.send(
50
- "#{e}=",f)}if Hash===a[-1];k.service *a;end;end;module Views;include X,Helpers
51
- end;module Models;autoload:Base,'camping/db';def Y;self;end;end;class Mab<Markaby::Builder
52
- include Views;def tag!*g,&b;h=g[-1];[:href,:action,:src].map{|a|(h[a]=self/h[a])rescue
53
- 0};super end end;H=HashWithIndifferentAccess;class H;def method_missing m,*a
54
- m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m]:raise(NoMethodError,"#{m}")end
55
- alias_method:u,:regular_update;end end
1
+ require "uri";require "rack";class Object;def meta_def m,&b;(class<<self;self
2
+ end).send:define_method,m,&b end end;module Camping;C=self;S=IO.read(__FILE__
3
+ )rescue nil;P="<h1>Cam\ping Problem!</h1><h2>%s</h2>";U=Rack::Utils;Apps=[]
4
+ class H<Hash;def method_missing m,*a;m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m.
5
+ to_s]:super end;undef id,type if ??==63;end;module Helpers;def R c,*g;p,h=
6
+ /\(.+?\)/,g.grep(Hash);g-=h;raise"bad route"unless u=c.urls.find{|x|break x if
7
+ x.scan(p).size==g.size&&/^#{x}\/?$/=~(x=g.inject(x){|x,a|x.sub p,U.escape((a[
8
+ a.class.primary_key]rescue a))})};h.any?? u+"?"+U.build_query(h[0]):u end;def
9
+ / p;p[0]==?/?@root + p : p end;def URL c='/',*a;c=R(c, *a) if c.respond_to?(
10
+ :urls);c=self/c;c=@request.url[/.{8,}?(?=\/)/]+c if c[0]==?/;URI c end end
11
+ module Base;attr_accessor:env,:request,:root,:input,:cookies,:state,:status,
12
+ :headers,:body;def render v,*a,&b;mab(/^_/!~v.to_s){send(v,*a,&b)} end;def
13
+ mab l=nil,&b;m=Mab.new({},self);s=m.capture(&b);s=m.capture{layout{s}} if l &&
14
+ m.respond_to?(:layout);s end;def r s,b,h={};b,h=h,b if Hash===b;@status=s;
15
+ @headers.merge!(h);@body=b;end;def redirect *a;r 302,'','Location'=>URL(*a).
16
+ to_s;end;def r404 p;P%"#{p} not found"end;def r500 k,m,e;raise e;end;def r501 m
17
+ P%"#{m.upcase} not implemented"end;def to_a;@env['rack.session']=@state;r=Rack::
18
+ Response.new(@body,@status,@headers);@cookies.each{|k,v|next if @old_cookies[
19
+ k]==v;v={:value=>v,:path=>self/"/"} if String===v;r.set_cookie(k,v)};r.to_a;end
20
+ def initialize(env,m) r=@request=Rack::Request.new(@env=env);@root,@input,
21
+ @cookies,@state,@headers,@status,@method=r.script_name.sub(/\/$/,''),n(r.params
22
+ ),H[@old_cookies = r.cookies],H[r.session],{},m=~/r(\d+)/?$1.to_i: 200,m end;def
23
+ n h;Hash===h ?h.inject(H[]){|m,(k,v)|m[k]=n(v);m}: h end;def service *a;r=catch(
24
+ :halt){send(@method,*a)};@body||=r;self;end;end;module Controllers;@r=[];class<<
25
+ self;def r;@r end;def R *u;r=@r;Class.new{meta_def(:urls){u};meta_def(:inherited
26
+ ){|x|r<<x}}end;def D p,m;p='/'if !p||!p[0];r.map{|k|k.urls.map{|x|return(k.
27
+ instance_method(m)rescue nil)?[k,m,*$~[1..-1]]:[I,'r501',m]if p=~/^#{x}\/?$/}}
28
+ [I,'r404',p] end;N=H.new{|_,x|x.downcase}.merge! "N"=>'(\d+)',"X"=>'([^/]+)',
29
+ "Index"=>'';def M;def M;end;constants.map{|c|k=const_get(c);k.send:include,C,
30
+ Base,Helpers,Models;@r=[k]+r if r-[k]==r;k.meta_def(:urls){ [ "/#{c.scan(
31
+ /.[^A-Z]*/).map(&N.method(:[]))*'/'}"]}if !k.respond_to?:urls}end end;I=R()
32
+ end;X=Controllers;class<<self;def goes m;Apps<<eval(S.gsub(/Camping/,m.to_s),
33
+ TOPLEVEL_BINDING) end;def call e;X.M;p=e['PATH_INFO']=U.unescape(e['PATH_INFO'])
34
+ k,m,*a=X.D p,e['REQUEST_METHOD'].downcase;k.new(e,m).service(*a).to_a;rescue
35
+ r500(:I,k,m,$!,:env=>e).to_a;end;def method_missing m,c,*a;X.M;h=Hash===a[-1]?
36
+ a.pop: {};e=H[Rack::MockRequest.env_for('',h.delete(:env)||{})];k=X.const_get(c
37
+ ).new(e,m.to_s);h.each{|i,v|k.send"#{i}=",v};k.service(*a);end;def use*a,&b;m=a.
38
+ shift.new(method(:call),*a,&b);meta_def(:call){|e|m.call(e)}end end;module Views
39
+ include X,Helpers end;module Models;autoload:Base,
40
+ 'camping/ar';end;autoload:Mab,'camping/mab';C end
@@ -9,13 +9,11 @@ end
9
9
  $AR_EXTRAS = %{
10
10
  Base = ActiveRecord::Base unless const_defined? :Base
11
11
 
12
- def Y; ActiveRecord::Base.verify_active_connections!; self; end
13
-
14
12
  class SchemaInfo < Base
15
13
  end
16
14
 
17
15
  def self.V(n)
18
- @final = [n, @final.to_i].max
16
+ @final = [n, @final.to_f].max
19
17
  m = (@migrations ||= [])
20
18
  Class.new(ActiveRecord::Migration) do
21
19
  meta_def(:version) { n }
@@ -37,7 +35,7 @@ $AR_EXTRAS = %{
37
35
 
38
36
  si = SchemaInfo.find(:first) || SchemaInfo.new(:version => opts[:assume])
39
37
  if si.version < opts[:version]
40
- @migrations.each do |k|
38
+ @migrations.sort_by { |m| m.version }.each do |k|
41
39
  k.migrate(:up) if si.version < k.version and k.version <= opts[:version]
42
40
  k.migrate(:down) if si.version > k.version and k.version > opts[:version]
43
41
  end
@@ -71,8 +69,7 @@ module Camping
71
69
  module_eval $AR_EXTRAS
72
70
  end
73
71
  end
74
- Camping::S.sub! "autoload:Base,'camping/db'", ""
75
- Camping::S.sub! "def Y;self;end", $AR_EXTRAS
76
- Camping::Apps.each do |app|
77
- app::Models.module_eval $AR_EXTRAS
72
+ Camping::S.sub! /autoload\s*:Base\s*,\s*['"]camping\/ar['"]/, $AR_EXTRAS
73
+ Camping::Apps.each do |c|
74
+ c::Models.module_eval $AR_EXTRAS
78
75
  end
@@ -0,0 +1,26 @@
1
+ class MissingLibrary < Exception #:nodoc: all
2
+ end
3
+ begin
4
+ require 'markaby'
5
+ rescue LoadError => e
6
+ raise MissingLibrary, "Markaby could not be loaded (is it installed?): #{e.message}"
7
+ end
8
+
9
+ $MAB_CODE = %{
10
+ # The Mab class wraps Markaby, allowing it to run methods from Camping::Views
11
+ # and also to replace :href, :action and :src attributes in tags by prefixing the root
12
+ # path.
13
+ class Mab < Markaby::Builder
14
+ include Views
15
+ def tag!(*g,&b)
16
+ h=g[-1]
17
+ [:href,:action,:src].map{|a|(h[a]&&=self/h[a])rescue 0}
18
+ super
19
+ end
20
+ end
21
+ }
22
+
23
+ Camping::S.sub! /autoload\s*:Mab\s*,\s*['"]camping\/mab['"]/, $MAB_CODE
24
+ Camping::Apps.each do |c|
25
+ c.module_eval $MAB_CODE
26
+ end
@@ -1,163 +1,191 @@
1
1
  module Camping
2
- # == The Camping Reloader
3
- #
4
- # Camping apps are generally small and predictable. Many Camping apps are
5
- # contained within a single file. Larger apps are split into a handful of
6
- # other Ruby libraries within the same directory.
7
- #
8
- # Since Camping apps (and their dependencies) are loaded with Ruby's require
9
- # method, there is a record of them in $LOADED_FEATURES. Which leaves a
10
- # perfect space for this class to manage auto-reloading an app if any of its
11
- # immediate dependencies changes.
12
- #
13
- # == Wrapping Your Apps
14
- #
15
- # Since bin/camping and the Camping::FastCGI class already use the Reloader,
16
- # you probably don't need to hack it on your own. But, if you're rolling your
17
- # own situation, here's how.
18
- #
19
- # Rather than this:
20
- #
21
- # require 'yourapp'
22
- #
23
- # Use this:
24
- #
25
- # require 'camping/reloader'
26
- # Camping::Reloader.new('/path/to/yourapp.rb')
27
- #
28
- # The reloader will take care of requiring the app and monitoring all files
29
- # for alterations.
30
- class Reloader
31
- attr_accessor :klass, :mtime, :mount, :requires
32
-
33
- # Creates the reloader, assigns a +script+ to it and initially loads the
34
- # application. Pass in the full path to the script, otherwise the script
35
- # will be loaded relative to the current working directory.
36
- def initialize(script)
37
- @script = File.expand_path(script)
38
- @mount = File.basename(script, '.rb')
39
- @requires = nil
40
- load_app
41
- end
42
-
43
- # Find the application, based on the script name.
44
- def find_app(title)
45
- @klass = Object.const_get(Object.constants.grep(/^#{title}$/i)[0]) rescue nil
46
- end
47
-
48
- # If the file isn't found, if we need to remove the app from the global
49
- # namespace, this will be sure to do so and set @klass to nil.
50
- def remove_app
51
- Object.send :remove_const, @klass.name if @klass
52
- @klass = nil
2
+ # == The Camping Reloader
3
+ #
4
+ # Camping apps are generally small and predictable. Many Camping apps are
5
+ # contained within a single file. Larger apps are split into a handful of
6
+ # other Ruby libraries within the same directory.
7
+ #
8
+ # Since Camping apps (and their dependencies) are loaded with Ruby's require
9
+ # method, there is a record of them in $LOADED_FEATURES. Which leaves a
10
+ # perfect space for this class to manage auto-reloading an app if any of its
11
+ # immediate dependencies changes.
12
+ #
13
+ # == Wrapping Your Apps
14
+ #
15
+ # Since bin/camping and the Camping::Server class already use the Reloader,
16
+ # you probably don't need to hack it on your own. But, if you're rolling your
17
+ # own situation, here's how.
18
+ #
19
+ # Rather than this:
20
+ #
21
+ # require 'yourapp'
22
+ #
23
+ # Use this:
24
+ #
25
+ # require 'camping/reloader'
26
+ # reloader = Camping::Reloader.new('/path/to/yourapp.rb')
27
+ # blog = reloader.apps[:Blog]
28
+ # wiki = reloader.apps[:Wiki]
29
+ #
30
+ # The <tt>blog</tt> and <tt>wiki</tt> objects will behave exactly like your
31
+ # Blog and Wiki, but they will update themselves if yourapp.rb changes.
32
+ #
33
+ # You can also give Reloader more than one script.
34
+ class Reloader
35
+ attr_reader :scripts
36
+
37
+ # This is a simple wrapper which causes the script to reload (if needed)
38
+ # on any method call. Then the method call will be forwarded to the
39
+ # app.
40
+ class App < (defined?(BasicObject) ? BasicObject : Object) # :nodoc:
41
+ if superclass == ::Object
42
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
43
+ end
44
+
45
+ attr_accessor :app, :script
46
+
47
+ def initialize(script)
48
+ @script = script
49
+ end
50
+
51
+ # Reloads if needed, before calling the method on the app.
52
+ def method_missing(meth, *args, &blk)
53
+ @script.reload!
54
+ @app.send(meth, *args, &blk)
55
+ end
53
56
  end
54
-
55
- # Loads (or reloads) the application. The reloader will take care of calling
56
- # this for you. You can certainly call it yourself if you feel it's warranted.
57
- def load_app
58
- title = File.basename(@script)[/^([\w_]+)/,1].gsub /_/,''
57
+
58
+ # This class is doing all the hard work; however, it only works on
59
+ # single files. Reloader just wraps up support for multiple scripts
60
+ # and hides away some methods you normally won't need.
61
+ class Script # :nodoc:
62
+ attr_reader :apps, :file, :dir, :extras
63
+
64
+ def initialize(file)
65
+ @file = File.expand_path(file)
66
+ @dir = File.dirname(@file)
67
+ @extras = File.join(@dir, File.basename(@file, ".rb"))
68
+ @mtime = Time.at(0)
69
+ @requires = []
70
+ @apps = {}
71
+ end
72
+
73
+ # Loads the apps availble in this script. Use <tt>apps</tt> to get
74
+ # the loaded apps.
75
+ def load_apps
76
+ all_requires = $LOADED_FEATURES.dup
77
+ all_apps = Camping::Apps.dup
78
+
59
79
  begin
60
- all_requires = $LOADED_FEATURES.dup
61
- load @script
62
- @requires = ($LOADED_FEATURES - all_requires).select do |req|
63
- req.index(File.basename(@script) + "/") == 0 || req.index(title + "/") == 0
64
- end
80
+ load(@file)
65
81
  rescue Exception => e
66
- puts "!! trouble loading #{title}: [#{e.class}] #{e.message}"
67
- puts e.backtrace.join("\n")
68
- find_app title
69
- remove_app
70
- return
82
+ puts "!! Error loading #{@file}:"
83
+ puts "#{e.class}: #{e.message}"
84
+ puts e.backtrace
85
+ puts "!! Error loading #{@file}, see backtrace above"
71
86
  end
72
-
73
- @mtime = mtime
74
- find_app title
75
- unless @klass and @klass.const_defined? :C
76
- puts "!! trouble loading #{title}: not a Camping app, no #{title.capitalize} module found"
77
- remove_app
78
- return
87
+
88
+ @requires = ($LOADED_FEATURES - all_requires).map do |req|
89
+ full = full_path(req)
90
+ full if full == @file or full.index(@extras) == 0
79
91
  end
80
92
 
81
- Reloader.conditional_connect
82
- @klass.create if @klass.respond_to? :create
83
- @klass
84
- end
85
-
86
- # The timestamp of the most recently modified app dependency.
87
- def mtime
88
- ((@requires || []) + [@script]).map do |fname|
89
- fname = fname.gsub(/^#{Regexp::quote File.dirname(@script)}\//, '')
90
- begin
91
- File.mtime(File.join(File.dirname(@script), fname))
92
- rescue Errno::ENOENT
93
- remove_app
94
- @mtime
95
- end
96
- end.max
97
- end
98
-
99
- # Conditional reloading of the app. This gets called on each request and
100
- # only reloads if the modification times on any of the files is updated.
101
- def reload_app
102
- return if @klass and @mtime and mtime <= @mtime
103
-
104
- if @requires
105
- @requires.each { |req| $LOADED_FEATURES.delete(req) }
93
+ @mtime = mtime
94
+
95
+ new_apps = (Camping::Apps - all_apps)
96
+ old_apps = @apps.dup
97
+ @apps = new_apps.inject({}) do |hash, app|
98
+ key = app.name.to_sym
99
+ hash[key] = (old = old_apps[key]) || App.new(self)
100
+ hash[key].app = app
101
+ app.create if app.respond_to?(:create) && !old
102
+ hash
106
103
  end
107
- k = @klass
108
- Object.send :remove_const, k.name if k
109
- load_app
110
- end
111
-
112
- # Conditionally reloads (using reload_app.) Then passes the request through
113
- # to the wrapped Camping app.
114
- def run(*a)
115
- reload_app
116
- if @klass
117
- @klass.run(*a)
104
+ self
105
+ end
106
+
107
+ # Removes all the apps defined in this script.
108
+ def remove_apps
109
+ @apps.each do |name, app|
110
+ Camping::Apps.delete(app.app)
111
+ Object.send :remove_const, name
112
+ end
113
+ end
114
+
115
+ # Reloads the file if needed. No harm is done by calling this multiple
116
+ # times, so feel free call just to be sure.
117
+ def reload!
118
+ return if @mtime >= mtime
119
+ remove_apps
120
+ load_apps
121
+ end
122
+
123
+ # Checks if both scripts watches the same file.
124
+ def ==(other)
125
+ @file == other.file
126
+ end
127
+
128
+ private
129
+
130
+ def mtime
131
+ (@requires + [@file]).compact.map do |fname|
132
+ File.mtime(fname)
133
+ end.reject{|t| t > Time.now }.max
134
+ end
135
+
136
+ # Figures out the full path of a required file.
137
+ def full_path(req)
138
+ dir = $LOAD_PATH.detect { |l| File.exists?(File.join(l, req)) }
139
+ if dir
140
+ File.join(File.expand_path(dir), req)
118
141
  else
119
- Camping.run(*a)
142
+ req
120
143
  end
144
+ end
121
145
  end
122
146
 
123
- # Returns source code for the main script in the application.
124
- def view_source
125
- File.read(@script)
147
+ # Creates the reloader, assigns a +script+ to it and initially loads the
148
+ # application. Pass in the full path to the script, otherwise the script
149
+ # will be loaded relative to the current working directory.
150
+ def initialize(*scripts)
151
+ @scripts = []
152
+ update(*scripts)
126
153
  end
127
-
128
- class << self
129
- def database=(db)
130
- @database = db
131
- end
132
- def log=(log)
133
- @log = log
134
- end
135
- def conditional_connect
136
- # If database models are present, `autoload?` will return nil.
137
- unless Camping::Models.autoload? :Base
138
- require 'logger'
139
- require 'camping/session'
140
- Camping::Models::Base.establish_connection @database if @database
141
-
142
- case @log
143
- when Logger
144
- Camping::Models::Base.logger = @log
145
- when String
146
- Camping::Models::Base.logger = Logger.new(@log == "-" ? STDOUT : @log)
147
- end
148
-
149
- Camping::Models::Session.create_schema
150
-
151
- if @database and @database[:adapter] == 'sqlite3'
152
- begin
153
- require 'sqlite3_api'
154
- rescue LoadError
155
- puts "!! Your SQLite3 adapter isn't a compiled extension."
156
- abort "!! Please check out http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 for tips."
157
- end
158
- end
159
- end
154
+
155
+ # Updates the reloader to only use the scripts provided:
156
+ #
157
+ # reloader.update("examples/blog.rb", "examples/wiki.rb")
158
+ def update(*scripts)
159
+ old = @scripts.dup
160
+ clear
161
+ @scripts = scripts.map do |script|
162
+ s = Script.new(script)
163
+ if pos = old.index(s)
164
+ # We already got a script, so we use the old (which might got a mtime)
165
+ old[pos]
166
+ else
167
+ s.load_apps
160
168
  end
169
+ end
161
170
  end
162
- end
171
+
172
+ # Removes all the scripts from the reloader.
173
+ def clear
174
+ @scrips = []
175
+ end
176
+
177
+ # Simply calls reload! on all the Script objects.
178
+ def reload!
179
+ @scripts.each { |script| script.reload! }
180
+ end
181
+
182
+ # Returns a Hash of all the apps available in the scripts, where the key
183
+ # would be the name of the app (the one you gave to Camping.goes) and the
184
+ # value would be the app (wrapped inside App).
185
+ def apps
186
+ @scripts.inject({}) do |hash, script|
187
+ hash.merge(script.apps)
188
+ end
189
+ end
190
+ end
163
191
  end