picnic 0.7.1 → 0.8.0

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 (45) hide show
  1. data/History.txt +10 -0
  2. data/Manifest.txt +30 -13
  3. data/Rakefile +2 -0
  4. data/lib/picnic.rb +5 -120
  5. data/lib/picnic/authentication.rb +40 -4
  6. data/lib/picnic/cli.rb +86 -43
  7. data/lib/picnic/conf.rb +45 -43
  8. data/lib/picnic/logger.rb +41 -0
  9. data/lib/picnic/server.rb +99 -0
  10. data/lib/picnic/version.rb +2 -2
  11. data/picnic.gemspec +44 -0
  12. data/vendor/{camping-1.5.180 → camping-2.0.20090212}/CHANGELOG +17 -10
  13. data/vendor/{camping-1.5.180 → camping-2.0.20090212}/COPYING +0 -0
  14. data/vendor/{camping-1.5.180 → camping-2.0.20090212}/README +2 -2
  15. data/vendor/{camping-1.5.180 → camping-2.0.20090212}/Rakefile +62 -5
  16. data/vendor/camping-2.0.20090212/bin/camping +99 -0
  17. data/vendor/camping-2.0.20090212/doc/camping.1.gz +0 -0
  18. data/vendor/camping-2.0.20090212/examples/README +5 -0
  19. data/vendor/camping-2.0.20090212/examples/blog.rb +375 -0
  20. data/vendor/camping-2.0.20090212/examples/campsh.rb +629 -0
  21. data/vendor/camping-2.0.20090212/examples/tepee.rb +242 -0
  22. data/vendor/camping-2.0.20090212/extras/Camping.gif +0 -0
  23. data/vendor/camping-2.0.20090212/extras/flipbook_rdoc.rb +491 -0
  24. data/vendor/camping-2.0.20090212/extras/permalink.gif +0 -0
  25. data/vendor/{camping-1.5.180 → camping-2.0.20090212}/lib/camping-unabridged.rb +168 -294
  26. data/vendor/camping-2.0.20090212/lib/camping.rb +54 -0
  27. data/vendor/{camping-1.5.180/lib/camping/db.rb → camping-2.0.20090212/lib/camping/ar.rb} +4 -4
  28. data/vendor/{camping-1.5.180/lib/camping → camping-2.0.20090212/lib/camping/ar}/session.rb +23 -14
  29. data/vendor/camping-2.0.20090212/lib/camping/mab.rb +26 -0
  30. data/vendor/camping-2.0.20090212/lib/camping/reloader.rb +163 -0
  31. data/vendor/camping-2.0.20090212/lib/camping/server.rb +158 -0
  32. data/vendor/camping-2.0.20090212/lib/camping/session.rb +74 -0
  33. data/vendor/camping-2.0.20090212/setup.rb +1551 -0
  34. data/vendor/camping-2.0.20090212/test/apps/env_debug.rb +65 -0
  35. data/vendor/camping-2.0.20090212/test/apps/forms.rb +95 -0
  36. data/vendor/camping-2.0.20090212/test/apps/misc.rb +86 -0
  37. data/vendor/camping-2.0.20090212/test/apps/sessions.rb +38 -0
  38. data/vendor/camping-2.0.20090212/test/test_camping.rb +54 -0
  39. metadata +43 -16
  40. data/lib/picnic/postambles.rb +0 -292
  41. data/lib/picnic/utils.rb +0 -36
  42. data/vendor/camping-1.5.180/lib/camping.rb +0 -57
  43. data/vendor/camping-1.5.180/lib/camping/fastcgi.rb +0 -244
  44. data/vendor/camping-1.5.180/lib/camping/reloader.rb +0 -163
  45. data/vendor/camping-1.5.180/lib/camping/webrick.rb +0 -65
