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