Created
March 23, 2010 19:36
-
-
Save zuk/341555 to your computer and use it in GitHub Desktop.
Attempt at re-writing Reststop for Camping 2.0
This file contains hidden or 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
#!/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 |
This file contains hidden or 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
# 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