camping 2.0 → 2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,6 +11,8 @@ $MAB_CODE = %{
11
11
  # and also to replace :href, :action and :src attributes in tags by prefixing the root
12
12
  # path.
13
13
  class Mab < Markaby::Builder
14
+ attr_reader :builder
15
+
14
16
  include Views
15
17
  def tag!(*g,&b)
16
18
  h=g[-1]
@@ -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] = (old = old_apps[key]) || App.new(self)
100
- hash[key].app = app
101
- app.create if app.respond_to?(:create) && !old
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.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
- old = @scripts.dup
149
+ old_scripts = @scripts.dup
160
150
  clear
151
+
161
152
  @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
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
- @scrips = []
163
+ @scripts = []
164
+ @apps = {}
175
165
  end
176
166
 
177
- # Simply calls reload! on all the Script objects.
178
- def reload!
179
- @scripts.each { |script| script.reload! }
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
- # 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)
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
@@ -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
- class Camping::Server
26
- attr_reader :reloader
27
- attr_accessor :conf
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
- def initialize(conf, paths)
30
- @conf = conf
31
- @paths = paths
32
- @reloader = Camping::Reloader.new
33
- connect(@conf.database) if @conf.database
34
- end
35
-
36
- def connect(db)
37
- unless Camping.autoload?(:Models)
38
- Camping::Models::Base.establish_connection(db)
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
- end
41
-
42
- def find_scripts
43
- scripts = @paths.map do |path|
44
- case
45
- when File.file?(path)
46
- path
47
- when File.directory?(path)
48
- Dir[File.join(path, '*.rb')]
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.flatten.compact
51
- @reloader.update(*scripts)
52
- end
53
-
54
- def index_page(apps)
55
- welcome = "You are Camping"
56
- header = <<-HTML
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>#{welcome}</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
- <h1>#{welcome}</h1>
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
- header + main + footer
87
- end
88
-
89
- def app
90
- reload!
91
- all_apps = apps
92
- rapp = case all_apps.length
93
- when 0
94
- proc{|env|[200,{'Content-Type'=>'text/html'},index_page([])]}
95
- when 1
96
- apps.values.first
97
- else
98
- hash = {
99
- "/" => proc {|env|[200,{'Content-Type'=>'text/html'},index_page(all_apps)]}
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
- Rack::URLMap.new(hash)
110
- end
111
- rapp = Rack::ContentLength.new(rapp)
112
- rapp = Rack::Lint.new(rapp)
113
- rapp = XSendfile.new(rapp)
114
- rapp = Rack::ShowExceptions.new(rapp)
115
- end
116
-
117
- def apps
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