Created
August 19, 2009 19:03
-
-
Save skoon/170587 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# == 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. | |
# | |
# == Requirements | |
# | |
# Camping requires at least Ruby 1.8.2. | |
# | |
# Camping depends on the following libraries. If you install through RubyGems, | |
# these will be automatically installed for you. | |
# | |
# * ActiveRecord, used in your models. | |
# ActiveRecord is an object-to-relational database mapper with adapters | |
# for SQLite3, MySQL, PostgreSQL, SQL Server and more. | |
# * Markaby, used in your views to describe HTML in plain Ruby. | |
# * MetAid, a few metaprogramming methods which Camping uses. | |
# * Tempfile, for storing file uploads. | |
# | |
# Camping also works well with Mongrel, the swift Ruby web server. | |
# http://rubyforge.org/projects/mongrel Mongrel comes with examples | |
# in its examples/camping directory. | |
# | |
%w[active_support markaby tempfile uri].each { |lib| require lib } | |
# == Camping | |
# | |
# The camping module contains three modules for separating your application: | |
# | |
# * Camping::Models for your database interaction classes, all derived from ActiveRecord::Base. | |
# * Camping::Controllers for storing controller classes, which map URLs to code. | |
# * Camping::Views for storing methods which generate HTML. | |
# | |
# Of use to you is also one module for storing helpful additional methods: | |
# | |
# * Camping::Helpers which can be used in controllers and views. | |
# | |
# == The Camping Server | |
# | |
# How do you run Camping apps? Oh, uh... The Camping Server! | |
# | |
# The Camping Server is, firstly and thusly, a set of rules. At the very least, The Camping Server must: | |
# | |
# * Load all Camping apps in a directory. | |
# * Load new apps that appear in that directory. | |
# * Mount those apps according to their filename. (e.g. blog.rb is mounted at /blog.) | |
# * Run each app's create method upon startup. | |
# * Reload the app if its modification time changes. | |
# * Reload the app if it requires any files under the same directory and one of their modification times changes. | |
# * Support the X-Sendfile header. | |
# | |
# In fact, Camping comes with its own little The Camping Server. | |
# | |
# At a command prompt, run: camping examples/ and the entire examples/ directory will be served. | |
# | |
# Configurations also exist for Apache and Lighttpd. See http://code.whytheluckystiff.net/camping/wiki/TheCampingServer. | |
# | |
# == The create method | |
# | |
# Many postambles will check for your application's create method and will run it | |
# when the web server starts up. This is a good place to check for database tables and create | |
# those tables to save users of your application from needing to manually set them up. | |
# | |
# def Blog.create | |
# unless Blog::Models::Post.table_exists? | |
# ActiveRecord::Schema.define do | |
# create_table :blog_posts, :force => true do |t| | |
# t.column :id, :integer, :null => false | |
# t.column :user_id, :integer, :null => false | |
# t.column :title, :string, :limit => 255 | |
# t.column :body, :text | |
# end | |
# end | |
# end | |
# end | |
# | |
# For more tips, see http://code.whytheluckystiff.net/camping/wiki/GiveUsTheCreateMethod. | |
module Camping | |
# Stores an +Array+ of all Camping applications modules. Modules are added | |
# automatically by +Camping.goes+. | |
# | |
# Camping.goes :Blog | |
# Camping.goes :Tepee | |
# Camping::Apps # => [Blog, Tepee] | |
# | |
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. | |
# All Camping query string and cookie variables are loaded as this. | |
# | |
# To access the query string, for instance, use the @input variable. | |
# | |
# module Blog::Models | |
# class Index < R '/' | |
# def get | |
# if page = @input.page.to_i > 0 | |
# page -= 1 | |
# end | |
# @posts = Post.find :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] or @input['page'] | |
# to get the value for the page query variable. | |
# | |
# Use the @cookies variable in the same fashion to access cookie variables. | |
# Also, the @env variable is an H containing the HTTP headers and server info. | |
class H | |
# 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]:raise(NoMethodError,"#{m}") | |
end | |
alias_method :u, :regular_update | |
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. | |
# | |
# == 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 ActionView::Helpers::FormHelper class, | |
# you'll find that it's loaded from the action_view/helpers/form_helper.rb | |
# file. You'll need to have the ActionPack gem installed for this to work. | |
# | |
# require 'action_view/helpers/form_helper.rb' | |
# | |
# # This example is unfinished.. soon.. | |
# | |
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: /edit/1. | |
# | |
# You may also pass in a model object and the ID of the object will be used. | |
# | |
# 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 / (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 Blog::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 | |
# http://localhost:3301/frodo and that a controller named Logout | |
# is assigned to route /logout. The HTML will come out as: | |
# | |
# | |
# Home # Profile # Logout # Google # | |
# | |
def R(c,*g) | |
p,h=/\(.+?\)/,g.grep(Hash) | |
(g-=h).inject(c.urls.find{|x|x.scan(p).size==g.size}.dup){|s,a| | |
s.sub p,C.escape((a[a.class.primary_key]rescue a)) | |
}+(h.any?? "?"+h[0].map{|x|x.map{|z|C.escape z}*"="}*"&": "") | |
end | |
# Shows AR validation errors for the object passed. | |
# There is no output if there are no errors. | |
# | |
# An example might look like: | |
# | |
# errors_for @post | |
# | |
# Might (depending on actual data) render something like this in Markaby: | |
# | |
# ul.errors do | |
# li "Body can't be empty" | |
# li "Title must be unique" | |
# end | |
# | |
# Add a simple ul.errors {color:red; font-weight:bold;} CSS rule and you | |
# have built-in, usable error checking in only one line of code. :-) | |
# | |
# See AR validation documentation for details on validations. | |
def errors_for(o); ul.errors { o.errors.each_full { |er| li er } } if o.errors.any?; 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" | |
# | |
def /(p); p[/^\//]?@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. | |
# No scheme is given (http or https). | |
# | |
# You can use this to grab URLs for controllers using the R-style syntax. | |
# So, if your application is mounted at http://test.ing/blog/ | |
# and you have a View controller which routes as R '/view/(\d+)': | |
# | |
# URL(View, @post.id) #=> # | |
# | |
# Or you can use the direct path: | |
# | |
# self.URL #=> # | |
# self.URL + "view/12" #=> # | |
# URL("/view/12") #=> # | |
# | |
# Since no scheme is given, you will need to add the scheme yourself: | |
# | |
# "http" + URL("/view/12") #=> "http://test.ing/blog/view/12" | |
# | |
# It's okay to pass URL strings through this method as well: | |
# | |
# 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 = "//"+@env.HTTP_HOST+c if c[/^\//] | |
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. | |
# | |
# == Treating controller methods like Response objects | |
# | |
# Camping originally came with a barebones Response object, but it's often much more readable | |
# to just use your controller as the response. | |
# | |
# Go ahead and alter the status, cookies, headers and body instance variables as you | |
# see fit in order to customize the response. | |
# | |
# module Camping::Controllers | |
# class SoftLink | |
# def get | |
# redirect "/" | |
# end | |
# end | |
# end | |
# | |
# Is equivalent to: | |
# | |
# module Camping::Controllers | |
# class SoftLink | |
# def get | |
# @status = 302 | |
# @headers['Location'] = "/" | |
# end | |
# end | |
# end | |
# | |
module Base | |
include Helpers | |
attr_accessor :input, :cookies, :env, :headers, :body, :status, :root | |
Z = "\r\n" | |
# Display a view, calling it by its method name +m+. If a layout | |
# method is found in Camping::Views, it will be used to wrap the HTML. | |
# | |
# module Camping::Controllers | |
# class Show | |
# def get | |
# @posts = Post.find :all | |
# render :index | |
# end | |
# end | |
# end | |
# | |
def render(m); end; undef_method :render | |
# Any stray method calls will be passed to Markaby. This means you can reply | |
# with HTML directly from your controller for quick debugging. | |
# | |
# module Camping::Controllers | |
# class Info | |
# def get; code @env.inspect end | |
# end | |
# end | |
# | |
# If you have a layout method in Camping::Views, it will be used to | |
# wrap the HTML. | |
def method_missing(*a,&b) | |
a.shift if a[0]==:render | |
m=Mab.new({},self) | |
s=m.capture{send(*a,&b)} | |
s=m.capture{send(:layout){s}} if /^_/!~a[0].to_s and m.respond_to?:layout | |
s | |
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 http://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. | |
def redirect(*a) | |
r(302,'','Location'=>URL(*a)) | |
end | |
# A quick means of setting this controller's status, body and headers. | |
# Used internally by Camping, but... by all means... | |
# | |
# r(302, '', 'Location' => self / "/view/12") | |
# | |
# Is equivalent to: | |
# | |
# redirect "/view/12" | |
# | |
def r(s, b, h = {}); @status = s; @headers.merge!(h); @body = b; end | |
# Turn a controller into an array. 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, @env.REQUEST_URI) | |
# end | |
# end | |
# | |
def to_a;[@status, @body, @headers] end | |
def initialize(r, e, m) #:nodoc: | |
e = H[e.to_hash] | |
@status, @method, @env, @headers, @root = 200, m.downcase, e, | |
{'Content-Type'=>'text/html'}, e.SCRIPT_NAME.sub(/\/$/,'') | |
@k = C.kp(e.HTTP_COOKIE) | |
qs = C.qsp(e.QUERY_STRING) | |
@in = r | |
if %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)|n.match(e.CONTENT_TYPE) | |
b = /(?:\r?\n|\A)#{Regexp::quote("--#$1")}(?:--)?\r$/ | |
until @in.eof? | |
fh=H[] | |
for l in @in | |
case l | |
when Z: break | |
when /^Content-Disposition: form-data;/ | |
fh.u H[*$'.scan(/(?:\s(\w+)="([^"]+)")/).flatten] | |
when /^Content-Type: (.+?)(\r$|\Z)/m | |
puts "=> fh[type] = #$1" | |
fh[:type] = $1 | |
end | |
end | |
fn=fh[:name] | |
o=if fh[:filename] | |
o=fh[:tempfile]=Tempfile.new(:C) | |
o.binmode | |
else | |
fh="" | |
end | |
while l=@in.read(16384) | |
if l=~b | |
o<<$`.chomp | |
@in.seek(-$'.size,IO::SEEK_CUR) | |
break | |
end | |
o<<<"#{k}: #{x}"}} | |
"Status: #{@status}#{Z+a*Z+Z*2+@body}" | |
end | |
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. | |
# | |
# module Camping::Controllers | |
# class Edit < R '/edit/(\d+)' | |
# def get; end | |
# def post; end | |
# end | |
# end | |
# | |
# If no route is set, Camping will guess the route from the class name. | |
# The rule is very simple: the route becomes a slash followed by the lowercased | |
# class name. See Controllers::D for the complete rules of dispatch. | |
# | |
# == Special classes | |
# | |
# There are two special classes used for handling 404 and 500 errors. The | |
# NotFound class handles URLs not found. The ServerError class handles exceptions | |
# uncaught by your application. | |
module Controllers | |
@r = [] | |
class << self | |
def r #:nodoc: | |
@r | |
end | |
# Add routes to a controller class by piling them into the R method. | |
# | |
# module Camping::Controllers | |
# class Edit < R '/edit/(\d+)', '/new' | |
# def get(id) | |
# if id # edit | |
# else # new | |
# end | |
# end | |
# end | |
# end | |
# | |
# You will need to use routes in either of these cases: | |
# | |
# * You want to assign multiple routes to a controller. | |
# * You want your controller to receive arguments. | |
# | |
# Most of the time the rules inferred by dispatch method Controllers::D will get you | |
# by just fine. | |
def R *u | |
r=@r | |
Class.new { | |
meta_def(:urls){u} | |
meta_def(:inherited){|x|r<< R() | |
def get(p) | |
r(404, Mab.new{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 | |
# by Camping. The controller class +k+ and request method +m+ (GET, POST, etc.) where the error | |
# took place are passed in, along with the Exception +e+ which can be mined for useful info. | |
# | |
# module Camping::Controllers | |
# class ServerError | |
# def get(k,m,e) | |
# @status = 500 | |
# div do | |
# h1 'Camping Problem!' | |
# h2 "in #{k}.#{m}" | |
# h3 "#{e.class} #{e.message}:" | |
# ul do | |
# e.backtrace.each do |bt| | |
# li bt | |
# end | |
# end | |
# end | |
# end | |
# end | |
# end | |
# | |
class ServerError < R() | |
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 | |
end | |
X = Controllers | |
class << self | |
# When you are running many applications, you may want to create independent | |
# modules for each Camping application. Namespaces for each. Camping::goes | |
# defines a toplevel constant with the whole MVC rack inside. | |
# | |
# require 'camping' | |
# Camping.goes :Blog | |
# | |
# module Blog::Controllers; ... end | |
# module Blog::Models; ... end | |
# module Blog::Views; ... end | |
# | |
def goes(m) | |
eval S.gsub(/Camping/,m.to_s).gsub("A\pps = []","Cam\ping::Apps< "I%27d+go+to+the+museum+straightway%21" | |
# | |
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(/%([\da-f]{2})/in){[$1].pack('H*')} end | |
# Parses a query string into an Camping::H object. | |
# | |
# input = Camping.qsp("name=Philarp+Tremain&hair=sandy+blonde") | |
# input.name | |
# #=> "Philarp Tremaine" | |
# | |
# Also parses out the Hash-like syntax used in PHP and Rails and builds | |
# nested hashes from it. | |
# | |
# input = Camping.qsp("post[id]=1&post[user]=_why") | |
# #=> {'post' => {'id' => '1', 'user' => '_why'}} | |
# | |
def qsp(qs, d='&;', y=nil, z=H[]) | |
m = proc {|_,o,n|o.u(n,&m)rescue([*o]<Cookie header. | |
def kp(s); c = qsp(s, ';,'); end | |
# Fields a request through Camping. For traditional CGI applications, the method can be | |
# executed without arguments. | |
# | |
# if __FILE__ == $0 | |
# Camping::Models::Base.establish_connection :adapter => 'sqlite3', | |
# :database => 'blog3.db' | |
# Camping::Models::Base.logger = Logger.new('camping.log') | |
# puts Camping.run | |
# end | |
# | |
# The Camping controller returned from run has a to_s method in case you | |
# are running from CGI or want to output the full HTTP output. In the above example, puts | |
# will call to_s for you. | |
# | |
# For FastCGI and Webrick-loaded applications, you will need to use a request loop, with run | |
# at the center, passing in the read +r+ and write +w+ streams. You will also need to mimick or | |
# pass in the ENV replacement as part of your wrapper. | |
# | |
# See Camping::FastCGI and Camping::WEBrick for examples. | |
# | |
def run(r=$stdin,e=ENV) | |
X.M | |
k,a=X.D un("/#{e['PATH_INFO']}".gsub(/\/+/,'/')) | |
k.new(r,e,(m=e['REQUEST_METHOD']||"GET")).Y.service *a | |
rescue Object=>x | |
X::ServerError.new(r,e,'get').service(k,m,x) | |
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) | |
# #=> # | |
# | |
# 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.get(:Info, :env => {:HTTP_HOST => 'wagon'}) | |
# #=> #'wagon'} ...> | |
# | |
def method_missing(m, c, *a) | |
X.M | |
k = X.const_get(c).new(StringIO.new, | |
H['HTTP_HOST','','SCRIPT_NAME','','HTTP_COOKIE',''],m.to_s) | |
H.new(a.pop).each { |e,f| k.send("#{e}=",f) } if Hash === a[-1] | |
k.service *a | |
end | |
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 to in Views at this time. | |
module Models | |
autoload :Base,'camping/db' | |
def Y;self;end | |
end | |
# Views is an empty module for storing methods which create HTML. The HTML is described | |
# using the Markaby language. | |
# | |
# == Using the layout method | |
# | |
# If your Views module has a layout method defined, it will be called with a block | |
# which will insert content from your view. | |
module Views; include Controllers, Helpers end | |
# The Mab class wraps Markaby, allowing it to run methods from Camping::Views | |
# and also to replace :href, :action and :src attributes in tags by prefixing the root | |
# path. | |
class Mab < Markaby::Builder | |
include Views | |
def tag!(*g,&b) | |
h=g[-1] | |
[:href,:action,:src].each{|a|(h[a]=self/h[a])rescue 0} | |
super | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment