Skip to content

Instantly share code, notes, and snippets.

@zuk
Created March 23, 2010 19:36
Show Gist options
  • Save zuk/341555 to your computer and use it in GitHub Desktop.
Save zuk/341555 to your computer and use it in GitHub Desktop.
Attempt at re-writing Reststop for Camping 2.0
#!/usr/bin/env ruby
require 'rubygems'
require 'ruby-debug'
#gem 'camping', '~> 2.0'
#gem 'reststop', '~> 0.3'
$: << '../../camping-camping/lib'
$: << '../lib'
require 'camping-unabridged'
require 'camping/ar'
require 'camping/session'
#begin
# try to use local copy of library
require '../lib/reststop2'
#rescue LoadError
# # ... otherwise default to rubygem
# require 'reststop'
#end
Camping.goes :Blog
module Blog
include Camping::Session
include Reststop
Controllers.extend Reststop::Controllers
end
module Blog::Base
alias camping_render render
alias camping_service service
include Reststop::Base
alias service reststop_service
alias render reststop_render
end
module Blog::Models
class Post < Base
belongs_to :user
before_save do |record|
cloth = RedCloth.new(record.body)
cloth.hard_breaks = false
record.html_body = cloth.to_html
end
end
class Comment < Base; belongs_to :user; end
class User < Base; end
class BasicFields < V 1.1
def self.up
create_table :blog_posts, :force => true do |t|
t.integer :user_id, :null => false
t.string :title, :limit => 255
t.text :body, :html_body
t.timestamps
end
create_table :blog_users, :force => true do |t|
t.string :username, :password
end
create_table :blog_comments, :force => true do |t|
t.integer :post_id, :null => false
t.string :username
t.text :body, :html_body
t.timestamps
end
User.create :username => 'admin', :password => 'camping'
end
def self.down
drop_table :blog_posts
drop_table :blog_users
drop_table :blog_comments
end
end
end
module Blog::Controllers
extend Reststop::Controllers
class Posts < REST 'posts'
# POST /posts
def create
require_login!
@post = Post.create :title => input.post_title, :body => input.post_body,
:user_id => @state.user_id
redirect R(@post)
end
# GET /posts/1
# GET /posts/1.xml
def read(post_id)
@post = Post.find(post_id)
@comments = Models::Comment.find(:all, :conditions => ['post_id = ?', post_id])
render :view
end
# PUT /posts/1
def update(post_id)
require_login!
@post = Post.find(post_id)
@post.update_attributes :title => input.post_title, :body => input.post_body
redirect R(@post)
end
# DELETE /posts/1
def delete(post_id)
require_login!
@post = Post.find post_id
if @post.destroy
redirect R(Posts)
else
_error("Unable to delete post #{@post.id}", 500)
end
end
# GET /posts
# GET /posts.xml
def list
@posts = Post.all(:order => 'updated_at DESC')
render :index
end
# GET /posts/new
def new
@state.user_id = 1
require_login!
@post = Post.new
render :add
end
# GET /posts/1/edit
def edit(post_id)
require_login!
@post = Post.find(post_id)
render :edit
end
end
class Comments < REST 'comments'
# POST /comments
def create
Models::Comment.create(:username => input.post_username,
:body => input.post_body, :post_id => input.post_id)
redirect R(Posts, input.post_id)
end
end
class Sessions < REST 'sessions'
# POST /sessions
def create
@user = User.find_by_username_and_password(input.username, input.password)
if @user
@state.user_id = @user.id
redirect R(Posts)
else
@info = 'Wrong username or password.'
end
render :login
end
# DELETE /sessions
def delete
@state.user_id = nil
redirect Index
end
end
# You can use old-fashioned Camping controllers too!
class Style < R '/styles.css'
def get
@headers["Content-Type"] = "text/css; charset=utf-8"
@body = %{
body {
font-family: Utopia, Georga, serif;
}
h1.header {
background-color: #fef;
margin: 0; padding: 10px;
}
div.content {
padding: 10px;
}
}
end
end
end
module Blog::Helpers
alias_method :_R, :R
remove_method :R
include Reststop::Helpers
def logged_in?
!!@state.user_id
end
def require_login!
unless logged_in?
redirect Controllers::Login
throw :halt
end
end
end
module Blog::Views
extend Reststop::Views
module HTML
include Blog::Controllers
include Blog::Views
def layout
html do
head do
title 'blog'
link :rel => 'stylesheet', :type => 'text/css',
:href => self/'/styles.css', :media => 'screen'
end
body do
h1.header { a 'blog', :href => R(Posts) }
div.content do
self << yield
end
end
end
end
def index
if @posts.empty?
p 'No posts found.'
else
for post in @posts
_post(post)
end
end
p { a 'Add', :href => R(Posts, 'new') }
end
def login
p { b @login }
p { a 'Continue', :href => R(Posts, 'new') }
end
def logout
p "You have been logged out."
p { a 'Continue', :href => R(Posts) }
end
def add
if @user
_form(@post, :action => R(Posts))
else
_login
end
end
def edit
if @user
_form(@post, :action => R(@post), :method => :put)
else
_login
end
end
def view
_post(@post)
p "Comment for this post:"
for c in @comments
h1 c.username
p c.body
end
form :action => R(Comments), :method => 'post' do
label 'Name', :for => 'post_username'; br
input :name => 'post_username', :type => 'text'; br
label 'Comment', :for => 'post_body'; br
textarea :name => 'post_body' do; end; br
input :type => 'hidden', :name => 'post_id', :value => @post.id
input :type => 'submit'
end
end
# partials
def _login
form :action => R(Sessions), :method => 'post' do
label 'Username', :for => 'username'; br
input :name => 'username', :type => 'text'; br
label 'Password', :for => 'password'; br
input :name => 'password', :type => 'text'; br
input :type => 'submit', :name => 'login', :value => 'Login'
end
end
def _post(post)
h1 post.title
p post.body
p do
[a("Edit", :href => R(Posts, post.id, 'edit')), a("View", :href => R(Posts, post.id, 'edit'))].join " | "
end
end
def _form(post, opts)
form(:action => R(Sessions), :method => 'delete') do
p do
span "You are logged in as #{@user.username}"
span " | "
button(:type => 'submit') {'Logout'}
end
end
form({:method => 'post'}.merge(opts)) do
label 'Title', :for => 'post_title'; br
input :name => 'post_title', :type => 'text',
:value => post.title; br
label 'Body', :for => 'post_body'; br
textarea post.body, :name => 'post_body'; br
input :type => 'hidden', :name => 'post_id', :value => post.id
input :type => 'submit'
end
end
end
default_format :HTML
module XML
def layout
yield
end
def index
@posts.to_xml(:root => 'blog')
end
def view
@post.to_xml(:root => 'post')
end
end
end
def Blog.create
Blog::Models.create_schema :assume => (Blog::Models::Post.table_exists? ? 1.0 : 0.0)
end
# Unfinished attempt at re-writing Reststop for Camping 2.0
# Oogly, oogly, oogly.
#
# Might be easier to just fork Camping, implement the restful stuff, and call it Resting :)
#
# I think all of the routing is taken care of, but there's something weird going on with #reststop_render
# Rack complains about invalid output (or something).
#
# Right now you'll have to do some weird gymnastics to get this hooked in to a Camping app...
# Something like:
#
# Camping.goes :Blog
#
# module Blog
# include Reststop
# end
#
# module Blog::Base
# alias camping_render render
# alias camping_service service
# include Reststop::Base
# alias service reststop_service
# alias render reststop_render
# end
#
# module Blog::Controllers
# extend Reststop::Controllers
# ...
# end
#
# module Blog::Helpers
# alias_method :_R, :R
# remove_method :R
# include Reststop::Helpers
# ...
# end
#
# module Blog::Views
# extend Reststop::Views
# ...
# end
#
# The hope is that this could all get taken care of in a
# `include Reststop` call (via overriding of #extended)
$LOG = Logger.new(STDOUT)
module Reststop
module Base
def reststop_service(*a)
if @env['REQUEST_METHOD'] == 'POST' && (input['_method'] == 'put' || input['_method'] == 'delete')
@env['REQUEST_METHOD'] = input._method.upcase
@method = input._method
end
camping_service(*a)
end
# Overrides Camping's render method to add the ability to specify a format
# module when rendering a view.
#
# The format can also be specified in other ways (shown in this order
# of precedence):
#
# 1. By providing a second parameter to render()
# (eg: <tt>render(:foo, :HTML)</tt>)
# 2. By setting the @format variable
# 3. By providing a 'format' parameter in the request (i.e. input[:format])
# 4. By adding a file-format extension to the url (e.g. /items.xml or
# /items/2.html).
#
# For example, you could have:
#
# module Foobar::Views
#
# module HTML
# def foo
# # ... render some HTML content
# end
# end
#
# module RSS
# def foo
# # ... render some RSS content
# end
# end
#
# end
#
# Then in your controller, you would call render() like this:
#
# render(:foo, :HTML) # render the HTML version of foo
#
# or
#
# render(:foo, :RSS) # render the RSS version of foo
#
# or
#
# @format = :RSS
# render(:foo) # render the RSS version of foo
#
# or
#
# # url is /foobar/1?format=RSS
# render(:foo) # render the RSS version of foo
#
# or
#
# # url is /foobar/1.rss
# render(:foo) # render the RSS version of foo
#
# If no format is specified, render() will behave like it normally does in
# Camping, by looking for a matching view method directly
# in the Views module.
#
# You can also specify a default format module by calling
# <tt>default_format</tt> after the format module definition.
# For example:
#
# module Foobar::Views
# module HTML
# # ... etc.
# end
# default_format :HTML
# end
#
def reststop_render(action, format = nil)
format ||= @format
if format.nil?
begin
ct = CONTENT_TYPE
rescue NameError
ct = 'text/html'
end
@headers['Content-Type'] ||= ct
camping_render(action)
else
m = Mab.new({}, self)
mod = "Camping::Views::#{format.to_s}".constantize
m.extend mod
begin
ct = mod::CONTENT_TYPE
rescue NameError
ct = "text/#{format.to_s.downcase}"
end
@headers['Content-Type'] = ct
s = m.capture{m.send(action)}
s = m.capture{send(:layout){s}} if /^_/!~a[0].to_s and m.respond_to?(:layout)
s
end
end
end
module Views
# Call this inside your Views module to set a default format.
#
# For example:
#
# module Foobar::Views
# module HTML
# # ... etc.
# end
# default_format :XML
# end
def default_format(m)
mod = "#{self}::#{m.to_s}".constantize
mab = self.to_s.gsub('::Views','').constantize
mab.class_eval{include mod}
end
end
module Helpers
# Overrides Camping's routing helper to make it possible to route RESTful resources.
#
# Some usage examples:
#
# R(Kittens) # /kittens
# R(Kittens, 'new') # /kittens/new
# R(Kittens, 1, 'meow') # /kittens/1/meow
# R(@kitten) # /kittens/1
# R(@kitten, 'meow') # /kittens/1/meow
# R(Kittens, 'list', :colour => 'black') # /kittens/list?colour=black
#
# The current output format is retained, so if the current <tt>@format</tt> is <tt>:XML</tt>,
# the URL will be /kittens/1.xml rather than /kittens/1.
#
# Note that your controller names might not be loaded if you're calling <tt>R</tt> inside a
# view module. In that case you should use the fully qualified name (i.e. Myapp::Controllers::Kittens)
# or include the Controllers module into your view module.
def R(c, *g)
if Controllers.constants.include?(cl = c.class.name.split("::").last.pluralize)
path = "/#{cl.underscore}/#{c.id}"
path << ".#{@format.to_s.downcase}" if @format
path << "/#{g.shift}" unless g.empty?
self / path
elsif c.respond_to?(:restful?) && c.restful?
base = c.name.split("::").last.underscore
id_or_action = g.shift
if id_or_action =~ /\d+/
id = id_or_action
action = g.shift
else
action = id_or_action
end
path = "/#{base}"
path << "/#{id}" if id
path << "/#{action}" if action
path << ".#{@format.to_s.downcase}" if @format
path << "?#{g.collect{|a|a.collect{|k,v| U.escape(k)+"="+U.escape(v)}.join("&")}.join("&")}" unless g.empty? # FIXME: undefined behaviour if there are multiple arguments left
return path
else
_R(c, *g)
end
end # def R
end # module Helpers
module Controllers
def self.determine_format(input, env) #:nodoc:
if input[:format] && !input[:format].empty?
input[:format].upcase.intern
elsif env['PATH_INFO'] =~ /\.([a-z]+)$/
$~[1].upcase.intern
end
end
# Calling <tt>REST "<resource name>"</tt> creates a controller with the
# appropriate routes and maps your REST methods to standard
# Camping controller mehods. This is meant to be used in your Controllers
# module in place of <tt>R <routes></tt>.
#
# Your REST class should define the following methods:
#
# * create
# * read(id)
# * update(id)
# * destroy(id)
# * list
#
# Routes will be automatically created based on the resource name fed to the
# REST method. <b>Your class must have the same (but CamelCaps'ed)
# name as the resource name.</b> So if your resource name is 'kittens',
# your controller class must be Kittens.
#
# For example:
#
# module Foobar::Controllers
# class Kittens < REST 'kittens'
# # POST /kittens
# def create
# end
#
# # GET /kittens/(\d+)
# def read(id)
# end
#
# # PUT /kittens/(\d+)
# def update(id)
# end
#
# # DELETE /kittens/(\d+)
# def destroy(id)
# end
#
# # GET /kittens
# def list
# end
# end
# end
#
# Custom actions are also possible. For example, to implement a 'meow'
# action simply add a 'meow' method to the above controller:
#
# # POST/GET/PUT/DELETE /kittens/meow
# # POST/GET/PUT/DELETE /kittens/(\d+)/meow
# def meow(id)
# end
#
# Note that a custom action will respond to all four HTTP methods
# (POST/GET/PUT/DELETE).
#
# Optionally, you can specify a <tt>:prefix</tt> key that will prepend the
# given string to the routes. For example, the following will create all
# of the above routes, prefixed with "/pets"
# (i.e. <tt>POST '/pets/kittens'</tt>, <tt>GET '/pets/kittens/(\d+)'</tt>,
# etc.):
#
# module Foobar::Controllers
# class Items < REST 'kittens', :prefix => '/pets'
# # ...
# end
# end
#
# Format-based routing similar to that in ActiveResource is also implemented.
# For example, to get a list of kittens in XML format, place a
# <tt>GET</tt> call to <tt>/kittens.xml</tt>.
# See the documentation for the render() method for more info.
#
def REST(r, options = {})
crud = R "#{options[:prefix]}/#{r}/([0-9a-zA-Z]+)/([a-z_]+)(?:\.[a-z]+)?",
"#{options[:prefix]}/#{r}/([0-9a-zA-Z]+)(?:\.[a-z]+)?",
"#{options[:prefix]}/#{r}/([a-z_]+)(?:\.[a-z]+)?",
"#{options[:prefix]}/#{r}(?:\.[a-z]+)?"
crud.module_eval do
meta_def(:restful?){true}
$LOG.debug("Creating RESTful controller for #{r.inspect} using Reststop #{'pull version number here'}") if $LOG
def get(id_or_custom_action = nil, custom_action = nil) # :nodoc:
id = input['id'] if input['id']
custom_action = input['action'] if input['action']
if self.methods.include? id_or_custom_action
custom_action ||= id_or_custom_action
id ||= nil
else
id ||= id_or_custom_action
end
id = id.to_i if id && id =~ /^[0-9]+$/
@format = Reststop::Controllers.determine_format(input, @env)
begin
if id.nil? && input['id'].nil?
custom_action ? send(custom_action) : list
else
custom_action ? send(custom_action, id || input['id']) : read(id || input['id'])
end
rescue NoMethodError => e
# FIXME: this is probably not a good way to do this, but we need to somehow differentiate
# between 'no such route' vs. other NoMethodErrors
if e.message =~ /no such method/
return no_method(e)
else
raise e
end
rescue ActiveRecord::RecordNotFound => e
return not_found(e)
end
end
def post(custom_action = nil) # :nodoc:
@format = Reststop::Controllers.determine_format(input, @env)
custom_action ? send(custom_action) : create
end
def put(id, custom_action = nil) # :nodoc:
id = id.to_i if id =~ /^[0-9]+$/
@format = Reststop::Controllers.determine_format(input, @env)
custom_action ? send(custom_action, id || input['id']) : update(id || input['id'])
end
def delete(id, custom_action = nil) # :nodoc:
id = id.to_i if id =~ /^[0-9]+$/
@format = Reststop::Controllers.determine_format(input, @env)
custom_action ? send(custom_action, id || input['id']) : destroy(id || input['id'])
end
private
def _error(message, status_code = 500, e = nil)
@status = status_code
@message = message
begin
render "error_#{status_code}".intern
rescue NoMethodError
if @format.to_s == 'XML'
"<error code='#{status_code}'>#{@message}</error>"
else
out = "<strong>#{@message}</strong>"
out += "<pre style='color: #bbb'><strong>#{e.class}: #{e}</strong>\n#{e.backtrace.join("\n")}</pre>" if e
out
end
end
end
def no_method(e)
_error("No controller method responds to this route!", 501, e)
end
def not_found(e)
_error("Record not found!", 404, e)
end
end
crud
end # def REST
end # module Controllers
end # module Reststop
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment