Sat Jan 23 21:53:06 +0100 2010
If you’re new to Camping, you should probably start by reading the first chapters of The Camping Book.
Okay. So, the important thing to remember is that Camping.goes :Nuts copies the Camping module into Nuts. This means that you should never use any of these methods/classes on the Camping module, but rather on your own app. Here’s a short explanation on how Camping is organized:
Camping also ships with:
More importantly, Camping also installs The Camping Server, please see Camping::Server.
Ruby web servers use this method to enter the Camping realm. The e argument is the environment variables hash as per the Rack specification. And array with [status, headers, body] is expected at the output.
See: rack.rubyforge.org/doc/SPEC.html
[ show source ]
# File lib/camping-unabridged.rb, line 583
583: def call(e)
584: X.M
585: p = e['PATH_INFO'] = U.unescape(e['PATH_INFO'])
586: k,m,*a=X.D p,e['REQUEST_METHOD'].downcase
587: k.new(e,m).service(*a).to_a
588: rescue
589: r500(:I, k, m, $!, :env => e).to_a
590: end
When you are running many applications, you may want to create independent modules for each Camping application. Camping::goes defines a toplevel constant with the whole MVC rack inside:
require 'camping' Camping.goes :Nuts module Nuts::Controllers; ... end module Nuts::Models; ... end module Nuts::Views; ... end
All the applications will be available in Camping::Apps.
[ show source ]
# File lib/camping-unabridged.rb, line 574
574: def goes(m)
575: Apps << eval(S.gsub(/Camping/,m.to_s), TOPLEVEL_BINDING)
576: end
The Camping scriptable dispatcher. Any unhandled method call to the app module will be sent to a controller class, specified as an argument.
Blog.get(:Index) #=> #<Blog::Controllers::Index ... >
The controller object contains all the @cookies, @body, @headers, etc. formulated by the response.
You can also feed environment variables and query variables as a hash, the final argument.
Blog.post(:Login, :input => {'username' => 'admin', 'password' => 'camping'})
#=> #<Blog::Controllers::Login @user=... >
Blog.get(:Info, :env => {'HTTP_HOST' => 'wagon'})
#=> #<Blog::Controllers::Info @headers={'HTTP_HOST'=>'wagon'} ...>
[ show source ]
# File lib/camping-unabridged.rb, line 610
610: def method_missing(m, c, *a)
611: X.M
612: h = Hash === a[-1] ? a.pop : {}
613: e = H[Rack::MockRequest.env_for('',h.delete(:env)||{})]
614: k = X.const_get(c).new(e,m.to_s)
615: h.each { |i, v| k.send("#{i}=", v) }
616: k.service(*a)
617: end
Injects a middleware:
module Blog
use Rack::MethodOverride
use Rack::Session::Memcache, :key => "session"
end
[ show source ]
# File lib/camping-unabridged.rb, line 625
625: def use(*a, &b)
626: m = a.shift.new(method(:call), *a, &b)
627: meta_def(:call) { |e| m.call(e) }
628: end
Camping::Base is built into each controller by way of the generic routing class Camping::R. In some ways, this class is trying to do too much, but it saves code for all the glue to stay in one place. Forgivable, considering that it’s only really a handful of methods and accessors.
Everything in this module is accessable inside your controllers.
You can directly return HTML form your controller for quick debugging by calling this method and pass some Markaby to it.
module Nuts::Controllers
class Info
def get; mab{ code @headers.inspect } end
end
end
You can also pass true to use the :layout HTML wrapping method
[ show source ]
# File lib/camping-unabridged.rb, line 264
264: def mab(l=nil,&b)
265: m=Mab.new({},self)
266: s=m.capture(&b)
267: s=m.capture{layout{s}} if l && m.respond_to?(:layout)
268: s
269: end
A quick means of setting this controller’s status, body and headers based on a Rack response:
r(302, 'Location' => self / "/view/12", '') r(*another_app.call(@env))
You can also switch the body and the header if you want:
r(404, "Could not find page")
[ show source ]
# File lib/camping-unabridged.rb, line 282
282: def r(s, b, h = {})
283: b, h = h, b if Hash === b
284: @status = s
285: @headers.merge!(h)
286: @body = b
287: end
Called when a controller was not found. You can override this if you want to customize the error page:
module Nuts
def r404(path)
@path = path
render :not_found
end
end
[ show source ]
# File lib/camping-unabridged.rb, line 316
316: def r404(p)
317: P % "#{p} not found"
318: end
Called when an exception is raised. However, if there is a parse error in Camping or in your application’s source code, it will not be caught.
k is the controller class, m is the request method (GET, POST, etc.) and e is the Exception which can be mined for useful info.
Be default this simply re-raises the error so a Rack middleware can handle it, but you are free to override it here:
module Nuts
def r500(klass, method, exception)
send_email_alert(klass, method, exception)
render :server_error
end
end
[ show source ]
# File lib/camping-unabridged.rb, line 335
335: def r500(k,m,e)
336: raise e
337: end
Called if an undefined method is called on a controller, along with the request method m (GET, POST, etc.)
[ show source ]
# File lib/camping-unabridged.rb, line 341
341: def r501(m)
342: P % "#{m.upcase} not implemented"
343: end
Formulate a redirect response: a 302 status with Location header and a blank body. Uses Helpers#URL to build the location from a controller route or path.
So, given a root of localhost:3301/articles:
redirect "view/12" # redirects to "//localhost:3301/articles/view/12" redirect View, 12 # redirects to "//localhost:3301/articles/view/12"
NOTE: This method doesn’t magically exit your methods and redirect. You’ll need to return redirect(...) if this isn’t the last statement in your code, or throw :halt if it’s in a helper.
See: Controllers
[ show source ]
# File lib/camping-unabridged.rb, line 303
303: def redirect(*a)
304: r(302,'','Location'=>URL(*a).to_s)
305: end
Display a view, calling it by its method name v. If a layout method is found in Camping::Views, it will be used to wrap the HTML.
module Nuts::Controllers
class Show
def get
@posts = Post.find :all
render :index
end
end
end
[ show source ]
# File lib/camping-unabridged.rb, line 250
250: def render(v,*a,&b)
251: mab(/^_/!~v.to_s){send(v,*a,&b)}
252: end
All requests pass through this method before going to the controller. Some magic in Camping can be performed by overriding this method.
[ show source ]
# File lib/camping-unabridged.rb, line 389
389: def service(*a)
390: r = catch(:halt){send(@method, *a)}
391: @body ||= r
392: self
393: end
Turn a controller into a Rack response. This is designed to be used to pipe controllers into the r method. A great way to forward your requests!
class Read < '/(\d+)'
def get(id)
Post.find(id)
rescue
r *Blog.get(:NotFound, @headers.REQUEST_URI)
end
end
[ show source ]
# File lib/camping-unabridged.rb, line 356
356: def to_a
357: @env['rack.session'] = @state
358: r = Rack::Response.new(@body, @status, @headers)
359: @cookies.each do |k, v|
360: next if @old_cookies[k] == v
361: v = { :value => v, :path => self / "/" } if String === v
362: r.set_cookie(k, v)
363: end
364: r.to_a
365: end
Controllers receive the requests and sends a response back to the client. A controller is simply a class which must implement the HTTP methods it wants to accept:
module Nuts::Controllers
class Index
def get
"Hello World"
end
end
class Posts
def post
Post.create(@input)
redirect Index
end
end
end
There are two ways to define controllers: Just defining a class and let Camping figure out the route, or add the route explicitly using R.
If you don’t use R, Camping will first split the controller name up by words (HelloWorld => Hello and World). Then it would do the following:
Here’s a few examples:
Index # => / PostN # => /post/(\d+) PageX # => /page/([^/]+) Pages # => /pages
You have these variables which describes the request:
You can change these variables to your needs:
If you haven’t set @body, it will use the return value of the method:
module Nuts::Controllers
class Index
def get
"This is the body"
end
end
class Posts
def get
@body = "Hello World!"
"This is ignored"
end
end
end
Dispatch routes to controller classes. For each class, routes are checked for a match based on their order in the routing list given to Controllers::R. If no routes were given, the dispatcher uses a slash followed by the name of the controller lowercased.
Controllers are searched in this order:
So, define your catch-all controllers last.
[ show source ]
# File lib/camping-unabridged.rb, line 519
519: def D(p, m)
520: p = '/' if !p || !p[0]
521: r.map { |k|
522: k.urls.map { |x|
523: return (k.instance_method(m) rescue nil) ?
524: [k, m, *$~[1..-1]] : [I, 'r501', m] if p =~ /^#{x}\/?$/
525: }
526: }
527: [I, 'r404', p]
528: end
The route maker, this is called by Camping internally, you shouldn’t need to call it.
Still, it’s worth know what this method does. Since Ruby doesn’t keep track of class creation order, we’re keeping an internal list of the controllers which inherit from R(). This method goes through and adds all the remaining routes to the beginning of the list and ensures all the controllers have the right mixins.
Anyway, if you are calling the URI dispatcher from outside of a Camping server, you’ll definitely need to call this to set things up. Don’t call it too early though. Any controllers added after this method is called won’t work properly
[ show source ]
# File lib/camping-unabridged.rb, line 544
544: def M
545: def M #:nodoc:
546: end
547: constants.map { |c|
548: k = const_get(c)
549: k.send :include,C,Base,Helpers,Models
550: @r=[k]+r if r-[k]==r
551: k.meta_def(:urls){["/#{c.scan(/.[^A-Z]*/).map(&N.method(:[]))*'/'}"]}if !k.respond_to?:urls
552: }
553: end
Add routes to a controller class by piling them into the R method.
The route is a regexp which will match the request path. Anything enclosed in parenthesis will be sent to the method as arguments.
module Camping::Controllers
class Edit < R '/edit/(\d+)', '/new'
def get(id)
if id # edit
else # new
end
end
end
end
[ show source ]
# File lib/camping-unabridged.rb, line 500
500: def R *u
501: r=@r
502: Class.new {
503: meta_def(:urls){u}
504: meta_def(:inherited){|x|r<<x}
505: }
506: end
An object-like Hash. All Camping query string and cookie variables are loaded as this.
To access the query string, for instance, use the @input variable.
module Blog::Controllers
class Index < R '/'
def get
if (page = @input.page.to_i) > 0
page -= 1
end
@posts = Post.all, :offset => page * 20, :limit => 20
render :index
end
end
end
In the above example if you visit /?page=2, you’ll get the second page of twenty posts. You can also use @input['page'] to get the value for the page query variable.
Gets or sets keys in the hash.
@cookies.my_favorite = :macadamian @cookies.my_favorite => :macadamian
[ show source ]
# File lib/camping-unabridged.rb, line 76
76: def method_missing(m,*a)
77: m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m.to_s]:super
78: end
Helpers contains methods available in your controllers and views. You may add methods of your own to this module, including many helper methods from Rails. This is analogous to Rails’ ApplicationHelper module.
If you’d like to include helpers from Rails’ modules, you’ll need to look up the helper module in the Rails documentation at api.rubyonrails.org/.
For example, if you look up the ActionView::Helpers::FormTagHelper class, you’ll find that it’s loaded from the action_view/helpers/form_tag_helper.rb file. You’ll need to have the ActionPack gem installed for this to work.
Often the helpers depends on other helpers, so you would have to look up the dependencies too. FormTagHelper for instance required the content_tag provided by TagHelper.
require 'action_view/helpers/form_tag_helper'
module Nuts::Helpers
include ActionView::Helpers::TagHelper
include ActionView::Helpers::FormTagHelper
end
If you need to return a response inside a helper, you can use throw :halt.
module Nuts::Helpers
def requires_login!
unless @state.user_id
redirect Login
throw :halt
end
end
end
module Nuts::Controllers
class Admin
def get
requires_login!
"Never gets here unless you're logged in"
end
end
end
Simply builds a complete path from a path p within the app. If your application is mounted at /blog:
self / "/view/1" #=> "/blog/view/1" self / "styles.css" #=> "styles.css" self / R(Edit, 1) #=> "/blog/edit/1"
[ show source ]
# File lib/camping-unabridged.rb, line 197
197: def /(p); p[0]==?/?@root+p:p end
From inside your controllers and views, you will often need to figure out the route used to get to a certain controller c. Pass the controller class and any arguments into the R method, a string containing the route will be returned to you.
Assuming you have a specific route in an edit controller:
class Edit < R '/edit/(\d+)'
A specific route to the Edit controller can be built with:
R(Edit, 1)
Which outputs: /edit/1.
If a controller has many routes, the route will be selected if it is the first in the routing list to have the right number of arguments.
Keep in mind that this route doesn’t include the root path. You will need to use / (the slash method above) in your controllers. Or, go ahead and use the Helpers#URL method to build a complete URL for a route.
However, in your views, the :href, :src and :action attributes automatically pass through the slash method, so you are encouraged to use R or URL in your views.
module Nuts::Views
def menu
div.menu! do
a 'Home', :href => URL()
a 'Profile', :href => "/profile"
a 'Logout', :href => R(Logout)
a 'Google', :href => 'http://google.com'
end
end
end
Let’s say the above example takes place inside an application mounted at localhost:3301/frodo and that a controller named Logout is assigned to route /logout. The HTML will come out as:
<div id="menu">
<a href="http://localhost:3301/frodo/">Home</a>
<a href="/frodo/profile">Profile</a>
<a href="/frodo/logout">Logout</a>
<a href="http://google.com">Google</a>
</div>
[ show source ]
# File lib/camping-unabridged.rb, line 179
179: def R(c,*g)
180: p,h=/\(.+?\)/,g.grep(Hash)
181: g-=h
182: raise "bad route" unless u = c.urls.find{|x|
183: break x if x.scan(p).size == g.size &&
184: /^#{x}\/?$/ =~ (x=g.inject(x){|x,a|
185: x.sub p,U.escape((a[a.class.primary_key]rescue a))})
186: }
187: h.any?? u+"?"+U.build_query(h[0]) : u
188: end
Builds a URL route to a controller or a path, returning a URI object. This way you’ll get the hostname and the port number, a complete URL.
You can use this to grab URLs for controllers using the R-style syntax. So, if your application is mounted at test.ing/blog/ and you have a View controller which routes as R '/view/(d+)':
URL(View, @post.id) #=> #<URL:http://test.ing/blog/view/12>
Or you can use the direct path:
self.URL #=> #<URL:http://test.ing/blog/>
self.URL + "view/12" #=> #<URL:http://test.ing/blog/view/12>
URL("/view/12") #=> #<URL:http://test.ing/blog/view/12>
It’s okay to pass URL strings through this method as well:
URL("http://google.com") #=> #<URL:http://google.com>
Any string which doesn’t begin with a slash will pass through unscathed.
[ show source ]
# File lib/camping-unabridged.rb, line 220
220: def URL c='/',*a
221: c = R(c, *a) if c.respond_to? :urls
222: c = self/c
223: c = @request.url[/.{8,}?(?=\/)/]+c if c[0]==?/
224: URI(c)
225: end
Models is an empty Ruby module for housing model classes derived from ActiveRecord::Base. As a shortcut, you may derive from Base which is an alias for ActiveRecord::Base.
module Camping::Models
class Post < Base; belongs_to :user end
class User < Base; has_many :posts end
end
Models are used in your controller classes. However, if your model class name conflicts with a controller class name, you will need to refer to it using the Models module.
module Camping::Controllers
class Post < R '/post/(\d+)'
def get(post_id)
@post = Models::Post.find post_id
render :index
end
end
end
Models cannot be referred to in Views at this time.
The default prefix for Camping model classes is the topmost module name lowercase and followed with an underscore.
Tepee::Models::Page.table_name_prefix
#=> "tepee_pages"
[ show source ]
# File lib/camping/ar.rb, line 66
66: def Base.table_name_prefix
67: "#{name[/\w+/]}_".downcase.sub(/^(#{A}|camping)_/i,'')
68: end
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.
Since bin/camping and the Camping::Server 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'
reloader = Camping::Reloader.new('/path/to/yourapp.rb')
blog = reloader.apps[:Blog]
wiki = reloader.apps[:Wiki]
The blog and wiki objects will behave exactly like your Blog and Wiki, but they will update themselves if yourapp.rb changes.
You can also give Reloader more than one script.
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.
[ show source ]
# File lib/camping/reloader.rb, line 147
147: def initialize(*scripts)
148: @scripts = []
149: update(*scripts)
150: end
Returns a Hash of all the apps available in the scripts, where the key would be the name of the app (the one you gave to Camping.goes) and the value would be the app (wrapped inside App).
[ show source ]
# File lib/camping/reloader.rb, line 182
182: def apps
183: @scripts.inject({}) do |hash, script|
184: hash.merge(script.apps)
185: end
186: end
Removes all the scripts from the reloader.
[ show source ]
# File lib/camping/reloader.rb, line 170
170: def clear
171: @scrips = []
172: end
Simply calls reload! on all the Script objects.
[ show source ]
# File lib/camping/reloader.rb, line 175
175: def reload!
176: @scripts.each { |script| script.reload! }
177: end
Updates the reloader to only use the scripts provided:
reloader.update("examples/blog.rb", "examples/wiki.rb")
[ show source ]
# File lib/camping/reloader.rb, line 155
155: def update(*scripts)
156: old = @scripts.dup
157: clear
158: @scripts = scripts.map do |script|
159: s = Script.new(script)
160: if pos = old.index(s)
161: # We already got a script, so we use the old (which might got a mtime)
162: old[pos]
163: else
164: s.load_apps
165: end
166: end
167: end
Camping includes a pretty nifty server which is built for development. It follows these rules:
Run it like this:
camping examples/ # Mounts all apps in that directory camping blog.rb # Mounts Blog at /
And visit localhost:3301/ in your browser.
[ show source ]
# File lib/camping/server.rb, line 29
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
[ show source ]
# File lib/camping/server.rb, line 90
90: def app
91: reload!
92: all_apps = apps
93: rapp = case all_apps.length
94: when 0
95: proc{|env|[200,{'Content-Type'=>'text/html'},index_page([])]}
96: when 1
97: apps.values.first
98: else
99: hash = {
100: "/" => proc {|env|[200,{'Content-Type'=>'text/html'},index_page(all_apps)]}
101: }
102: all_apps.each do |mount, wrapp|
103: # We're doing @reloader.reload! ourself, so we don't need the wrapper.
104: app = wrapp.app
105: hash["/#{mount}"] = app
106: hash["/code/#{mount}"] = proc do |env|
107: [200,{'Content-Type'=>'text/plain','X-Sendfile'=>wrapp.script.file},'']
108: end
109: end
110: Rack::URLMap.new(hash)
111: end
112: rapp = Rack::ContentLength.new(rapp)
113: rapp = Rack::Lint.new(rapp)
114: rapp = XSendfile.new(rapp)
115: rapp = Rack::ShowExceptions.new(rapp)
116: end
[ show source ]
# File lib/camping/server.rb, line 118
118: def apps
119: @reloader.apps.inject({}) do |h, (mount, wrapp)|
120: h[mount.to_s.downcase] = wrapp
121: h
122: end
123: end
[ show source ]
# File lib/camping/server.rb, line 125
125: def call(env)
126: app.call(env)
127: end
[ show source ]
# File lib/camping/server.rb, line 36
36: def connect(db)
37: unless Camping.autoload?(:Models)
38: Camping::Models::Base.establish_connection(db)
39: end
40: end
[ show source ]
# File lib/camping/server.rb, line 42
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')]
49: end
50: end.flatten.compact
51: @reloader.update(*scripts)
52: end
[ show source ]
# File lib/camping/server.rb, line 54
54: def index_page(apps)
55: welcome = "You are Camping"
56: header = "<html>\n<head>\n<title>\#{welcome}</title>\n<style type=\"text/css\">\nbody {\nfont-family: verdana, arial, sans-serif;\npadding: 10px 40px;\nmargin: 0;\n}\nh1, h2, h3, h4, h5, h6 {\nfont-family: utopia, georgia, serif;\n}\n</style>\n</head>\n<body>\n<h1>\#{welcome}</h1>\n"
57: footer = '</body></html>'
58: main = if apps.empty?
59: "<p>Good day. I'm sorry, but I could not find any Camping apps."\
60: "You might want to take a look at the console to see if any errors"\
61: "have been raised</p>"
62: else
63: "<p>Good day. These are the Camping apps you've mounted.</p><ul>" +
64: apps.map do |mount, app|
65: "<li><h3 style=\"display: inline\"><a href=\"/#{mount}\">#{app}</a></h3><small> / <a href=\"/code/#{mount}\">View source</a></small></li>"
66: end.join("\n") + '</ul>'
67: end
68:
69: header + main + footer
70: end
[ show source ]
# File lib/camping/server.rb, line 149
149: def reload!
150: find_scripts
151: @reloader.reload!
152: end
[ show source ]
# File lib/camping/server.rb, line 129
129: def start
130: handler, conf = case @conf.server
131: when "console"
132: puts "** Starting console"
133: reload!
134: this = self; eval("self", TOPLEVEL_BINDING).meta_def(:reload!) { this.reload!; nil }
135: ARGV.clear
136: IRB.start
137: exit
138: when "mongrel"
139: puts "** Starting Mongrel on #{@conf.host}:#{@conf.port}"
140: [Rack::Handler::Mongrel, {:Port => @conf.port, :Host => @conf.host}]
141: when "webrick"
142: puts "** Starting WEBrick on #{@conf.host}:#{@conf.port}"
143: [Rack::Handler::WEBrick, {:Port => @conf.port, :BindAddress => @conf.host}]
144: end
145: reload!
146: handler.run(self, conf)
147: end
A Rack middleware for reading X-Sendfile. Should only be used in development.
[ show source ]
# File lib/camping/server.rb, line 164
164: def initialize(app)
165: @app = app
166: end
[ show source ]
# File lib/camping/server.rb, line 168
168: def call(env)
169: status, headers, body = @app.call(env)
170: headers = Rack::Utils::HeaderHash.new(headers)
171: if header = HEADERS.detect { |header| headers.include?(header) }
172: path = headers[header]
173: body = File.read(path)
174: headers['Content-Length'] = body.length.to_s
175: end
176: [status, headers, body]
177: end
To get sessions working for your application:
require 'camping/session' # 1
module Nuts
include Camping::Session # 2
secret "Oh yeah!" # 3
end
Camping only ships with session-cookies. However, the @state variable is simply a shortcut for @env. Therefore you can also use any middleware which sets this variable:
module Nuts
use Rack::Session::Memcache
end
[ show source ]
# File lib/camping/session.rb, line 28
28: def self.included(app)
29: key = "#{app}.state".downcase
30: secret = [__FILE__, File.mtime(__FILE__)].join(":")
31:
32: app.meta_def(:secret) { |val| secret.replace(val) }
33: app.use Rack::Session::Cookie, :key => key, :secret => secret
34: end
Views is an empty module for storing methods which create HTML. The HTML is described using the Markaby language.
Templates are simply Ruby methods with Markaby inside:
module Blog::Views
def index
p "Welcome to my blog"
end
def show
h1 @post.title
self << @post.content
end
end
In your controllers you just call render :template_name which will invoke the template. The views and controllers will share instance variables (as you can see above).
If your Views module has a layout method defined, it will be called with a block which will insert content from your view:
module Blog::Views
def layout
html do
head { title "My Blog "}
body { self << yield }
end
end
end