class BackendAPI
VERSION = [0,3,5]
WRAP = <<-EOT
%s
%s
EOT
# Automatically use MethodOverride before
# Thx Konstantin Haase for the trick
def self.new(*); ::Rack::MethodOverride.new(super); end
def initialize(app=nil); @app = app; end
def call(env); dup.call!(env); end
def call!(env)
@req = ::Rack::Request.new(env)
@res = ::Rack::Response.new
@res.headers['Content-Type'] = 'text/html'
# Simple dispatcher
@model_name, @id, *a = @req.path_info.split('/').find_all{|s| s!=''}
# Special case
return @res.finish{@res.write(v)} if @model_name=='_version'
build_model_vars
__send__(@req.request_method.downcase) unless @res.status==404
@res.status==404&&!@app.nil? ? @app.call(env) : @res.finish
end
private
# =========
# = Paths =
# =========
def v; VERSION.join('.'); end
# Create
def post
return put unless @id.nil?
@model_instance = @model_class.backend_post(@req['model'])
save_and_respond
end
# Read
def get
@model_instance ||= @model_class.backend_post
@model_instance.backend_put @req['model']
form = @model_instance.backend_form(@req.path, @req['fields'], :destination => @req['_destination'], :submit_text => @req['_submit_text'], :no_wrap => @req['_no_wrap'])
@res.write(wrap_response(form))
end
# Update
def put
if @id.nil? && @req[@model_class_name]
@model_class.sort(@req[@model_class_name])
else
@model_instance.backend_put @req['model']
save_and_respond
end
end
# Delete
def delete
@model_instance.backend_delete
@req['_destination'].nil? ? @res.status=204 : @res.redirect(::Rack::Utils::unescape(@req['_destination'])) # 204 No Content
end
# Cost less than checking if is not GET, POST, PUT or DELETE
def head; get; end; def options; get; end; def patch; get; end; def trace; get; end
# ===========
# = Helpers =
# ===========
def build_model_vars
@model_class_name = camel_case(@model_name)
if !@model_name.nil? && ::Object.const_defined?(@model_class_name)
@model_class = Kernel.const_get(@model_class_name)
@model_instance = @model_class.backend_get(@id) unless @id.nil?
@clone_instance = @model_class.backend_get(@req['clone_id']) unless @req['clone_id'].nil?
unless @clone_instance.nil?
@req['fields'] ||= @clone_instance.cloning_backend_columns.map{|k|k.to_s}
@req['model'] = @clone_instance.backend_values.reject{|k,v| !@req['fields'].include?(k.to_s)}
end
@req['model'] ||= {}
send_404 if @model_instance.nil?&&!@id.nil?
else
send_404
end
end
def send_404
@res.status=404 # Not Found
@res.headers['X-Cascade']='pass'
@res.write 'Not Found'
end
def camel_case(s)
return if s.nil?
c = RUBY_VERSION>='1.9.0' ? s[0].ord : s[0]
c>=65&&c<=90 ? s : s.split('_').map{|e|e.capitalize}.join
end
def save_and_respond
if @model_instance.backend_save?
if @req['_destination'].nil?
@res.write(wrap_response(@model_instance.backend_show))
@res.status=201 # Created
else
@res.redirect(::Rack::Utils::unescape(@req['_destination']))
end
else
form = @model_instance.backend_form(@req.path, @req['fields']||@req['model'].keys, :destination => @req['_destination'], :submit_text => @req['_submit_text'], :no_wrap => @req['_no_wrap'])
@res.write(wrap_response(form))
# Bad Request
# I used 400 code before but it does not work through Ajax anymore
# At least on Safari
# Content is not passed with this code
@res.status=200
end
end
def wrap_response(content)
if @req['_no_wrap'] || @req.xhr?
content
else
WRAP % [@model_class_name, content]
end
end
end
# Require Adapter when known ORM detected
::Sequel::Model.plugin :rack_backend_api_adapter if defined? Sequel