Skip to content

Commit

Permalink
* lib/camping.rb: a few of zimbatm's wondrous patches (#32, got rid …
Browse files Browse the repository at this point in the history
…of R class, using respond_to? :urls instead.)

   Also, trimmed down the `un`, `escape` and `table_name_prefix` methods to clear some space.
 * lib/camping-unabridged.rb: ditto.
 * lib/camping/reloader.rb: extracted from bin/camping, such an essential part of TheCampingServer.
 * bin/camping: console mode (#65 from zimbatm as well!)
  • Loading branch information
_why committed Jul 6, 2006
1 parent 718f74d commit c1f6e46
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 94 deletions.
96 changes: 23 additions & 73 deletions bin/camping
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ require 'stringio'
require 'rubygems'
require 'camping'
require 'camping/session'
require 'camping/reloader'

host, port, db, log = '0.0.0.0', 3301
server, host, port, db, log = :mongrel, '0.0.0.0', 3301

homes = []
homes << File.join( ENV['HOME'], '.camping.db' ) if ENV['HOME']
Expand All @@ -34,6 +35,7 @@ opts = OptionParser.new do |opts|
opts.on("-p", "--port NUM", "Port for web server (defaults to #{port})") { |port| }
opts.on("-d", "--database FILE", "Database file (defaults to #{db})") { |db| }
opts.on("-l", "--log FILE", "Start a database log ('-' for STDOUT)") { |log| }
opts.on("-C", "--console", "Run in console mode with IRB") { server = :console }

opts.separator ""
opts.separator "Common options:"
Expand Down Expand Up @@ -78,73 +80,6 @@ rescue LoadError
abort "!! Please check out http://code.whytheluckystiff.net/camping/wiki/BeAlertWhenOnSqlite3 for tips."
end

class CampingReloader
attr_accessor :klass, :mtime, :mount, :requires

def initialize(script)
@script = File.expand_path(script)
@mount = File.basename(script, '.rb')
@requires = nil
load_app
end

def load_app
title = File.basename(@script)[/^(\w+)/,1]
begin
all_requires = $LOADED_FEATURES.dup
load @script
@requires = ($LOADED_FEATURES - all_requires).select do |req|
req.index(File.basename(@script) + "/") == 0 || req.index(title + "/") == 0
end
rescue Exception => e
puts "!! trouble loading #{title}: [#{e.class}] #{e.message}"
@klass = nil
return
end

@mtime = mtime
@klass = Object.const_get(Object.constants.grep(/^#{title}$/i)[0]) rescue nil
unless @klass and @klass.const_defined? :C
puts "!! trouble loading #{title}: not a Camping app"
@klass = nil
return
end
@klass.create if @klass.respond_to? :create
@klass
end

def mtime
((@requires || []) + [@script]).map do |fname|
fname = fname.gsub(/^#{Regexp::quote File.dirname(@script)}\//, '')
File.mtime(File.join(File.dirname(@script), fname))
end.max
end

# Load the script, locate the module
def reload_app
return if @klass and @mtime and mtime <= @mtime

if @requires
@requires.each { |req| $LOADED_FEATURES.delete(req) }
end
k = @klass
Object.instance_eval { remove_const k.name } if k
load_app
end

def run(*a)
reload_app
if @klass
@klass.run(*a)
else
Camping.run(*a)
end
end

def view_source
File.read(@script)
end
end

apps = ARGV.inject([]) do |apps, script|
if File.directory? script
Expand All @@ -154,7 +89,7 @@ apps = ARGV.inject([]) do |apps, script|
end
end

apps.map! { |script| CampingReloader.new(script) }
apps.map! { |script| Camping::Reloader.new(script) }
abort("** No apps successfully loaded") unless apps.detect { |app| app.klass }

def apps.index_page
Expand Down Expand Up @@ -194,9 +129,18 @@ def apps.index_page
b.to_s
end

begin
require 'mongrel'
require 'mongrel/camping'
# Check that mongrel exists
if server == :mongrel
begin
require 'mongrel'
require 'mongrel/camping'
rescue LoadError
server = :webrick
end
end

case server
when :mongrel
class IndexHandler < Mongrel::HttpHandler
def initialize(apps)
@apps = apps
Expand Down Expand Up @@ -235,7 +179,7 @@ begin
end
end
config.join
rescue LoadError
when :webrick
require 'webrick/httpserver'
require 'camping/webrick'

Expand All @@ -259,4 +203,10 @@ rescue LoadError
s.shutdown
end
s.start
when :console
ARGV.clear # Avoid passing args to IRB

require 'irb'
require 'irb/completion'
IRB.start
end
18 changes: 8 additions & 10 deletions lib/camping-unabridged.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ module Camping
Apps = []
C = self
S = IO.read(__FILE__).sub(/S=I.+$/,'')
P="Cam\ping Problem!"

H = HashWithIndifferentAccess
# An object-like Hash, based on ActiveSupport's HashWithIndifferentAccess.
Expand Down Expand Up @@ -439,9 +440,6 @@ def markaby #:nodoc:
end
end
# The R class is the parent class for all routed controllers.
class R; include Base end
# Controllers is a module for placing classes which handle URLs. This is done
# by defining a route to each class using the Controllers::R method.
#
Expand Down Expand Up @@ -477,7 +475,7 @@ module Controllers
# end
# end
#
class NotFound; def get(p); r(404, div{h1("Cam\ping Problem!");h2("#{p} not found")}); end end
class NotFound; def get(p); r(404, div{h1(P);h2("#{p} not found")}); end end
# The ServerError class is a special controller class for handling many (but not all) 500 errors.
# If there is a parse error in Camping or in your application's source code, it will not be caught
Expand All @@ -502,7 +500,7 @@ class NotFound; def get(p); r(404, div{h1("Cam\ping Problem!");h2("#{p} not foun
# end
# end
#
class ServerError; include Base; def get(k,m,e); r(500, Mab.new { h1 "Cam\ping Problem!"; h2 "#{k}.#{m}"; h3 "#{e.class} #{e.message}:"; ul { e.backtrace.each { |bt| li bt } } }.to_s) end end
class ServerError; include Base; def get(k,m,e); r(500, Mab.new { h1(P); h2 "#{k}.#{m}"; h3 "#{e.class} #{e.message}:"; ul { e.backtrace.each { |bt| li bt } } }.to_s) end end
class << self
# Add routes to a controller class by piling them into the R method.
Expand All @@ -524,7 +522,7 @@ class << self
#
# Most of the time the rules inferred by dispatch method Controllers::D will get you
# by just fine.
def R(*urls); Class.new(R) { meta_def(:urls) { urls } }; end
def R(*urls); Class.new { meta_def(:urls) { urls } }; end
# Dispatch routes to controller classes. Classes are searched in no particular order.
# For each class, routes are checked for a match based on their order in the routing list
Expand All @@ -533,7 +531,7 @@ def R(*urls); Class.new(R) { meta_def(:urls) { urls } }; end
def D(path)
constants.inject(nil) do |d,c|
k = const_get(c)
k.meta_def(:urls){["/#{c.downcase}"]}if !(k<R)
k.meta_def(:urls){["/#{c.downcase}"]}if !k.respond_to? :urls
d||([k, $~[1..-1]] if k.urls.find { |x| path =~ /^#{x}\/?$/ })
end||[NotFound, [path]]
end
Expand Down Expand Up @@ -563,13 +561,13 @@ def goes(m)
# #=> "I%27d+go+to+the+museum+straightway%21"
#
def escape(s); s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n){'%'+$1.unpack('H2'*$1.size).join('%').upcase}.tr(' ', '+') end
def escape(s); s.to_s.gsub(/[^ \w.-]+/n){'%'+($&.unpack('H2'*$&.size)*'%').upcase}.tr(' ', '+') end
# Unescapes a URL-encoded string.
#
# Camping.un("I%27d+go+to+the+museum+straightway%21")
# #=> "I'd go to the museum straightway!"
#
def un(s); s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%')].pack('H*')} end
def un(s); s.tr('+', ' ').gsub(/%([\da-f]){2}/in){[$1].pack('H*')} end
# Parses a query string into an Camping::H object.
#
Expand Down Expand Up @@ -674,7 +672,7 @@ module Models
# #=> "tepee_pages"
#
def Base.table_name_prefix
"#{name[/^(\w+)/,1]}_".downcase.sub(/^(#{A}|camping)_/i,'')
"#{name[/\w+/]}_".downcase.sub(/^(#{A}|camping)_/i,'')
end
end
Expand Down
23 changes: 12 additions & 11 deletions lib/camping.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
%w[active_record markaby metaid tempfile uri].each{|l|require l}
module Camping;Apps=[];C=self;S=IO.read(__FILE__).sub(/S=I.+$/,'')
P="Cam\ping Problem!"
module Helpers;def R c,*args;p=/\(.+?\)/;args.inject(c.urls.find{|x|x.scan(p).
size==args.size}.dup){|str,a|str.sub(p,C.escape((a.__send__(a.class.primary_key
)rescue a)))} end;def URL c='/',*a;c=R(c,*a)if c.respond_to?:urls;c=self/c;c=
Expand All @@ -25,25 +26,25 @@ module Base;include Helpers;attr_accessor :input,:cookies,:env,:headers,:body,
" if v != @k[k]}.compact;self end;def to_s;"Status: #{@status}\r\n#{@headers.map{
|k,v|[*v].map{|x|"#{k}: #{x}"}}*"\r\n"}\r\n\r\n#{@body}" end;def markaby;Mab.new(
instance_variables.map{|iv|[iv[1..-1],instance_variable_get(iv)]}) end;def
markaview m,*a,&b;h=markaby;h.send m,*a,&b;h.to_s end end;class R;include Base
end;module Controllers;class NotFound;def get p;r(404,div{h1 "Cam\ping Problem!"
markaview m,*a,&b;h=markaby;h.send m,*a,&b;h.to_s end end
module Controllers;class NotFound;def get p;r(404,div{h1 P
h2 p+" not found"}) end end;class ServerError;include Base;def get k,m,e;r(500,
Mab.new{h1 "Cam\ping Problem!";h2 "#{k}.#{m}";h3 "#{e.class} #{e.message}:";ul{
e.backtrace.each{|bt|li(bt)}}}.to_s)end end;class<<self;def R *urls;Class.new(
R){meta_def(:urls){urls}}end;def D path;constants.inject(nil){|d,c|k=const_get c
k.meta_def(:urls){["/#{c.downcase}"]}if !(k<R);d||([k,$~[1..-1]]if k.urls.find{
Mab.new{h1 P;h2 "#{k}.#{m}";h3 "#{e.class} #{e.message}:";ul{
e.backtrace.each{|bt|li(bt)}}}.to_s)end end;class<<self;def R *urls;Class.new{
meta_def(:urls){urls}}end;def D path;constants.inject(nil){|d,c|k=const_get c
k.meta_def(:urls){["/#{c.downcase}"]}if !k.respond_to? :urls;d||([k,$~[1..-1]]if k.urls.find{
|x|path=~/^#{x}\/?$/})}||[NotFound,[path]] end end end;class<<self;def goes m
eval(S.gsub(/Camping/,m.to_s),TOPLEVEL_BINDING);Apps<<const_get(m);end;def escape s;s.to_s.gsub(
/([^ a-zA-Z0-9_.-]+)/n){'%'+$1.unpack('H2'*$1.size).join('%').upcase}.tr ' ','+'
end;def un s;s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%'
)].pack('H*')} end;def qs_parse q,d='&;';m=proc{|_,o,n|o.u(n,&m)rescue([*o
eval(S.gsub(/Camping/,m.to_s),TOPLEVEL_BINDING);Apps<<const_get(m);end
def escape(s)s.to_s.gsub(/[^ \w.-]+/n){'%'+($&.unpack('H2'*$&.size)*'%').upcase}.tr(' ','+')end
def un(s)s.tr('+',' ').gsub(/%([\da-f]{2})/in){[$1].pack('H*')} end
def qs_parse q,d='&;';m=proc{|_,o,n|o.u(n,&m)rescue([*o
]<<n)};q.to_s.split(/[#{d}] */n).inject(H[]){|h,p|k,v=un(p).split('=',2)
h.u k.split(/[\]\[]+/).reverse.inject(v){|x,i|H[i,x]},&m}end;def kp(s);c=
qs_parse(s,';,')end;def run r=$stdin,e=ENV;k,a=Controllers.D un("/#{e['PATH_INFO']
}".gsub(/\/+/,'/'));k.send:include,C,Base,Models;k.new(r,e,(m=e['REQUEST_METHOD'
]||"GET")).service *a;rescue Exception=>x;Controllers::ServerError.new(r,e,'get').service(
k,m,x)end end;module Views;include Controllers,Helpers end;module Models;A=
ActiveRecord;Base=A::Base;def Base.table_name_prefix;"#{name[/^(\w+)/,1]}_".
ActiveRecord;Base=A::Base;def Base.table_name_prefix;"#{name[/\w+/]}_".
downcase.sub(/^(#{A}|camping)_/i,'')end end;class Mab<Markaby::Builder;include \
Views;def tag! *g,&b;h=g[-1];[:href,:action,:src].map{|a|(h[a]=self/h[a])rescue
0};super end end;H=HashWithIndifferentAccess;class H;def method_missing m,*a
Expand Down
107 changes: 107 additions & 0 deletions lib/camping/reloader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
module Camping
# == The Camping Reloader
#
# Camping apps are generally small and predictable. Many Camping apps are
# contained within a single file. Larger apps are split into a handful of
# other Ruby libraries within the same directory.
#
# Since Camping apps (and their dependencies) are loaded with Ruby's require
# method, there is a record of them in $LOADED_FEATURES. Which leaves a
# perfect space for this class to manage auto-reloading an app if any of its
# immediate dependencies changes.
#
# == Wrapping Your Apps
#
# Since bin/camping and the Camping::FastCGI class already use the Reloader,
# you probably don't need to hack it on your own. But, if you're rolling your
# own situation, here's how.
#
# Rather than this:
#
# require 'yourapp'
#
# Use this:
#
# require 'camping/reloader'
# Camping::Reloader.new('/path/to/yourapp.rb')
#
# The reloader will take care of requiring the app and monitoring all files
# for alterations.
class Reloader
attr_accessor :klass, :mtime, :mount, :requires

# Creates the reloader, assigns a +script+ to it and initially loads the
# application. Pass in the full path to the script, otherwise the script
# will be loaded relative to the current working directory.
def initialize(script)
@script = File.expand_path(script)
@mount = File.basename(script, '.rb')
@requires = nil
load_app
end

# Loads (or reloads) the application. The reloader will take care of calling
# this for you. You can certainly call it yourself if you feel it's warranted.
def load_app
title = File.basename(@script)[/^([\w_]+)/,1].gsub /_/,''
begin
all_requires = $LOADED_FEATURES.dup
load @script
@requires = ($LOADED_FEATURES - all_requires).select do |req|
req.index(File.basename(@script) + "/") == 0 || req.index(title + "/") == 0
end
rescue Exception => e
puts "!! trouble loading #{title}: [#{e.class}] #{e.message}"
@klass = nil
return
end

@mtime = mtime
@klass = Object.const_get(Object.constants.grep(/^#{title}$/i)[0]) rescue nil
unless @klass and @klass.const_defined? :C
puts "!! trouble loading #{title}: not a Camping app"
@klass = nil
return
end
@klass.create if @klass.respond_to? :create
@klass
end

# The timestamp of the most recently modified app dependency.
def mtime
((@requires || []) + [@script]).map do |fname|
fname = fname.gsub(/^#{Regexp::quote File.dirname(@script)}\//, '')
File.mtime(File.join(File.dirname(@script), fname))
end.max
end

# Conditional reloading of the app. This gets called on each request and
# only reloads if the modification times on any of the files is updated.
def reload_app
return if @klass and @mtime and mtime <= @mtime

if @requires
@requires.each { |req| $LOADED_FEATURES.delete(req) }
end
k = @klass
Object.instance_eval { remove_const k.name } if k
load_app
end

# Conditionally reloads (using reload_app.) Then passes the request through
# to the wrapped Camping app.
def run(*a)
reload_app
if @klass
@klass.run(*a)
else
Camping.run(*a)
end
end

# Returns source code for the main script in the application.
def view_source
File.read(@script)
end
end
end

0 comments on commit c1f6e46

Please sign in to comment.