camping 2.0 → 2.1
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.
- data/CHANGELOG +11 -0
- data/Rakefile +21 -3
- data/bin/camping +2 -85
- data/book/51_upgrading +17 -0
- data/lib/camping-unabridged.rb +56 -16
- data/lib/camping.rb +37 -32
- data/lib/camping/mab.rb +2 -0
- data/lib/camping/reloader.rb +39 -45
- data/lib/camping/server.rb +217 -130
- data/lib/camping/session.rb +5 -6
- data/lib/camping/template.rb +17 -0
- data/test/app_markup.rb +51 -0
- data/test/app_route_generating.rb +22 -0
- data/test/app_sessions.rb +46 -0
- data/test/app_simple.rb +97 -0
- data/test/test_helper.rb +51 -0
- metadata +9 -16
- data/doc/api.html +0 -1953
- data/doc/book.html +0 -73
- data/doc/book/01_introduction.html +0 -57
- data/doc/book/02_getting_started.html +0 -573
- data/doc/book/51_upgrading.html +0 -146
- data/doc/created.rid +0 -1
- data/doc/images/Camping.gif +0 -0
- data/doc/images/loadingAnimation.gif +0 -0
- data/doc/images/permalink.gif +0 -0
- data/doc/index.html +0 -148
- data/doc/js/camping.js +0 -79
- data/doc/js/jquery.js +0 -32
- data/doc/rdoc.css +0 -117
data/lib/camping/mab.rb
CHANGED
data/lib/camping/reloader.rb
CHANGED
@@ -32,28 +32,7 @@ module Camping
|
|
32
32
|
#
|
33
33
|
# You can also give Reloader more than one script.
|
34
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
|
56
|
-
end
|
35
|
+
attr_reader :scripts, :apps
|
57
36
|
|
58
37
|
# This class is doing all the hard work; however, it only works on
|
59
38
|
# single files. Reloader just wraps up support for multiple scripts
|
@@ -61,13 +40,14 @@ module Camping
|
|
61
40
|
class Script # :nodoc:
|
62
41
|
attr_reader :apps, :file, :dir, :extras
|
63
42
|
|
64
|
-
def initialize(file)
|
43
|
+
def initialize(file, callback = nil)
|
65
44
|
@file = File.expand_path(file)
|
66
45
|
@dir = File.dirname(@file)
|
67
46
|
@extras = File.join(@dir, File.basename(@file, ".rb"))
|
68
47
|
@mtime = Time.at(0)
|
69
48
|
@requires = []
|
70
49
|
@apps = {}
|
50
|
+
@callback = callback
|
71
51
|
end
|
72
52
|
|
73
53
|
# Loads the apps availble in this script. Use <tt>apps</tt> to get
|
@@ -94,20 +74,25 @@ module Camping
|
|
94
74
|
|
95
75
|
new_apps = (Camping::Apps - all_apps)
|
96
76
|
old_apps = @apps.dup
|
77
|
+
|
97
78
|
@apps = new_apps.inject({}) do |hash, app|
|
98
79
|
key = app.name.to_sym
|
99
|
-
hash[key] =
|
100
|
-
|
101
|
-
|
80
|
+
hash[key] = app
|
81
|
+
|
82
|
+
if !old_apps.include?(key)
|
83
|
+
@callback.call(app) if @callback
|
84
|
+
app.create if app.respond_to?(:create)
|
85
|
+
end
|
102
86
|
hash
|
103
87
|
end
|
88
|
+
|
104
89
|
self
|
105
90
|
end
|
106
91
|
|
107
92
|
# Removes all the apps defined in this script.
|
108
93
|
def remove_apps
|
109
94
|
@apps.each do |name, app|
|
110
|
-
Camping::Apps.delete(app
|
95
|
+
Camping::Apps.delete(app)
|
111
96
|
Object.send :remove_const, name
|
112
97
|
end
|
113
98
|
end
|
@@ -149,42 +134,51 @@ module Camping
|
|
149
134
|
# will be loaded relative to the current working directory.
|
150
135
|
def initialize(*scripts)
|
151
136
|
@scripts = []
|
137
|
+
@apps = {}
|
152
138
|
update(*scripts)
|
153
139
|
end
|
154
140
|
|
141
|
+
def on_reload(&blk)
|
142
|
+
@callback = blk
|
143
|
+
end
|
144
|
+
|
155
145
|
# Updates the reloader to only use the scripts provided:
|
156
146
|
#
|
157
147
|
# reloader.update("examples/blog.rb", "examples/wiki.rb")
|
158
148
|
def update(*scripts)
|
159
|
-
|
149
|
+
old_scripts = @scripts.dup
|
160
150
|
clear
|
151
|
+
|
161
152
|
@scripts = scripts.map do |script|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
old[pos]
|
166
|
-
else
|
167
|
-
s.load_apps
|
168
|
-
end
|
153
|
+
file = File.expand_path(script)
|
154
|
+
old_scripts.detect { |s| s.file == file } or
|
155
|
+
Script.new(script, @callback)
|
169
156
|
end
|
157
|
+
|
158
|
+
reload!
|
170
159
|
end
|
171
160
|
|
172
161
|
# Removes all the scripts from the reloader.
|
173
162
|
def clear
|
174
|
-
@
|
163
|
+
@scripts = []
|
164
|
+
@apps = {}
|
175
165
|
end
|
176
166
|
|
177
|
-
#
|
178
|
-
def
|
179
|
-
@scripts.each
|
167
|
+
# Returns the script which provided the given app.
|
168
|
+
def script(app)
|
169
|
+
@scripts.each do |script|
|
170
|
+
return script if script.apps.values.include?(app)
|
171
|
+
end
|
172
|
+
|
173
|
+
false
|
180
174
|
end
|
181
175
|
|
182
|
-
#
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
176
|
+
# Simply calls reload! on all the Script objects.
|
177
|
+
def reload!
|
178
|
+
@apps = {}
|
179
|
+
@scripts.each do |script|
|
180
|
+
script.reload!
|
181
|
+
@apps.update(script.apps)
|
188
182
|
end
|
189
183
|
end
|
190
184
|
end
|
data/lib/camping/server.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'irb'
|
2
|
+
require 'erb'
|
2
3
|
require 'rack'
|
3
4
|
require 'camping/reloader'
|
4
5
|
|
@@ -22,41 +23,182 @@ require 'camping/reloader'
|
|
22
23
|
# camping blog.rb # Mounts Blog at /
|
23
24
|
#
|
24
25
|
# And visit http://localhost:3301/ in your browser.
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
module Camping
|
27
|
+
class Server < Rack::Server
|
28
|
+
class Options
|
29
|
+
if home = ENV['HOME'] # POSIX
|
30
|
+
DB = File.join(home, '.camping.db')
|
31
|
+
RC = File.join(home, '.campingrc')
|
32
|
+
elsif home = ENV['APPDATA'] # MSWIN
|
33
|
+
DB = File.join(home, 'Camping.db')
|
34
|
+
RC = File.join(home, 'Campingrc')
|
35
|
+
else
|
36
|
+
DB = nil
|
37
|
+
RC = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
HOME = File.expand_path(home) + '/'
|
41
|
+
|
42
|
+
def parse!(args)
|
43
|
+
args = args.dup
|
44
|
+
options = {}
|
45
|
+
|
46
|
+
opt_parser = OptionParser.new("", 24, ' ') do |opts|
|
47
|
+
opts.banner = "Usage: camping app1.rb app2.rb..."
|
48
|
+
opts.define_head "#{File.basename($0)}, the microframework ON-button for ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
|
49
|
+
opts.separator ""
|
50
|
+
opts.separator "Specific options:"
|
51
|
+
|
52
|
+
opts.on("-h", "--host HOSTNAME",
|
53
|
+
"Host for web server to bind to (default is all IPs)") { |v| options[:Host] = v }
|
54
|
+
|
55
|
+
opts.on("-p", "--port NUM",
|
56
|
+
"Port for web server (defaults to 3301)") { |v| options[:Port] = v }
|
57
|
+
|
58
|
+
db = DB.sub(HOME, '~/') if DB
|
59
|
+
opts.on("-d", "--database FILE",
|
60
|
+
"SQLite3 database path (defaults to #{db ? db : '<none>'})") { |db_path| options[:database] = db_path }
|
61
|
+
|
62
|
+
opts.on("-C", "--console",
|
63
|
+
"Run in console mode with IRB") { options[:server] = "console" }
|
64
|
+
|
65
|
+
server_list = ["mongrel", "webrick", "console"]
|
66
|
+
opts.on("-s", "--server NAME",
|
67
|
+
"Server to force (#{server_list.join(', ')})") { |v| options[:server] = v }
|
28
68
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
69
|
+
opts.separator ""
|
70
|
+
opts.separator "Common options:"
|
71
|
+
|
72
|
+
# No argument, shows at tail. This will print an options summary.
|
73
|
+
# Try it and see!
|
74
|
+
opts.on_tail("-?", "--help", "Show this message") do
|
75
|
+
puts opts
|
76
|
+
exit
|
77
|
+
end
|
78
|
+
|
79
|
+
# Another typical switch to print the version.
|
80
|
+
opts.on_tail("-v", "--version", "Show version") do
|
81
|
+
puts Gem.loaded_specs['camping'].version
|
82
|
+
exit
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
opt_parser.parse!(args)
|
87
|
+
|
88
|
+
if args.empty?
|
89
|
+
puts opt_parser
|
90
|
+
exit
|
91
|
+
end
|
92
|
+
|
93
|
+
options[:scripts] = args
|
94
|
+
options
|
95
|
+
end
|
39
96
|
end
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
97
|
+
|
98
|
+
def initialize(*)
|
99
|
+
super
|
100
|
+
@reloader = Camping::Reloader.new
|
101
|
+
@reloader.on_reload do |app|
|
102
|
+
if !app.options.has_key?(:dynamic_templates)
|
103
|
+
app.options[:dynamic_templates] = true
|
104
|
+
end
|
105
|
+
|
106
|
+
if !Camping::Models.autoload?(:Base) && options[:database]
|
107
|
+
Camping::Models::Base.establish_connection(
|
108
|
+
:adapter => 'sqlite3',
|
109
|
+
:database => options[:database]
|
110
|
+
)
|
111
|
+
end
|
49
112
|
end
|
50
|
-
end
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
113
|
+
end
|
114
|
+
|
115
|
+
def opt_parser
|
116
|
+
Options.new
|
117
|
+
end
|
118
|
+
|
119
|
+
def default_options
|
120
|
+
super.merge({
|
121
|
+
:Port => 3301,
|
122
|
+
:database => Options::DB
|
123
|
+
})
|
124
|
+
end
|
125
|
+
|
126
|
+
def middleware
|
127
|
+
h = super
|
128
|
+
h["development"].unshift [XSendfile]
|
129
|
+
h
|
130
|
+
end
|
131
|
+
|
132
|
+
def start
|
133
|
+
if options[:server] == "console"
|
134
|
+
puts "** Starting console"
|
135
|
+
reload!
|
136
|
+
this = self
|
137
|
+
eval("self", TOPLEVEL_BINDING).meta_def(:reload!) { this.reload!; nil }
|
138
|
+
ARGV.clear
|
139
|
+
IRB.start
|
140
|
+
exit
|
141
|
+
else
|
142
|
+
name = server.name[/\w+$/]
|
143
|
+
puts "** Starting #{name} on #{options[:Host]}:#{options[:Port]}"
|
144
|
+
super
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def find_scripts
|
149
|
+
scripts = options[:scripts].map do |path|
|
150
|
+
if File.file?(path)
|
151
|
+
path
|
152
|
+
elsif File.directory?(path)
|
153
|
+
Dir[File.join(path, '*.rb')]
|
154
|
+
end
|
155
|
+
end.flatten.compact
|
156
|
+
|
157
|
+
@reloader.update(*scripts)
|
158
|
+
end
|
159
|
+
|
160
|
+
def reload!
|
161
|
+
find_scripts
|
162
|
+
end
|
163
|
+
|
164
|
+
def app
|
165
|
+
self
|
166
|
+
end
|
167
|
+
|
168
|
+
def call(env)
|
169
|
+
reload!
|
170
|
+
apps = @reloader.apps
|
171
|
+
|
172
|
+
case apps.length
|
173
|
+
when 0
|
174
|
+
index_page(apps)
|
175
|
+
when 1
|
176
|
+
apps.values.first.call(env)
|
177
|
+
else
|
178
|
+
apps.each do |name, app|
|
179
|
+
mount = name.to_s.downcase
|
180
|
+
case env["PATH_INFO"]
|
181
|
+
when %r{^/#{mount}}
|
182
|
+
env["SCRIPT_NAME"] = env["SCRIPT_NAME"] + $&
|
183
|
+
env["PATH_INFO"] = $'
|
184
|
+
return app.call(env)
|
185
|
+
when %r{^/code/#{mount}}
|
186
|
+
return [200, {'Content-Type' => 'text/plain', 'X-Sendfile' => @reloader.script(app).file}, []]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
index_page(apps)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def index_page(apps)
|
195
|
+
[200, {'Content-Type' => 'text/html'}, [TEMPLATE.result(binding)]]
|
196
|
+
end
|
197
|
+
|
198
|
+
SOURCE = <<-HTML
|
57
199
|
<html>
|
58
200
|
<head>
|
59
|
-
<title
|
201
|
+
<title>You are Camping</title>
|
60
202
|
<style type="text/css">
|
61
203
|
body {
|
62
204
|
font-family: verdana, arial, sans-serif;
|
@@ -66,113 +208,58 @@ class Camping::Server
|
|
66
208
|
h1, h2, h3, h4, h5, h6 {
|
67
209
|
font-family: utopia, georgia, serif;
|
68
210
|
}
|
211
|
+
h3 { display: inline; }
|
69
212
|
</style>
|
70
213
|
</head>
|
71
214
|
<body>
|
72
|
-
|
215
|
+
<% if apps.empty? %>
|
216
|
+
<p>Good day. I'm sorry, but I could not find any Camping apps.
|
217
|
+
You might want to take a look at the console to see if any errors
|
218
|
+
have been raised.</p>
|
219
|
+
<% else %>
|
220
|
+
<p>Good day. These are the Camping apps you've mounted.</p>
|
221
|
+
<ul>
|
222
|
+
<% apps.each do |name, app| %>
|
223
|
+
<li>
|
224
|
+
<h3><a href="/<%= name.to_s.downcase %>"><%= app %></a></h3>
|
225
|
+
<small> / <a href="/code/<%= name.to_s.downcase %>">View source</a></small>
|
226
|
+
</li>
|
227
|
+
<% end %>
|
228
|
+
</ul>
|
229
|
+
<% end %>
|
230
|
+
</body>
|
231
|
+
</html>
|
73
232
|
HTML
|
74
|
-
footer = '</body></html>'
|
75
|
-
main = if apps.empty?
|
76
|
-
"<p>Good day. I'm sorry, but I could not find any Camping apps. "\
|
77
|
-
"You might want to take a look at the console to see if any errors "\
|
78
|
-
"have been raised.</p>"
|
79
|
-
else
|
80
|
-
"<p>Good day. These are the Camping apps you've mounted.</p><ul>" +
|
81
|
-
apps.map do |mount, app|
|
82
|
-
"<li><h3 style=\"display: inline\"><a href=\"/#{mount}\">#{app}</a></h3><small> / <a href=\"/code/#{mount}\">View source</a></small></li>"
|
83
|
-
end.join("\n") + '</ul>'
|
84
|
-
end
|
85
233
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
all_apps.each do |mount, wrapp|
|
102
|
-
# We're doing @reloader.reload! ourself, so we don't need the wrapper.
|
103
|
-
app = wrapp.app
|
104
|
-
hash["/#{mount}"] = app
|
105
|
-
hash["/code/#{mount}"] = proc do |env|
|
106
|
-
[200,{'Content-Type'=>'text/plain','X-Sendfile'=>wrapp.script.file},'']
|
234
|
+
TEMPLATE = ERB.new(SOURCE)
|
235
|
+
|
236
|
+
class XSendfile
|
237
|
+
def initialize(app)
|
238
|
+
@app = app
|
239
|
+
end
|
240
|
+
|
241
|
+
def call(env)
|
242
|
+
status, headers, body = @app.call(env)
|
243
|
+
|
244
|
+
if key = headers.keys.grep(/X-Sendfile/i).first
|
245
|
+
filename = headers[key]
|
246
|
+
content = open(filename,'rb') { | io | io.read}
|
247
|
+
headers['Content-Length'] = size(content).to_s
|
248
|
+
body = [content]
|
107
249
|
end
|
250
|
+
|
251
|
+
return status, headers, body
|
108
252
|
end
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
@reloader.apps.inject({}) do |h, (mount, wrapp)|
|
119
|
-
h[mount.to_s.downcase] = wrapp
|
120
|
-
h
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def call(env)
|
125
|
-
app.call(env)
|
126
|
-
end
|
127
|
-
|
128
|
-
def start
|
129
|
-
handler, conf = case @conf.server
|
130
|
-
when "console"
|
131
|
-
puts "** Starting console"
|
132
|
-
reload!
|
133
|
-
this = self; eval("self", TOPLEVEL_BINDING).meta_def(:reload!) { this.reload!; nil }
|
134
|
-
ARGV.clear
|
135
|
-
IRB.start
|
136
|
-
exit
|
137
|
-
when "mongrel"
|
138
|
-
puts "** Starting Mongrel on #{@conf.host}:#{@conf.port}"
|
139
|
-
[Rack::Handler::Mongrel, {:Port => @conf.port, :Host => @conf.host}]
|
140
|
-
when "webrick"
|
141
|
-
puts "** Starting WEBrick on #{@conf.host}:#{@conf.port}"
|
142
|
-
[Rack::Handler::WEBrick, {:Port => @conf.port, :BindAddress => @conf.host}]
|
143
|
-
end
|
144
|
-
reload!
|
145
|
-
handler.run(self, conf)
|
146
|
-
end
|
147
|
-
|
148
|
-
def reload!
|
149
|
-
find_scripts
|
150
|
-
@reloader.reload!
|
151
|
-
end
|
152
|
-
|
153
|
-
# A Rack middleware for reading X-Sendfile. Should only be used in
|
154
|
-
# development.
|
155
|
-
class XSendfile
|
156
|
-
|
157
|
-
HEADERS = [
|
158
|
-
"X-Sendfile",
|
159
|
-
"X-Accel-Redirect",
|
160
|
-
"X-LIGHTTPD-send-file"
|
161
|
-
]
|
162
|
-
|
163
|
-
def initialize(app)
|
164
|
-
@app = app
|
165
|
-
end
|
166
|
-
|
167
|
-
def call(env)
|
168
|
-
status, headers, body = @app.call(env)
|
169
|
-
headers = Rack::Utils::HeaderHash.new(headers)
|
170
|
-
if header = HEADERS.detect { |header| headers.include?(header) }
|
171
|
-
path = headers[header]
|
172
|
-
body = File.read(path)
|
173
|
-
headers['Content-Length'] = body.length.to_s
|
253
|
+
|
254
|
+
if "".respond_to?(:bytesize)
|
255
|
+
def size(str)
|
256
|
+
str.bytesize
|
257
|
+
end
|
258
|
+
else
|
259
|
+
def size(str)
|
260
|
+
str.size
|
261
|
+
end
|
174
262
|
end
|
175
|
-
[status, headers, body]
|
176
263
|
end
|
177
|
-
end
|
264
|
+
end
|
178
265
|
end
|