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.
@@ -1,51 +1,52 @@
1
- %w[rubygems active_record markaby metaid tempfile uri].each{|l|require l}
2
- module Camping;C=self;F=__FILE__;S=IO.read(F).gsub(/_+FILE_+/,F.dump)
3
- module Helpers;def R c,*args;p=/\(.+?\)/;args.inject(c.urls.find{|x|x.scan(p).
4
- size==args.size}.dup){|str,a|str.sub(p,(a.__send__(a.class.primary_key)rescue
5
- a).to_s)} end;def URL c='/',*a;c=R(c,*a)if c.respond_to?:urls;c=self/c;c=
6
- "//"+@env.HTTP_HOST+c if c[/^\//];URI(c) end;def / p;p[/^\//]?@root+p:p end
7
- def errors_for o;ul.errors{o.errors.each_full{|x|li x}}if o.errors.any? end end
8
- module Base;include Helpers;attr_accessor :input,:cookies,:env,:headers,:body,
9
- :status,:root;def method_missing m,*a,&b;s=m==:render ? markaview(*a,&b):eval(
10
- "markaby.#{m}(*a,&b)");s=markaview(:layout){s} if Views.method_defined?:layout
11
- r 200,s.to_s end;def r s,b,h={};@status=s;@headers.merge!(h);@body=b end;def
12
- redirect *a;r 302,'','Location'=>URL(*a) end;def initialize r,e,m;e=H[e.to_hash]
13
- @status,@method,@env,@headers,@root=200,m.downcase,e,{'Content-Type'=>"text/htm\
14
- l"},e.SCRIPT_NAME.sub(/\/$/,'');@k=C.kp e.HTTP_COOKIE;q=C.qs_parse e.QUERY_STRING
15
- @in=r;if %r|\Amultipart/form-.*boundary=\"?([^\";,]+)|n.match e.CONTENT_TYPE;b=
16
- /(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/;until @in.eof?;fh=H[];for l in
17
- @in;case l;when "\r\n":break;when /^Content-Disposition: form-data;/:fh.u H[*$'.
18
- scan(/(?:\s(\w+)="([^"]+)")/).flatten];when /^Content-Type: (.+?)(\r$|\Z)/m;fh[
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=/\(.+?\)/;g.inject(c.
4
+ urls.find{|x|x.scan(p).size==g.size}.dup){|s,a|s.sub p,C.escape((a[
5
+ a.class.primary_key]rescue a))}end;def URL c='/',*a;c=R(c,*a)if c.
6
+ respond_to?:urls;c=self/c;c="//"+@env.HTTP_HOST+c if c[/^\//];URI(c)end;def/p
7
+ p[/^\//]?@root+p : p end;def errors_for o;ul.errors{o.errors.each_full{|x|li x}
8
+ }if o.errors.any?end end;module Base;include Helpers;attr_accessor:input,
9
+ :cookies,:env,:headers,:body,:status,:root;def method_missing*a,&b
10
+ a.shift if a[0]==:render;m=Mab.new({},self);s=m.capture{send(*a,&b)};s=m.layout{s}if
11
+ /^_/!~a[0].to_s and m.respond_to?:layout;s end;def r s,b,h={};@status=s;@headers.
12
+ merge!h;@body=b end;def redirect*a;r 302,'','Location'=>URL(*a)end;Z="\r\n"
13
+ def initialize r,e,m;e=H[e.to_hash];@status,@method,@env,@headers,@root=200,m.
14
+ downcase,e,{'Content-Type'=>"text/html"},e.SCRIPT_NAME.sub(/\/$/,'')
15
+ @k=C.kp e.HTTP_COOKIE;q=C.qs_parse e.QUERY_STRING;@in=r
16
+ if%r|\Amultipart/form-.*boundary=\"?([^\";,]+)|n.match e.CONTENT_TYPE
17
+ b=/(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/;until@in.eof?;fh=H[];for l in@in
18
+ case l;when Z;break;when/^Content-D.+?: form-data;/;fh.u H[*$'.
19
+ scan(/(?:\s(\w+)="([^"]+)")/).flatten];when/^Content-Type: (.+?)(\r$|\Z)/m;fh[
19
20
  :type]=$1;end;end;fn=fh[:name];o=if fh[:filename];o=fh[:tempfile]=Tempfile.new(:C)
20
- o.binmode;else;fh="";end;while l=@in.read(16384);if l=~b;o<<$`.chomp;@in.seek(-$'.
21
+ o.binmode;else;fh=""end;while l=@in.read(16384);if l=~b;o<<$`.chomp;@in.seek(-$'.
21
22
  size,IO::SEEK_CUR);break;end;o<<l;end;q[fn]=fh if fn;fh[:tempfile].rewind if
22
- fh.is_a?H;end;elsif @method=="post";q.u C.qs_parse(@in.read) end;@cookies,@input=
23
- @k.dup,q.dup end;def service *a;@body=send(@method,*a)if respond_to?@method
24
- @headers["Set-Cookie"]=@cookies.map{|k,v|"#{k}=#{C.escape(v)}; path=#{self/"/"}\
25
- " if v != @k[k]}.compact;self end;def to_s;"Status: #{@status}\n#{@headers.map{
26
- |k,v|[*v].map{|x|"#{k}: #{x}"}*"\n"}*"\n"}\n\n#{@body}" end;def markaby;Mab.new(
27
- instance_variables.map{|iv|[iv[1..-1],instance_variable_get(iv)]}) end;def
28
- markaview m,*a,&b;h=markaby;h.send m,*a,&b;h.to_s end end;class R;include Base
29
- end;module Controllers;class NotFound;def get p;r(404,div{h1 "Cam\ping Problem!"
30
- h2 p+" not found"}) end end;class ServerError;include Base;def get k,m,e;r(500,
31
- Mab.new{h1 "Cam\ping Problem!";h2 "#{k}.#{m}";h3 "#{e.class} #{e.message}:";ul{
32
- e.backtrace.each{|bt|li(bt)}}}.to_s)end end;class<<self;def R *urls;Class.new(
33
- R){meta_def(:urls){urls}}end;def D path;constants.inject(nil){|d,c|k=const_get c
34
- k.meta_def(:urls){["/#{c.downcase}"]}if !(k<R);d||([k,$~[1..-1]]if k.urls.find{
35
- |x|path=~/^#{x}\/?$/})}||[NotFound,[path]] end end end;class<<self;def goes m
36
- eval(S.gsub(/Camping/,m.to_s),TOPLEVEL_BINDING)end;def escape s;s.to_s.gsub(
37
- /([^ a-zA-Z0-9_.-]+)/n){'%'+$1.unpack('H2'*$1.size).join('%').upcase}.tr ' ','+'
38
- end;def un s;s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%'
39
- )].pack('H*')} end;def qs_parse q,d='&;';m=proc{|_,o,n|o.u(n,&m)rescue([*o
40
- ]<<n)};q.to_s.split(/[#{d}] */n).inject(H[]){|h,p|k,v=un(p).split('=',2)
41
- h.u k.split(/[\]\[]+/).reverse.inject(v){|x,i|H[i,x]},&m}end;def kp(s);c=
42
- qs_parse(s,';,')end;def run r=$stdin,e=ENV;k,a=Controllers.D un("/#{e['PATH_INFO']
43
- }".gsub(/\/+/,'/'));k.send:include,C,Base,Models;k.new(r,e,(m=e['REQUEST_METHOD'
44
- ]||"GET")).service *a;rescue=>x;Controllers::ServerError.new(r,e,'get').service(
45
- k,m,x)end end;module Views;include Controllers,Helpers end;module Models;A=
46
- ActiveRecord;Base=A::Base;def Base.table_name_prefix;"#{name[/^(\w+)/,1]}_".
47
- downcase.sub(/^(#{A}|camping)_/i,'')end end;class Mab<Markaby::Builder;include \
48
- Views;def tag! *g,&b;h=g[-1];[:href,:action,:src].map{|a|(h[a]=self/h[a])rescue
23
+ fh.is_a?H;end;elsif@method=="post";q.u C.qs_parse(@in.read)end;@cookies,@input=
24
+ @k.dup,q.dup end;def service*a;@body=send(@method,*a)if respond_to?@method
25
+ @headers["Set-Cookie"]=@cookies.map{|k,v|"#{k}=#{C.escape(v)}; path=#{self/'/'
26
+ }"if v!=@k[k]}-[nil];self end;def to_s;"Status: #{@status}#{Z+@headers.map{|k,v|
27
+ [*v].map{|x|"#{k}: #{x}"}}*Z+Z*2+@body}"end;end
28
+ X=module Controllers;@r=[];class<<self;def r;@r;end;def R*u;r=@r;Class.new{
29
+ meta_def(:urls){u};meta_def(:inherited){|x|r<<x}}end;def M;def M;end;constants.map{|c|
30
+ k=const_get(c);k.send:include,C,Base,Models;r[0,0]=k if !r.include?k;k.meta_def(
31
+ :urls){["/#{c.downcase}"]}if !k.respond_to?:urls}end;def D p;r.map{|k|k.urls.
32
+ map{|x|return k,$~[1..-1]if p=~/^#{x}\/?$/}};[NotFound,[p]]end end;class
33
+ NotFound<R();def get p;r(404,Mab.new{h1 P;h2 p+" not found"})end end;class
34
+ ServerError<R();def get k,m,e;r(500,Mab.new{h1 P;h2"#{k}.#{m}";h3"#{e.class
35
+ } #{e.message}:";ul{e.backtrace.each{|bt|li(bt)}}}.to_s)end end;self;end;class<<
36
+ self;def goes m;eval S.gsub(/Camping/,m.to_s).gsub("A\pps=[]","Cam\ping::Apps<<\
37
+ self"),TOPLEVEL_BINDING;end;def escape s;s.to_s.gsub(/[^ \w.-]+/n){'%'+($&.
38
+ unpack('H2'*$&.size)*'%').upcase}.tr(' ','+')end;def un s;s.tr('+',' ').gsub(
39
+ /%([\da-f]{2})/in){[$1].pack('H*')}end;def qs_parse q,d='&;';m=proc{|_,o,n|o.u(
40
+ n,&m)rescue([*o]<<n)};q.to_s.split(/[#{d}] */n).inject(H[]){|h,p|k,v=un(p).
41
+ split('=',2);h.u k.split(/[\]\[]+/).reverse.inject(v){|x,i|H[i,x]},&m}end;def
42
+ kp s;c=qs_parse(s,';,')end;def run r=$stdin,e=ENV;X.M;k,a=X.D un("/#{e[
43
+ 'PATH_INFO']}".gsub(/\/+/,'/'));k.new(r,e,(m=e['REQUEST_METHOD']||"GET")).Y.
44
+ service *a;rescue Exception=>x;X::ServerError.new(r,e,'get').service(k,m,x)end
45
+ def method_missing m,c,*a;X.M;k=X.const_get(c).new(StringIO.new,H['HTTP_HOST',
46
+ '','SCRIPT_NAME','','HTTP_COOKIE',''],m.to_s);H.new(a.pop).each{|e,f|k.send(
47
+ "#{e}=",f)}if Hash===a[-1];k.service *a;end;end;module Views;include X,Helpers
48
+ end;module Models;autoload:Base,'camping/db';def Y;self;end;end;class Mab<Markaby::Builder
49
+ include Views;def tag!*g,&b;h=g[-1];[:href,:action,:src].map{|a|(h[a]=self/h[a])rescue
49
50
  0};super end end;H=HashWithIndifferentAccess;class H;def method_missing m,*a
50
- if m.to_s=~/=$/;self[$`]=a[0];elsif a.empty?;self[m];else;raise NoMethodError,
51
- "#{m}" end end;alias_method:u,:regular_update;end end
51
+ m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m]:raise(NoMethodError,"#{m}")end
52
+ alias_method:u,:regular_update;end end
@@ -0,0 +1,78 @@
1
+ class MissingLibrary < Exception #:nodoc: all
2
+ end
3
+ begin
4
+ require 'active_record'
5
+ rescue LoadError => e
6
+ raise MissingLibrary, "ActiveRecord could not be loaded (is it installed?): #{e.message}"
7
+ end
8
+
9
+ $AR_EXTRAS = %{
10
+ Base = ActiveRecord::Base unless const_defined? :Base
11
+
12
+ def Y; ActiveRecord::Base.verify_active_connections!; self; end
13
+
14
+ class SchemaInfo < Base
15
+ end
16
+
17
+ def self.V(n)
18
+ @final = [n, @final.to_i].max
19
+ m = (@migrations ||= [])
20
+ Class.new(ActiveRecord::Migration) do
21
+ meta_def(:version) { n }
22
+ meta_def(:inherited) { |k| m << k }
23
+ end
24
+ end
25
+
26
+ def self.create_schema(opts = {})
27
+ opts[:assume] ||= 0
28
+ opts[:version] ||= @final
29
+ if @migrations
30
+ unless SchemaInfo.table_exists?
31
+ ActiveRecord::Schema.define do
32
+ create_table SchemaInfo.table_name do |t|
33
+ t.column :version, :float
34
+ end
35
+ end
36
+ end
37
+
38
+ si = SchemaInfo.find(:first) || SchemaInfo.new(:version => opts[:assume])
39
+ if si.version < opts[:version]
40
+ @migrations.each do |k|
41
+ k.migrate(:up) if si.version < k.version and k.version <= opts[:version]
42
+ k.migrate(:down) if si.version > k.version and k.version > opts[:version]
43
+ end
44
+ si.update_attributes(:version => opts[:version])
45
+ end
46
+ end
47
+ end
48
+ }
49
+
50
+ module Camping
51
+ module Models
52
+ A = ActiveRecord
53
+ # Base is an alias for ActiveRecord::Base. The big warning I'm going to give you
54
+ # about this: *Base overloads table_name_prefix.* This means that if you have a
55
+ # model class Blog::Models::Post, it's table name will be <tt>blog_posts</tt>.
56
+ #
57
+ # ActiveRecord is not loaded if you never reference this class. The minute you
58
+ # use the ActiveRecord or Camping::Models::Base class, then the ActiveRecord library
59
+ # is loaded.
60
+ Base = A::Base
61
+
62
+ # The default prefix for Camping model classes is the topmost module name lowercase
63
+ # and followed with an underscore.
64
+ #
65
+ # Tepee::Models::Page.table_name_prefix
66
+ # #=> "tepee_pages"
67
+ #
68
+ def Base.table_name_prefix
69
+ "#{name[/\w+/]}_".downcase.sub(/^(#{A}|camping)_/i,'')
70
+ end
71
+ module_eval $AR_EXTRAS
72
+ end
73
+ 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
78
+ end
@@ -0,0 +1,198 @@
1
+ # == About camping/fastcgi.rb
2
+ #
3
+ # Camping works very well with FastCGI, since your application is only loaded
4
+ # once -- when FastCGI starts. In addition, this class lets you mount several
5
+ # Camping apps under a single FastCGI process, to help save memory costs.
6
+ #
7
+ # So where do you use the Camping::FastCGI class? Use it in your application's
8
+ # postamble and then you can point your web server directly at your application.
9
+ # See Camping::FastCGI docs for more.
10
+ require 'camping'
11
+ require 'fcgi'
12
+
13
+ module Camping
14
+ # Camping::FastCGI is a small class for hooking one or more Camping apps up to
15
+ # FastCGI. Generally, you'll use this class in your application's postamble.
16
+ #
17
+ # == The Smallest Example
18
+ #
19
+ # if __FILE__ == $0
20
+ # require 'camping/fastcgi'
21
+ # Camping::FastCGI.start(YourApp)
22
+ # end
23
+ #
24
+ # This example is stripped down to the basics. The postamble has no database
25
+ # connection. It just loads this class and calls Camping::FastCGI.start.
26
+ #
27
+ # Now, in Lighttpd or Apache, you can point to your app's file, which will
28
+ # be executed, only to discover that your app now speaks the FastCGI protocol.
29
+ #
30
+ # Here's a sample lighttpd.conf (tested with Lighttpd 1.4.11) to serve as example:
31
+ #
32
+ # server.port = 3044
33
+ # server.bind = "127.0.0.1"
34
+ # server.modules = ( "mod_fastcgi" )
35
+ # server.document-root = "/var/www/camping/blog/"
36
+ # server.errorlog = "/var/www/camping/blog/error.log"
37
+ #
38
+ # #### fastcgi module
39
+ # fastcgi.server = ( "/" => (
40
+ # "localhost" => (
41
+ # "socket" => "/tmp/camping-blog.socket",
42
+ # "bin-path" => "/var/www/camping/blog/blog.rb",
43
+ # "check-local" => "disable",
44
+ # "max-procs" => 1 ) ) )
45
+ #
46
+ # The file <tt>/var/www/camping/blog/blog.rb</tt> is the Camping app with
47
+ # the postamble.
48
+ #
49
+ # == Mounting Many Apps
50
+ #
51
+ # require 'camping/fastcgi'
52
+ # fast = Camping::FastCGI.new
53
+ # fast.mount("/blog", Blog)
54
+ # fast.mount("/tepee", Tepee)
55
+ # fast.mount("/", Index)
56
+ # fast.start
57
+ #
58
+ class FastCGI
59
+ CHUNK_SIZE=(4 * 1024)
60
+
61
+ # Creates a Camping::FastCGI class with empty mounts.
62
+ def initialize
63
+ @mounts = {}
64
+ end
65
+ # Mounts a Camping application. The +dir+ being the name of the directory
66
+ # to serve as the application's root. The +app+ is a Camping class.
67
+ def mount(dir, app)
68
+ dir.gsub!(/\/{2,}/, '/')
69
+ dir.gsub!(/\/+$/, '')
70
+ @mounts[dir] = app
71
+ end
72
+ #
73
+ # Starts the FastCGI main loop.
74
+ def start
75
+ FCGI.each do |req|
76
+ dir, app = nil
77
+ begin
78
+ root, path = "/"
79
+ if ENV['FORCE_ROOT'] and ENV['FORCE_ROOT'].to_i == 1
80
+ path = req.env['REQUEST_URI']
81
+ else
82
+ root = req.env['SCRIPT_NAME']
83
+ path = req.env['PATH_INFO']
84
+ end
85
+
86
+ dir, app = @mounts.max { |a,b| match(path, a[0]) <=> match(path, b[0]) }
87
+ unless dir and app
88
+ dir, app = '/', Camping
89
+ end
90
+ yield dir, app if block_given?
91
+
92
+ req.env['SERVER_SCRIPT_NAME'] = req.env['SCRIPT_NAME']
93
+ req.env['SERVER_PATH_INFO'] = req.env['PATH_INFO']
94
+ req.env['SCRIPT_NAME'] = File.join(root, dir)
95
+ req.env['PATH_INFO'] = path.gsub(/^#{dir}/, '')
96
+
97
+ controller = app.run(req.in, req.env)
98
+ sendfile = nil
99
+ headers = {}
100
+ controller.headers.each do |k, v|
101
+ if k =~ /^X-SENDFILE$/i and !ENV['SERVER_X_SENDFILE']
102
+ sendfile = v
103
+ else
104
+ headers[k] = v
105
+ end
106
+ end
107
+
108
+ body = controller.body
109
+ controller.body = ""
110
+ controller.headers = headers
111
+
112
+ req.out << controller.to_s
113
+ if sendfile
114
+ File.open(sendfile, "rb") do |f|
115
+ while chunk = f.read(CHUNK_SIZE) and chunk.length > 0
116
+ req.out << chunk
117
+ end
118
+ end
119
+ elsif body.respond_to? :read
120
+ while chunk = body.read(CHUNK_SIZE) and chunk.length > 0
121
+ req.out << chunk
122
+ end
123
+ body.close if body.respond_to? :close
124
+ else
125
+ req.out << body.to_s
126
+ end
127
+ rescue Exception => e
128
+ req.out << "Content-Type: text/html\r\n\r\n" +
129
+ "<h1>Camping Problem!</h1>" +
130
+ "<h2><strong>#{root}</strong>#{path}</h2>" +
131
+ "<h3>#{e.class} #{esc e.message}</h3>" +
132
+ "<ul>" + e.backtrace.map { |bt| "<li>#{esc bt}</li>" }.join + "</ul>" +
133
+ "<hr /><p>#{req.env.inspect}</p>"
134
+ ensure
135
+ req.finish
136
+ end
137
+ end
138
+ end
139
+
140
+ # A simple single-app starter mechanism
141
+ #
142
+ # Camping::FastCGI.start(Blog)
143
+ #
144
+ def self.start(app)
145
+ cf = Camping::FastCGI.new
146
+ cf.mount("/", app)
147
+ cf.start
148
+ end
149
+
150
+ # Serve an entire directory of Camping apps. (See
151
+ # http://code.whytheluckystiff.net/camping/wiki/TheCampingServer.)
152
+ #
153
+ # Use this method inside your FastCGI dispatcher:
154
+ #
155
+ # #!/usr/local/bin/ruby
156
+ # require 'rubygems'
157
+ # require 'camping/fastcgi'
158
+ # Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => "/path/to/db"
159
+ # Camping::FastCGI.serve("/home/why/cvs/camping/examples")
160
+ #
161
+ def self.serve(path, index=nil)
162
+ require 'camping/reloader'
163
+ if File.directory? path
164
+ fast = Camping::FastCGI.new
165
+ script_load = proc do |script|
166
+ app = Camping::Reloader.new(script)
167
+ fast.mount("/#{app.mount}", app)
168
+ app
169
+ end
170
+ Dir[File.join(path, '*.rb')].each &script_load
171
+ fast.mount("/", index) if index
172
+
173
+ fast.start do |dir, app|
174
+ Dir[File.join(path, dir, '*.rb')].each do |script|
175
+ smount = "/" + File.basename(script, '.rb')
176
+ script_load[script] unless @mounts.has_key? smount
177
+ end
178
+ end
179
+ else
180
+ start(Camping::Reloader.new(path))
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def match(path, mount)
187
+ m = path.match(/^#{Regexp::quote mount}(\/|$)/)
188
+ if m; m.end(0)
189
+ else -1
190
+ end
191
+ end
192
+
193
+ def esc(str)
194
+ str.gsub(/&/n, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
195
+ end
196
+
197
+ end
198
+ end
@@ -0,0 +1,169 @@
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
53
+ 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 /_/,''
59
+ 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
65
+ 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
71
+ 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
79
+ end
80
+
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) }
106
+ 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)
118
+ else
119
+ Camping.run(*a)
120
+ end
121
+ end
122
+
123
+ # Returns source code for the main script in the application.
124
+ def view_source
125
+ File.read(@script)
126
+ 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
+ begin
150
+ Camping::Models::Session.create_schema
151
+ rescue MissingSourceFile
152
+ puts "** #$0 stopped: SQLite3 not found, please install."
153
+ puts "** See http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 for instructions."
154
+ exit
155
+ end
156
+
157
+ if @database and @database[:adapter] == 'sqlite3'
158
+ begin
159
+ require 'sqlite3_api'
160
+ rescue LoadError
161
+ puts "!! Your SQLite3 adapter isn't a compiled extension."
162
+ abort "!! Please check out http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 for tips."
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end