@@ -0,0 +1,54 @@
1
+ %w[uri stringio rack].map{|l|require l};class Object;def meta_def m,&b
2
+ (class<<self;self end).send:define_method,m,&b end end;module Camping;C=self
3
+ S=IO.read(__FILE__)rescue nil;P="<h1>Cam\ping Problem!</h1><h2>%s</h2>"
4
+ U=Rack::Utils;Apps=[];class H<Hash
5
+ def method_missing m,*a;m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m.to_s]:super end
6
+ undef id,type;end;module Helpers;def R c,*g
7
+ p,h=/\(.+?\)/,g.grep(Hash);g-=h;raise"bad route"unless u=c.urls.find{|x|
8
+ break x if x.scan(p).size==g.size&&/^#{x}\/?$/=~(x=g.inject(x){|x,a|
9
+ x.sub p,U.escape((a[a.class.primary_key]rescue a))})}
10
+ h.any?? u+"?"+U.build_query(h[0]):u end;def / p
11
+ p[0]==?/?@root+p:p end;def URL c='/',*a;c=R(c, *a) if c.respond_to?:urls
12
+ c=self/c;c=@request.url[/.{8,}?(?=\/)/]+c if c[0]==?/;URI c end
13
+ end;module Base;attr_accessor:input,:cookies,:headers,:body,:status,:root
14
+ def render v,*a,&b;mab(/^_/!~v.to_s){send(v,*a,&b)} end
15
+ def mab l=nil,&b;m=Mab.new({},self);s=m.capture(&b)
16
+ s=m.capture{layout{s}} if l && m.respond_to?(:layout);s end
17
+ def r s,b,h={};b,h=h,b if Hash===b;@status=s;
18
+ @headers.merge!(h);@body=b;end;def redirect *a;r 302,'','Location'=>URL(*a).
19
+ to_s;end;def r404 p=env.PATH;r 404,P%"#{p} not found"end;def r500 k,m,x
20
+ r 500,P%"#{k}.#{m}"+"<h3>#{x.class} #{x.message}: <ul>#{x.
21
+ backtrace.map{|b|"<li>#{b}</li>"}}</ul></h3>"end;def r501 m=@method
22
+ r 501,P%"#{m.upcase} not implemented"end;def to_a
23
+ @response.body=@body.respond_to?(:each)?@body:""
24
+ @response.status=@status;@response.headers.merge!(@headers)
25
+ @cookies.each{|k,v|v={:value=>v,:path=>self/"/"} if String===v
26
+ @response.set_cookie(k,v) if @request.cookies[k]!=v}
27
+ @response.to_a;end;def initialize(env)
28
+ @request,@response,@env=Rack::Request.new(env),Rack::Response.new,env
29
+ @root,@input,@cookies,@headers,@status=
30
+ @env.SCRIPT_NAME.sub(/\/$/,''),H[@request.params],
31
+ H[@request.cookies],@response.headers,@response.status
32
+ @input.each{|k,v|if k[-2..-1]=="[]";@input[k[0..-3]]=
33
+ @input.delete(k)elsif k=~/(.*)\[([^\]]+)\]$/
34
+ (@input[$1]||=H[])[$2]=@input.delete(k)end};end;def service *a
35
+ r=catch(:halt){send(@env.REQUEST_METHOD.downcase,*a)};@body||=r
36
+ self;end;end;module Controllers;@r=[];class<<self;def r;@r end;def R *u;r=@r
37
+ Class.new{meta_def(:urls){u};meta_def(:inherited){|x|r<<x}}end
38
+ def D p,m;p='/'if !p||!p[0]
39
+ r.map{|k|k.urls.map{|x|return(k.instance_method(m)rescue nil)?
40
+ [k,m,*$~[1..-1]]:[I,'r501',m]if p=~/^#{x}\/?$/}};[I,'r404',p] end
41
+ N=H.new{|_,x|x.downcase}.merge! "N"=>'(\d+)',"X"=>'(\w+)',"Index"=>''
42
+ def M;def M;end;constants.map{|c|k=const_get(c)
43
+ k.send:include,C,Base,Helpers,Models;@r=[k]+r if r-[k]==r
44
+ k.meta_def(:urls){["/#{c.scan(/.[^A-Z]*/).map(&N.method(:[]))*'/'}"]
45
+ }if !k.respond_to?:urls}end end;class I<R()
46
+ end; end;X=Controllers;class<<self;def goes m
47
+ Apps<<eval(S.gsub(/Camping/,m.to_s),TOPLEVEL_BINDING) end;def call(
48
+ e)X.M;e=H[e.to_hash];k,m,*a=X.D e.PATH_INFO,(e.REQUEST_METHOD||'get').downcase
49
+ e.REQUEST_METHOD=m;k.new(e).service(*a).to_a;end
50
+ def method_missing m,c,*a;X.M;h=Hash===a[-1]?H[a.pop]:{};e=
51
+ H[h[:env]||{}].merge!({'rack.input'=>StringIO.new,'REQUEST_METHOD'=>m.to_s})
52
+ k=X.const_get(c).new(H[e]);k.send("input=",h[:input])if h[:input]
53
+ k.service(*a);end;end;module Views;include X,Helpers end;module Models
54
+ autoload:Base,'camping/ar';def Y;self;end end;autoload:Mab,'camping/mab';C end
@@ -71,8 +71,8 @@ module Camping
71
71
  module_eval $AR_EXTRAS
72
72
  end
73
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
74
+ Camping::S.sub! /autoload\s*:Base\s*,\s*['"]camping\/ar['"]/, ""
75
+ Camping::S.sub! /def\s*Y[;\s]*self[;\s]*end/, $AR_EXTRAS
76
+ Camping::Apps.each do |c|
77
+ c::Models.module_eval $AR_EXTRAS
78
78
  end
@@ -1,22 +1,25 @@
1
- # == About camping/session.rb
1
+ # == About camping/ar/session.rb
2
2
  #
3
3
  # This file contains two modules which supply basic sessioning to your Camping app.
4
4
  # Again, we're dealing with a pretty little bit of code: approx. 60 lines.
5
5
  #
6
6
  # * Camping::Models::Session is a module which adds a single <tt>sessions</tt> table
7
7
  # to your database.
8
- # * Camping::Session is a module which you will mix into your application (or into
8
+ # * Camping::ARSession is a module which you will mix into your application (or into
9
9
  # specific controllers which require sessions) to supply a <tt>@state</tt> variable
10
10
  # you can use in controllers and views.
11
11
  #
12
- # For a basic tutorial, see the *Getting Started* section of the Camping::Session module.
12
+ # For a basic tutorial, see the *Getting Started* section of the Camping::ARSession module.
13
13
  require 'camping'
14
+ require 'camping/ar'
14
15
 
15
16
  module Camping::Models
16
17
  # A database table for storing Camping sessions. Contains a unique 32-character hashid, a
17
18
  # creation timestamp, and a column of serialized data called <tt>ivars</tt>.
18
19
  class Session < Base
19
20
  serialize :ivars
21
+ set_primary_key :hashid
22
+
20
23
  def []=(k, v) # :nodoc:
21
24
  self.ivars[k] = v
22
25
  end
@@ -24,13 +27,17 @@ class Session < Base
24
27
  self.ivars[k] rescue nil
25
28
  end
26
29
 
30
+ protected
27
31
  RAND_CHARS = [*'A'..'Z'] + [*'0'..'9'] + [*'a'..'z']
32
+ def before_create
33
+ rand_max = RAND_CHARS.size
34
+ sid = (0...32).inject("") { |ret,_| ret << RAND_CHARS[rand(rand_max)] }
35
+ write_attribute('hashid', sid)
36
+ end
28
37
 
29
38
  # Generates a new session ID and creates a row for the new session in the database.
30
39
  def self.generate cookies
31
- rand_max = RAND_CHARS.size
32
- sid = (0...32).inject("") { |ret,_| ret << RAND_CHARS[rand(rand_max)] }
33
- sess = Session.create :hashid => sid, :ivars => Camping::H[]
40
+ sess = Session.create :ivars => Camping::H[]
34
41
  cookies.camping_sid = sess.hashid
35
42
  sess
36
43
  end
@@ -38,6 +45,7 @@ class Session < Base
38
45
  # Gets the existing session based on the <tt>camping_sid</tt> available in cookies.
39
46
  # If none is found, generates a new session.
40
47
  def self.persist cookies
48
+ session = nil
41
49
  if cookies.camping_sid
42
50
  session = Camping::Models::Session.find_by_hashid cookies.camping_sid
43
51
  end
@@ -62,21 +70,22 @@ class Session < Base
62
70
  def self.create_schema
63
71
  unless table_exists?
64
72
  ActiveRecord::Schema.define do
65
- create_table :sessions, :force => true do |t|
66
- #t.column :id, :integer, :null => false
67
- t.column :hashid, :string, :limit => 32
73
+ create_table :sessions, :force => true, :id => false do |t|
74
+ t.column :hashid, :string, :limit => 32, :null => false
68
75
  t.column :created_at, :datetime
69
76
  t.column :ivars, :text
70
77
  end
78
+ add_index :sessions, [:hashid], :unique => true
71
79
  end
72
80
  reset_column_information
73
81
  end
74
82
  end
75
83
  end
84
+ Session.partial_updates = false if Session.respond_to?(:partial_updates=)
76
85
  end
77
86
 
78
87
  module Camping
79
- # The Camping::Session module is designed to be mixed into your application or into specific
88
+ # The Camping::ARSession module is designed to be mixed into your application or into specific
80
89
  # controllers which require sessions. This module defines a <tt>service</tt> method which
81
90
  # intercepts all requests handed to those controllers.
82
91
  #
@@ -85,7 +94,7 @@ module Camping
85
94
  # To get sessions working for your application:
86
95
  #
87
96
  # 1. <tt>require 'camping/session'</tt>
88
- # 2. Mixin the module: <tt>module YourApp; include Camping::Session end</tt>
97
+ # 2. Mixin the module: <tt>module YourApp; include Camping::ARSession end</tt>
89
98
  # 3. In your application's <tt>create</tt> method, add a call to <tt>Camping::Models::Session.create_schema</tt>
90
99
  # 4. Throughout your application, use the <tt>@state</tt> var like a hash to store your application's data.
91
100
  #
@@ -99,7 +108,7 @@ module Camping
99
108
  # * All mounted Camping apps using this class will use the same database table.
100
109
  # * However, your application's data is stored in its own hash.
101
110
  # * Session data is only saved if it has changed.
102
- module Session
111
+ module ARSession
103
112
  # This <tt>service</tt> method, when mixed into controllers, intercepts requests
104
113
  # and wraps them with code to start and close the session. If a session isn't found
105
114
  # in the database it is created. The <tt>@state</tt> variable is set and if it changes,
@@ -109,7 +118,8 @@ module Session
109
118
  app = self.class.name.gsub(/^(\w+)::.+$/, '\1')
110
119
  @state = (session[app] ||= Camping::H[])
111
120
  hash_before = Marshal.dump(@state).hash
112
- s = super(*a)
121
+ return super(*a)
122
+ ensure
113
123
  if session
114
124
  hash_after = Marshal.dump(@state).hash
115
125
  unless hash_before == hash_after
@@ -117,7 +127,6 @@ module Session
117
127
  session.save
118
128
  end
119
129
  end
120
- s
121
130
  end
122
131
  end
123
132
  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
@@ -0,0 +1,163 @@
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
+ if @klass
52
+ Camping::Apps.delete(@klass)
53
+ Object.send :remove_const, @klass.name
54
+ @klass = nil
55
+ end
56
+ end
57
+
58
+ # Loads (or reloads) the application. The reloader will take care of calling
59
+ # this for you. You can certainly call it yourself if you feel it's warranted.
60
+ def load_app
61
+ title = File.basename(@script)[/^([\w_]+)/,1].gsub /_/,''
62
+ begin
63
+ all_requires = $LOADED_FEATURES.dup
64
+ load @script
65
+ @requires = ($LOADED_FEATURES - all_requires).select do |req|
66
+ req.index(File.basename(@script) + "/") == 0 || req.index(title + "/") == 0
67
+ end
68
+ rescue Exception => e
69
+ puts "!! trouble loading #{title.inspect}: [#{e.class}] #{e.message}"
70
+ puts e.backtrace.join("\n")
71
+ find_app title
72
+ remove_app
73
+ return
74
+ end
75
+
76
+ @mtime = mtime
77
+ find_app title
78
+ unless @klass and @klass.const_defined? :C
79
+ puts "!! trouble loading #{title.inspect}: not a Camping app, no #{title.capitalize} module found"
80
+ remove_app
81
+ return
82
+ end
83
+
84
+ Reloader.conditional_connect
85
+ @klass.create if @klass.respond_to? :create
86
+ puts "** #{title.inspect} app loaded"
87
+ @klass
88
+ end
89
+
90
+ # The timestamp of the most recently modified app dependency.
91
+ def mtime
92
+ ((@requires || []) + [@script]).map do |fname|
93
+ fname = fname.gsub(/^#{Regexp::quote File.dirname(@script)}\//, '')
94
+ begin
95
+ File.mtime(File.join(File.dirname(@script), fname))
96
+ rescue Errno::ENOENT
97
+ remove_app
98
+ @mtime
99
+ end
100
+ end.reject{|t| t > Time.now }.max
101
+ end
102
+
103
+ # Conditional reloading of the app. This gets called on each request and
104
+ # only reloads if the modification times on any of the files is updated.
105
+ def reload_app
106
+ return if @klass and @mtime and mtime <= @mtime
107
+
108
+ if @requires
109
+ @requires.each { |req| $LOADED_FEATURES.delete(req) }
110
+ end
111
+ remove_app
112
+ load_app
113
+ end
114
+
115
+ # Conditionally reloads (using reload_app.) Then passes the request through
116
+ # to the wrapped Camping app.
117
+ def call(*a)
118
+ reload_app
119
+ if @klass
120
+ @klass.call(*a)
121
+ else
122
+ Camping.call(*a)
123
+ end
124
+ end
125
+
126
+ # Returns source code for the main script in the application.
127
+ def view_source
128
+ File.read(@script)
129
+ end
130
+
131
+ class << self
132
+ attr_writer :database, :log
133
+
134
+ def conditional_connect
135
+ # If database models are present, `autoload?` will return nil.
136
+ unless Camping::Models.autoload? :Base
137
+ if @database and @database[:adapter] == 'sqlite3'
138
+ begin
139
+ require 'sqlite3_api'
140
+ rescue LoadError
141
+ puts "!! Your SQLite3 adapter isn't a compiled extension."
142
+ abort "!! Please check out http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 for tips."
143
+ end
144
+ end
145
+
146
+ case @log
147
+ when Logger
148
+ Camping::Models::Base.logger = @log
149
+ when String
150
+ require 'logger'
151
+ Camping::Models::Base.logger = Logger.new(@log == "-" ? STDOUT : @log)
152
+ end
153
+
154
+ Camping::Models::Base.establish_connection @database if @database
155
+
156
+ if Camping::Models.const_defined?(:Session)
157
+ Camping::Models::Session.create_schema
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,158 @@
1
+ require 'irb'
2
+ require 'rack'
3
+ require 'camping/reloader'
4
+
5
+ module Camping::Server
6
+ class Base < Hash
7
+ include Enumerable
8
+
9
+ attr_reader :paths
10
+ attr_accessor :conf
11
+
12
+ def initialize(conf, paths = [])
13
+ unless conf.database
14
+ raise "!! No home directory found. Please specify a database file, see --help."
15
+ end
16
+
17
+ @conf = conf
18
+ Camping::Reloader.database = conf.database
19
+ Camping::Reloader.log = conf.log
20
+
21
+ @paths = []
22
+ paths.each { |script| add_app script }
23
+ # TODO exception instead of abort()
24
+ # abort("** No apps successfully loaded") unless self.detect { |app| app.klass }
25
+
26
+ end
27
+
28
+ def add_app(path)
29
+ @paths << path
30
+ if File.directory? path
31
+ Dir[File.join(path, '*.rb')].each { |s| insert_app(s)}
32
+ else
33
+ insert_app(path)
34
+ end
35
+ # TODO check to see if the application is created or not... exception perhaps?
36
+ end
37
+
38
+ def find_new_scripts
39
+ self.values.each { |app| app.reload_app }
40
+ @paths.each do |path|
41
+ Dir[File.join(path, '*.rb')].each do |script|
42
+ smount = File.basename(script, '.rb')
43
+ next if detect { |x| x.mount == smount }
44
+
45
+ puts "** Discovered new #{script}"
46
+ # TODO hmm. the next should be handled by the add_app thingy
47
+ app = insert_app(script)
48
+ next unless app
49
+
50
+ yield app
51
+
52
+ end
53
+ end
54
+ self.values.sort! { |x, y| x.mount <=> y.mount }
55
+ end
56
+ def index_page
57
+ welcome = "You are Camping"
58
+ apps = self
59
+ <<-HTML
60
+ <html>
61
+ <head>
62
+ <title>#{welcome}</title>
63
+ <style type="text/css">
64
+ body {
65
+ font-family: verdana, arial, sans-serif;
66
+ padding: 10px 40px;
67
+ margin: 0;
68
+ }
69
+ h1, h2, h3, h4, h5, h6 {
70
+ font-family: utopia, georgia, serif;
71
+ }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <h1>#{welcome}</h1>
76
+ <p>Good day. These are the Camping apps you've mounted.</p>
77
+ <ul>
78
+ #{apps.values.select{|app|app.klass}.map do |app|
79
+ "<li><h3 style=\"display: inline\"><a href=\"/#{app.mount}\">#{app.klass.name}</a></h3><small> / <a href=\"/code/#{app.mount}\">View source</a></small></li>"
80
+ end.join("\n")}
81
+ </ul>
82
+ </body>
83
+ </html>
84
+ HTML
85
+ end
86
+
87
+ def each(&b)
88
+ self.values.each(&b)
89
+ end
90
+
91
+ # for RSpec tests
92
+ def apps
93
+ self.values
94
+ end
95
+
96
+ def start
97
+ handler, conf = case @conf.server
98
+ when "console"
99
+ ARGV.clear
100
+ IRB.start
101
+ exit
102
+ when "mongrel"
103
+ puts "** Starting Mongrel on #{@conf.host}:#{@conf.port}"
104
+ [Rack::Handler::Mongrel, {:Port => @conf.port, :Host => @conf.host}]
105
+ when "webrick"
106
+ [Rack::Handler::WEBrick, {:Port => @conf.port, :BindAddress => @conf.host}]
107
+ end
108
+
109
+ rapp = if apps.length > 1
110
+ hash = {
111
+ "/" => proc {|env|[200,{'Content-Type'=>'text/html'},index_page]}
112
+ }
113
+ apps.each do |app|
114
+ hash["/#{app.mount}"] = app
115
+ hash["/code/#{app.mount}"] = proc do |env|
116
+ [200,{'Content-Type'=>'text/plain'},app.view_source]
117
+ end
118
+ end
119
+ Rack::URLMap.new(hash)
120
+ else
121
+ apps.first
122
+ end
123
+ rapp = Rack::Lint.new(rapp)
124
+ rapp = XSendfile.new(rapp)
125
+ rapp = Rack::ShowExceptions.new(rapp)
126
+ handler.run(rapp, conf)
127
+ end
128
+
129
+ private
130
+
131
+ def insert_app(script)
132
+ self[script] = Camping::Reloader.new(script)
133
+ end
134
+ end
135
+
136
+ # A Rack middleware for reading X-Sendfile. Should only be used in
137
+ # development.
138
+ class XSendfile
139
+
140
+ HEADERS = [
141
+ "X-Sendfile",
142
+ "X-Accel-Redirect",
143
+ "X-LIGHTTPD-send-file"
144
+ ]
145
+
146
+ def initialize(app)
147
+ @app = app
148
+ end
149
+
150
+ def call(env)
151
+ status, headers, body = @app.call(env)
152
+ if path = headers.values_at(*HEADERS).compact.first
153
+ body = File.read(path)
154
+ end
155
+ [status, headers, body]
156
+ end
157
+ end
158
+ end