camping 1.5.180 → 2.0.rc0

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.
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