Sat Apr 03 16:22:17 +0200 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 584 584: def call(e) 585: X.M 586: p = e['PATH_INFO'] = U.unescape(e['PATH_INFO']) 587: k,m,*a=X.D p,e['REQUEST_METHOD'].downcase 588: k.new(e,m).service(*a).to_a 589: rescue 590: r500(:I, k, m, $!, :env => e).to_a 591: 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 575 575: def goes(m) 576: Apps << eval(S.gsub(/Camping/,m.to_s), TOPLEVEL_BINDING) 577: 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 611 611: def method_missing(m, c, *a) 612: X.M 613: h = Hash === a[-1] ? a.pop : {} 614: e = H[Rack::MockRequest.env_for('',h.delete(:env)||{})] 615: k = X.const_get(c).new(e,m.to_s) 616: h.each { |i, v| k.send("#{i}=", v) } 617: k.service(*a) 618: end
Injects a middleware:
module Blog use Rack::MethodOverride use Rack::Session::Memcache, :key => "session" end
[ show source ]
# File lib/camping-unabridged.rb, line 626 626: def use(*a, &b) 627: m = a.shift.new(method(:call), *a, &b) 628: meta_def(:call) { |e| m.call(e) } 629: 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 265 265: def mab(l=nil,&b) 266: m=Mab.new({},self) 267: s=m.capture(&b) 268: s=m.capture{layout{s}} if l && m.respond_to?(:layout) 269: s 270: 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 283 283: def r(s, b, h = {}) 284: b, h = h, b if Hash === b 285: @status = s 286: @headers.merge!(h) 287: @body = b 288: 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 317 317: def r404(p) 318: P % "#{p} not found" 319: 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 336 336: def r500(k,m,e) 337: raise e 338: 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 342 342: def r501(m) 343: P % "#{m.upcase} not implemented" 344: 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 304 304: def redirect(*a) 305: r(302,'','Location'=>URL(*a).to_s) 306: 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 251 251: def render(v,*a,&b) 252: mab(/^_/!~v.to_s){send(v,*a,&b)} 253: 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 390 390: def service(*a) 391: r = catch(:halt){send(@method, *a)} 392: @body ||= r 393: self 394: 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 357 357: def to_a 358: @env['rack.session'] = @state 359: r = Rack::Response.new(@body, @status, @headers) 360: @cookies.each do |k, v| 361: next if @old_cookies[k] == v 362: v = { :value => v, :path => self / "/" } if String === v 363: r.set_cookie(k, v) 364: end 365: r.to_a 366: 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 520 520: def D(p, m) 521: p = '/' if !p || !p[0] 522: r.map { |k| 523: k.urls.map { |x| 524: return (k.instance_method(m) rescue nil) ? 525: [k, m, *$~[1..-1]] : [I, 'r501', m] if p =~ /^#{x}\/?$/ 526: } 527: } 528: [I, 'r404', p] 529: 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 545 545: def M 546: def M #:nodoc: 547: end 548: constants.map { |c| 549: k = const_get(c) 550: k.send :include,C,Base,Helpers,Models 551: @r=[k]+r if r-[k]==r 552: k.meta_def(:urls){["/#{c.scan(/.[^A-Z]*/).map(&N.method(:[]))*'/'}"]}if !k.respond_to?:urls 553: } 554: 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 501 501: def R *u 502: r=@r 503: Class.new { 504: meta_def(:urls){u} 505: meta_def(:inherited){|x|r<<x} 506: } 507: 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 77 77: def method_missing(m,*a) 78: m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m.to_s]:super 79: 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 198 198: 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 180 180: def R(c,*g) 181: p,h=/\(.+?\)/,g.grep(Hash) 182: g-=h 183: raise "bad route" unless u = c.urls.find{|x| 184: break x if x.scan(p).size == g.size && 185: /^#{x}\/?$/ =~ (x=g.inject(x){|x,a| 186: x.sub p,U.escape((a[a.class.primary_key]rescue a))}) 187: } 188: h.any?? u+"?"+U.build_query(h[0]) : u 189: 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 221 221: def URL c='/',*a 222: c = R(c, *a) if c.respond_to? :urls 223: c = self/c 224: c = @request.url[/.{8,}?(?=\/)/]+c if c[0]==?/ 225: URI(c) 226: 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 150 150: def initialize(*scripts) 151: @scripts = [] 152: update(*scripts) 153: 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 185 185: def apps 186: @scripts.inject({}) do |hash, script| 187: hash.merge(script.apps) 188: end 189: end
Removes all the scripts from the reloader.
[ show source ]
# File lib/camping/reloader.rb, line 173 173: def clear 174: @scrips = [] 175: end
Simply calls reload! on all the Script objects.
[ show source ]
# File lib/camping/reloader.rb, line 178 178: def reload! 179: @scripts.each { |script| script.reload! } 180: 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 158 158: def update(*scripts) 159: old = @scripts.dup 160: clear 161: @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 169: end 170: end
This is a simple wrapper which causes the script to reload (if needed) on any method call. Then the method call will be forwarded to the app.
[ show source ]
# File lib/camping/reloader.rb, line 47 47: def initialize(script) 48: @script = script 49: end
Reloads if needed, before calling the method on the app.
[ show source ]
# File lib/camping/reloader.rb, line 52 52: def method_missing(meth, *args, &blk) 53: @script.reload! 54: @app.send(meth, *args, &blk) 55: 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