Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
792 lines (750 sloc) 24.9 KB
# == About camping.rb
#
# Camping comes with two versions of its source code. The code contained in
# lib/camping.rb is compressed, stripped of whitespace, using compact algorithms
# to keep it tight. The unspoken rule is that camping.rb should be flowed with
# no more than 80 characters per line and must not exceed four kilobytes.
#
# On the other hand, lib/camping-unabridged.rb contains the same code, laid out
# nicely with piles of documentation everywhere. This documentation is entirely
# generated from lib/camping-unabridged.rb using RDoc and our "flipbook" template
# found in the extras directory of any camping distribution.
require "uri"
require "rack"
$LOADED_FEATURES << "camping.rb"
class Object #:nodoc:
def meta_def(m,&b) #:nodoc:
(class<<self;self end).send(:define_method,m,&b)
end
end
# If you're new to Camping, you should probably start by reading the first
# chapters of {The Camping Book}[file:book/01_introduction.html#toc].
#
# Okay. So, the important thing to remember is that <tt>Camping.goes :Nuts</tt>
# 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::Controllers is where your controllers live.
# * Camping::Models is where your models live.
# * Camping::Views is where your views live.
# * Camping::Base is a module which is included in all your controllers.
# * Camping::Helpers is a module with useful helpers, both for the controllers
# and the views. You should fill this up with your own helpers.
#
# Camping also ships with:
#
# * Camping::Session adds states to your app.
# * Camping::Server starts up your app in development.
# * Camping::Reloader automatically reloads your apps when a file has changed.
#
# More importantly, Camping also installs The Camping Server,
# please see Camping::Server.
module Camping
C = self
S = IO.read(__FILE__) rescue nil
P = "<h1>Cam\ping Problem!</h1><h2>%s</h2>"
U = Rack::Utils
O = {}
Apps = []
SK = :camping #Key for r.session
# An object-like Hash.
# All Camping query string and cookie variables are loaded as this.
#
# To access the query string, for instance, use the <tt>@input</tt> 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 <tt>/?page=2</tt>, you'll get the second
# page of twenty posts. You can also use <tt>@input['page']</tt> to get the
# value for the <tt>page</tt> query variable.
class H < Hash
# Gets or sets keys in the hash.
#
# @cookies.my_favorite = :macadamian
# @cookies.my_favorite
# => :macadamian
#
def method_missing(m,*a)
m.to_s=~/=$/?self[$`]=a[0]:a==[]?self[m.to_s]:super
end
undef id, type if ?? == 63
end
class Cookies < H
attr_accessor :_p
#
# Cookies that are set at this response
def _n; @n ||= {} end
alias _s []=
def set(k, v, o = {})
_s(j=k.to_s, v)
_n[j] = {:value => v, :path => _p}.update(o)
end
def []=(k, v)
set k, v, v.is_a?(Hash) ? v : {}
end
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' <tt>ApplicationHelper</tt> module.
#
# == Using ActionPack Helpers
#
# If you'd like to include helpers from Rails' modules, you'll need to look
# up the helper module in the Rails documentation at http://api.rubyonrails.org/.
#
# For example, if you look up the <tt>ActionView::Helpers::FormTagHelper</tt>
# class, you'll find that it's loaded from the <tt>action_view/helpers/form_tag_helper.rb</tt>
# file. You'll need to have the ActionPack gem installed for this to work.
#
# A helper often depends on other helpers, so you would have to look up
# the dependencies too. <tt>FormTagHelper</tt> for instance required the
# <tt>content_tag</tt> provided by <tt>TagHelper</tt>.
#
# require 'action_view/helpers/form_tag_helper'
#
# module Nuts::Helpers
# include ActionView::Helpers::TagHelper
# include ActionView::Helpers::FormTagHelper
# end
#
# == Return a response immediately
# If you need to return a response inside a helper, you can use <tt>throw :halt</tt>.
#
# 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
module Helpers
# 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: <tt>/edit/1</tt>.
#
# 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.
#
# == Using R in the View
#
# Keep in mind that this route doesn't include the root path. You will
# need to use <tt>/</tt> (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 <tt>R</tt> or <tt>URL</tt> 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
# <tt>http://localhost:3301/frodo</tt> and that a controller named
# <tt>Logout</tt> is assigned to route <tt>/logout</tt>.
# 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>
#
def R(c,*g)
p,h=/\(.+?\)/,g.grep(Hash)
g-=h
raise "bad route" if !u = c.urls.find{|x|
break x if x.scan(p).size == g.size &&
/^#{x}\/?$/ =~ (x=g.inject(x){|x,a|
x.sub p,U.escape((a.to_param rescue a))}.gsub(/\\(.)/){$1})
}
h.any?? u+"?"+U.build_query(h[0]) : u
end
# Simply builds a complete path from a path +p+ within the app. If your
# application is mounted at <tt>/blog</tt>:
#
# self / "/view/1" #=> "/blog/view/1"
# self / "styles.css" #=> "styles.css"
# self / R(Edit, 1) #=> "/blog/edit/1"
#
def /(p); p[0] == ?/ ? @root + p : p 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 <tt>http://test.ing/blog/</tt>
# and you have a View controller which routes as <tt>R '/view/(\d+)'</tt>:
#
# 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.
def URL c='/',*a
c = R(c, *a) if c.respond_to? :urls
c = self/c
c = @request.url[/.{8,}?(?=\/|$)/]+c if c[0]==?/
URI(c)
end
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 accessible inside your controllers.
module Base
attr_accessor :env, :request, :root, :input, :cookies, :state,
:status, :headers, :body
T = {}
L = :layout
# Finds a template, returning either:
#
# false # => Could not find template
# true # => Found template in Views
# instance of Tilt # => Found template in a file
def lookup(n)
T.fetch(n.to_sym) do |k|
t = Views.method_defined?(k) ||
(t = O[:_t].keys.grep(/^#{n}\./)[0]and Template[t].new{O[:_t][t]}) ||
(f = Dir[[O[:views] || "views", "#{n}.*"]*'/'][0]) &&
Template.new(f, O[f[/\.(\w+)$/, 1].to_sym] || {})
O[:dynamic_templates] ? t : T[k] = t
end
end
# Display a view, calling it by its method name +v+. If a <tt>layout</tt>
# 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
#
def render(v, *a, &b)
if t = lookup(v)
# Has this controller rendered before?
r = @_r
# Set @_r to truthy value
@_r = (o = Hash === a[-1] ? a.pop : {})
s = (t == true) ? mab { send(v, *a, &b) } : t.render(self, o[:locals] || {}, &b)
s = render(L, o.merge(L => false)) { s } if o[L] or o[L].nil? && lookup(L) && !r && v.to_s[0] != ?_
s
else
raise "no template: #{v}"
end
end
# You can directly return HTML from your controller for quick debugging
# by calling this method and passing 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
def mab(&b)
extend Mab
mab(&b)
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")
#
# See also: #r404, #r500 and #r501
def r(s, b, h = {})
b, h = h, b if Hash === b
@status = s
@headers.merge!(h)
@body = b
end
# Formulate a redirect response: a 302 status with <tt>Location</tt> header
# and a blank body. Uses Helpers#URL to build the location from a
# controller route or path.
#
# So, given a root of <tt>http://localhost:3301/articles</tt>:
#
# redirect "view/12" # redirects to "//localhost:3301/articles/view/12"
# redirect View, 12 # redirects to "//localhost:3301/articles/view/12"
#
# <b>NOTE:</b> This method doesn't magically exit your methods and redirect.
# You'll need to <tt>return redirect(...)</tt> if this isn't the last statement
# in your code, or <tt>throw :halt</tt> if it's in a helper.
#
# See: Controllers
def redirect(*a)
r(302,'','Location'=>URL(*a).to_s)
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
def r404(p)
P % "#{p} not found"
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.
#
# By 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
def r500(k,m,e)
raise e
end
# Called if an undefined method is called on a controller, along with the
# request method +m+ (GET, POST, etc.)
def r501(m)
P % "#{m.upcase} not implemented"
end
# Serves the string +c+ with the MIME type of the filename +p+.
# Defaults to text/html.
def serve(p, c)
t = Rack::Mime.mime_type(p[/\..*$/], "text/html") and @headers["Content-Type"] = t
c
end
# Turn a controller into a Rack response. This is designed to be used to
# pipe controllers into the <tt>r</tt> 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
def to_a
@env['rack.session'][SK] = Hash[@state]
r = Rack::Response.new(@body, @status, @headers)
@cookies._n.each do |k, v|
r.set_cookie(k, v)
end
r.to_a
end
def initialize(env, m) #:nodoc:
r = @request = Rack::Request.new(@env = env)
@root, @input, @cookies, @state,
@headers, @status, @method =
r.script_name.sub(/\/$/,''), n(r.params),
Cookies[r.cookies], H[r.session[SK]||{}],
{'Content-Type'=>'text/html'}, m =~ /r(\d+)/ ? $1.to_i : 200, m
@cookies._p = self/"/"
end
def n(h) # :nodoc:
if Hash === h
h.inject(H[]) do |m, (k, v)|
m[k] = n(v)
m
end
else
h
end
end
# All requests pass through this method before going to the controller.
# Some magic in Camping can be performed by overriding this method.
def service(*a)
r = catch(:halt){send(@method, *a)}
@body ||= r
self
end
end
# Controllers receive the requests and send 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
#
# == Defining a controller
#
# There are two ways to define controllers:
#
# 1. Define a class and let Camping figure out the route.
# 2. 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).
#
# After that, it will do the following:
#
# * Replace Index with /
# * Replace X with ([^/]+)
# * Replace N with (\\\d+)
# * Turn everything else into lowercase
# * Join the words with slashes
#
#--
# NB! N will actually be replaced with (\d+), but it needs to be escaped
# here in order to work correctly with RDoc.
#++
#
# Here are a few examples:
#
# Index # => /
# PostN # => /post/(\d+)
# PageX # => /page/([^/]+)
# Pages # => /pages
#
# == The request
#
# The following variables aid in describing request:
#
# * @env contains the environment as defined in http://rack.rubyforge.org/doc/SPEC.html
# * @request is Rack::Request.new(@env)
# * @root is the path where the app is mounted
# * @cookies is a hash with the cookies sent by the client
# * @state is a hash with the sessions (see Camping::Session)
# * @method is the HTTP method in lowercase
#
# == The response
#
# You can change these variables to your needs:
#
# * @status is the HTTP status (defaults to 200)
# * @headers is a hash with the headers
# * @body is the body (a string or something which responds to #each)
# * Any changes in @cookies and @state will also be sent to the client
#
# 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
module Controllers
@r = []
class << self
# 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
def R *u
r=@r
Class.new {
meta_def(:urls){u}
meta_def(:inherited){|x|r<<x}
}
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 lowercased name of the controller.
#
# Controllers are searched in this order:
#
# * Classes without routes, since they refer to a very specific URL.
# * Classes with routes are searched in order of their creation.
#
# So, define your catch-all controllers last.
def D(p, m, e)
p = '/' if !p || !p[0]
a=O[:_t].find{|n,_|n==p} and return [I, :serve, *a]
@r.map { |k|
k.urls.map { |x|
return (k.method_defined?(m)) ?
[k, m, *$~[1..-1].map{|x|U.unescape(x)}] : [I, 'r501', m] if p =~ /^#{x}\/?$/
}
}
[I, 'r404', p]
end
N = H.new { |_,x| x.downcase }.merge! "N" => '(\d+)', "X" => '([^/]+)', "Index" => ''
# The route maker, called by Camping internally.
#
# 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 was called won't work properly.
def M
def M #:nodoc:
end
constants.map { |c|
k = const_get(c)
k.send :include,C,X,Base,Helpers,Models
@r=[k]+@r if @r-[k]==@r
k.meta_def(:urls){["/#{c.to_s.scan(/.[^A-Z]*/).map(&N.method(:[]))*'/'}"]}if !k.respond_to?:urls
}
end
end
# Internal controller with no route. Used to show internal messages.
I = R()
end
X = Controllers
class << self
# When you are running multiple 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
#
# Additionally, you can pass a Binding as the second parameter,
# which enables you to create a Camping-based application within
# another module.
#
# Here's an example of namespacing your web interface and
# code for a worker process together:
#
# module YourApplication
# Camping.goes :Web, binding()
# module Web
# ...
# end
# module Worker
# ...
# end
# end
#
# All the applications will be available in Camping::Apps.
def goes(m, g=TOPLEVEL_BINDING)
Apps << a = eval(S.gsub(/Camping/,m.to_s), g)
caller[0]=~/:/
IO.read(a.set:__FILE__,$`)=~/^__END__/ &&
(b=$'.split(/^@@\s*(.+?)\s*\r?\n/m)).shift rescue nil
a.set :_t,H[*b||[]]
end
# Ruby web servers use this method to enter the Camping realm. The +e+
# argument is the environment variables hash as per the Rack specification.
# Array with [status, headers, body] is expected at the output.
#
# See: http://rack.rubyforge.org/doc/SPEC.html
def call(e)
X.M
k,m,*a=X.D e["PATH_INFO"],e['REQUEST_METHOD'].downcase,e
k.new(e,m).service(*a).to_a
rescue
r500(:I, k, m, $!, :env => e).to_a
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'} ...>
#
def method_missing(m, c, *a)
X.M
h = Hash === a[-1] ? a.pop : {}
e = H[Rack::MockRequest.env_for('',h.delete(:env)||{})]
k = X.const_get(c).new(e,m.to_s)
h.each { |i, v| k.send("#{i}=", v) }
k.service(*a)
end
# Injects a middleware:
#
# module Blog
# use Rack::MethodOverride
# use Rack::Session::Memcache, :key => "session"
# end
def use(*a, &b)
m = a.shift.new(method(:call), *a, &b)
meta_def(:call) { |e| m.call(e) }
end
# A hash where you can set different settings.
def options
O
end
# Shortcut for setting options:
#
# module Blog
# set :secret, "Hello!"
# end
def set(k, v)
O[k] = v
end
end
# Views is an empty module for storing methods which create HTML. The HTML
# is described using the Markaby language.
#
# == Defining and calling templates
#
# 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 <tt>render :template_name</tt> which will
# invoke the template. The views and controllers will share instance
# variables (as you can see above).
#
# == Using the layout method
#
# If your Views module has a <tt>layout</tt> 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
module Views; include X, Helpers 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
#
# == Where Models are Used
#
# 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 from Views at this time.
module Models
autoload :Base,'camping/ar'
Helpers.send(:include, X, self)
end
autoload :Mab, 'camping/mab'
autoload :Template, 'camping/template'
C
end