# RioOnRails
require 'digest/sha1'
require 'json/add/rails'
module Rio
module Helpers
def cache_key
@cache_key ||= ENV['cache_rio'] ? File.new(RAILS_ROOT).mtime.to_i : rand
end
def application_stylesheet
url = boot_compressed? ? "compressed/#{@rio_app}.css" : "#{@rio_app}_concat.css"
<<-CSS
CSS
end
def asset_prefix
# meant to be overriden
""
end
def after_rio(html)
@after_rio = "#{@after_rio || ""}\n#{html}"
end
def boot_compressed?
rio_compression == "compressed"
end
def rio_compression
ENV['rio_compression'] || ((RAILS_ENV == "development" and params.has_key?(:compressed)) ? "compressed" : "uncompressed")
end
def rio_environment
ENV['rio_environment'] || RAILS_ENV
end
def rio(opts = {})
if @rio_app
rio_html = <<-HTML
HTML
if (boot_compressed?)
<<-SCRIPTS
#{rio_html}
#{javascript_include_tag "#{asset_prefix}/javascripts/lib/compressed/boot.js?#{@rio_app},#{rio_environment},#{rio_compression},#{cache_key}"}
#{@after_rio || ""}
SCRIPTS
else
<<-SCRIPTS
#{rio_html}
#{javascript_include_tag "prototype/prototype.js"}
#{javascript_include_tag "lib/environment.js"}
#{javascript_include_tag "lib/boot.js?#{@rio_app},#{rio_environment},#{rio_compression},#{cache_key}"}
#{@after_rio || ""}
SCRIPTS
end
end
end
end
module ActiveRecordExtensions
def self.included(base)
base.extend ClassMethods
base.send(:include, Rio::ActiveRecordExtensions::Methods)
end
module ClassMethods
def rio_json(options = {})
self.send(:cattr_accessor, :rio_json_includes)
self.rio_json_includes = options[:include]
self.send(:include, Rio::RioModel)
end
def broadcast(mode)
self.send(:cattr_accessor, :broadcast_mode)
self.broadcast_mode = mode
end
def broadcast_mode
:normal
end
def broadcast?
self.broadcast_mode != :none
end
def validate_transaction!(transaction, objects)
true
end
end
module Methods
def broadcast_to
[]
end
end
end
module RioModel
# cattr_accessor :rio_json_includes
def has_json_options?
self.class.respond_to?("json_options")
end
def json_options
has_json_options? ? self.class.json_options : {}
end
def json_hash
result = ActiveRecord::Serialization::Serializer.new(self, json_options).serializable_record
# result = Hash.new
# self.class.columns.each do |column|
# # unless column.name == "created_at" || column.name == "updated_at"
# result[column.name.to_sym] = self.send(column.name)
# # end
# end
# if(has_json_options)
# (self.class.json_options[:methods] || []).each do |method|
# result[method.to_sym] = self.send(method)
# end
# end
ActiveRecord::Base.include_root_in_json ? { self.class.name.underscore => result } : result
end
def include_json
self.class.rio_json_includes.map do |inc|
{
:className => inc[:class_name],
:parameters => inc[:params].keys.inject({}) do |acc, k|
val = inc[:params][k]
acc[k] = val.class == Proc ? val.call(self) : val
acc
end,
:json => self.send(inc[:name])
}
end
end
def to_json(*args)
if self.class.rio_json_includes.nil? or self.class.rio_json_includes.empty?
json_hash.to_json
else
{
:_set => {
:self => json_hash,
:include => include_json
}
}.to_json
end
end
end
module ActionControllerExtensions
def rio_resource(options = {})
self.send(:cattr_accessor, :rio_actions)
self.rio_actions = options[:only] || [:index, :show, :create, :update, :destroy]
self.send(:include, Rio::RioResource)
end
end
module ApplicationControllerExtensions
def self.included(base)
base.send(:include, Rio::ApplicationControllerExtensions::Methods)
base.send(:helper_method, :current_user_id)
base.send(:before_filter, :reset_rio_cache)
end
module Methods
def current_user_id
nil
end
def reset_rio_cache
$rio_cache = {
:current_user_id => current_user_id
}
end
end
end
class Push
def self.push_class
(ENV["rio_push_class"] || "Juggernaut").constantize
end
def self.show_connected_clients
self.push_class.show_connected_clients
end
def self.show_clients_for_channel(channel)
self.push_class.show_clients_for_channel(channel)
end
def self.send_to_channel(data, channel)
self.push_class.send_to_channel(data, channel)
end
def self.send_to_clients(data, clients)
self.push_class.send_to_clients(data, clients)
end
end
module RioResource
def index
raise "Index operation not supported" unless self.class.rio_actions.include?(:index)
parsed_index_conditions = index_conditions
raise "Index operation not authorized" unless self.index_authorized?(parsed_index_conditions)
render :json => scope.find(:all, :conditions => parsed_index_conditions)
end
def index_conditions
ActiveSupport::JSON.decode(params[:conditions])
end
def show
raise "Show operation not supported" unless self.class.rio_actions.include?(:show)
raise "Show operation not authorized" unless self.show_authorized?(params[:id])
render :json => scope.find(params[:id])
end
def create
raise "Create operation not supported" unless self.class.rio_actions.include?(:create)
if (params["transaction"])
execute_transaction(params["transaction"], params["transaction_key"])
else
raise "Create operation not authorized" unless self.create_authorized?(params[model_param_name])
obj = create_scope.new(params[model_param_name])
valid = true
model.transaction do
if (obj.save)
objects = {}
objects[obj.id] = obj
unless (model.validate_transaction!({}, objects))
valid = false
raise ActiveRecord::Rollback
end
else
valid = false
raise ActiveRecord::Rollback
end
end
if (valid)
remote_create(obj, params["transaction_key"])
render :json => obj
else
render :json => obj.errors, :status => 500
end
end
end
def update
raise "Update operation not supported" unless self.class.rio_actions.include?(:update)
paranoid = scope.respond_to?(:find_with_deleted)
if (paranoid)
obj = scope.find_with_deleted(params[:id])
else
obj = scope.find(params[:id])
end
needs_recover = (paranoid and obj.deleted?)
valid = true
changed_fields = []
model.transaction do
if (needs_recover)
raise "Update operation not authorized" unless self.update_authorized?(obj, ["deleted_at"])
obj.recover!
end
obj.attributes = params[model_param_name]
changed_fields = obj.changed
raise "Update operation not authorized" unless self.update_authorized?(obj, changed_fields)
if (obj.save)
objects = {}
objects[obj.id] = obj
unless (model.validate_transaction!({}, objects))
valid = false
raise ActiveRecord::Rollback
end
else
valid = false
raise ActiveRecord::Rollback
end
end
if (valid)
if (needs_recover)
remote_create(obj, params["transaction_key"])
else
remote_update(obj, changed_fields, params["transaction_key"])
end
render :json => obj
else
render :json => obj.errors, :status => 500
end
end
def destroy
raise "Destroy operation not supported" unless self.class.rio_actions.include?(:destroy)
obj = scope.find(params[:id])
raise "Destroy operation not authorized" unless self.destroy_authorized?(obj)
valid = true
model.transaction do
if (obj.destroy)
objects = {}
objects[obj.id] = obj
unless (model.validate_transaction!({}, objects))
valid = false
raise ActiveRecord::Rollback
end
else
valid = false
raise ActiveRecord::Rollback
end
end
if (valid)
remote_destroy(obj, params["transaction_key"])
render :json => { :id => params[:id] }
else
render :json => obj.errors, :status => 500
end
end
protected
def scope
model
end
def create_scope
scope
end
def model
model_class_name = controller_class_name.match(/(.*)Controller$/)[1].singularize
model_class_name.constantize
end
def index_authorized?(params)
true
end
def show_authorized?(id)
true
end
def create_authorized?(params)
true
end
def update_authorized?(instance, attributes)
true
end
def destroy_authorized?(instance)
true
end
private
def execute_transaction(transaction, transaction_key)
valid = true
objects = {}
remote_transaction_data = []
# If any of these items aren't found the transaction is invalid and will fail.
# By verifying this here we can take for granted that some of these might be destroyed
# as part of an association or before/after filter and not be available when needed to be destroyed.
to_find = transaction.keys.select {|t| t.to_i > 0}
paranoid = scope.respond_to?(:find_with_deleted)
if (paranoid)
existing_entities = scope.find_with_deleted(to_find)
else
existing_entities = scope.find(to_find)
end
existing_entities = [existing_entities].flatten
model.transaction do
new_ids, existing_ids = transaction.keys.partition { |id| id.to_i < 0 }
transaction.keys.each do |id|
obj = existing_entities.detect { |e| e.id == id.to_i }
if (id.to_i < 0)
raise "Create operation not authorized" unless self.create_authorized?(transaction[id])
obj = create_scope.create(transaction[id])
remote_transaction_data.push({ :action => "create", :name => model.to_s, :id => obj.id, :json => obj })
elsif (paranoid and (not obj.nil?) and obj.deleted?)
obj.recover!
obj.attributes = transaction[id]
raise "Update operation not authorized" unless self.update_authorized?(obj, obj.changed)
obj.save
remote_transaction_data.push({ :action => "create", :name => model.to_s, :id => obj.id, :json => obj })
elsif (transaction[id] == "delete")
raise "Destroy operation not authorized" unless self.destroy_authorized?(obj)
obj.destroy
remote_transaction_data.push({ :action => "destroy", :name => model.to_s, :id => obj.id, :json => obj })
else
obj.attributes = transaction[id]
raise "Update operation not authorized" unless self.update_authorized?(obj, obj.changed)
json_fields = obj.changed
obj.save
remote_transaction_data.push({ :action => "update", :name => model.to_s, :id => obj.id, :obj => obj, :json_fields => json_fields })
end
if (!obj.valid?)
valid = false
end
objects[id] = obj
end
transaction.keys.each do |id|
if (transaction[id] != "delete")
(transaction[id].keys - ["id"]).each do |k|
if (new_ids.include?(transaction[id][k].to_s))
objects[id].update_attribute(k, objects[transaction[id][k].to_s].id)
end
end
end
end
unless (model.validate_transaction!(transaction, objects))
valid = false
raise ActiveRecord::Rollback
end
raise ActiveRecord::Rollback unless valid
end
if (valid)
remote_transaction(remote_transaction_data, transaction_key)
render :json => {
:transaction => objects.keys.inject({}) do |acc, id|
acc[id] = objects[id]
acc
end
}
else
render :json => {
:transaction => "FAILED"
}, :status => 500
end
end
def remote_create(obj, transaction_key)
if (Rio::Push and model.broadcast?)
begin
Rio::Push.send_to_clients("""
rio.Model.remoteCreate({
transactionKey: \"#{transaction_key}\",
name: \"#{ model.to_s }\",
id: \"#{ obj.id }\",
json: #{ obj.to_json }
});
""", other_client_ids(obj))
rescue
RAILS_DEFAULT_LOGGER.error("\n Failed to send a remote create \n")
end
end
end
def remote_update(obj, changed_fields, transaction_key)
if (Rio::Push and model.broadcast?)
begin
json = changed_fields.inject({}) do |acc, f|
acc[f] = obj.send(f)
acc
end
if (ActiveRecord::Base.include_root_in_json)
json = { "#{model.class_name.underscore}" => json }
end
Rio::Push.send_to_clients("""
rio.Model.remoteUpdate({
transactionKey: \"#{transaction_key}\",
name: \"#{ model.to_s }\",
id: \"#{ obj.id }\",
json: #{ json.to_json }
});
""", other_client_ids(obj))
rescue
RAILS_DEFAULT_LOGGER.error("\n Failed to send a remote update \n")
end
end
end
def remote_destroy(obj, transaction_key)
if(Rio::Push and model.broadcast?)
begin
Rio::Push.send_to_clients("""
rio.Model.remoteDestroy({
transactionKey: \"#{transaction_key}\",
name: \"#{ model.to_s }\",
id: \"#{ obj.id }\"
});
""", other_client_ids(obj))
rescue
RAILS_DEFAULT_LOGGER.error("\n Failed to send a remote destroy \n")
end
end
end
def remote_transaction(transaction, transaction_key)
if(Rio::Push and model.broadcast?)
begin
other_client_list = transaction.map {|t| other_client_ids(t[:obj] || t[:json]) }.inject { |acc, ids| acc & ids }
transaction.each do |t|
if (t[:obj])
obj = t.delete(:obj)
fields = t.delete(:json_fields)
t[:json] = fields.inject({}) do |acc, f|
acc[f] = obj.send(f)
acc
end
if (ActiveRecord::Base.include_root_in_json)
t[:json] = { "#{model.class_name.underscore}" => t[:json] }
end
end
end
Rio::Push.send_to_clients("""
rio.Model.remoteTransaction({
transactionKey: \"#{transaction_key}\",
transaction: #{transaction.to_json}
});
""", other_client_list)
rescue => e
RAILS_DEFAULT_LOGGER.error("\n Failed to send a remote transaction \n")
RAILS_DEFAULT_LOGGER.error(e)
end
end
end
def other_client_ids(instance)
if(model.broadcast_mode == :strict)
instance.broadcast_to
else
Rio::Push.show_connected_clients.map { |c| c["id"] || c["client_id"] }
end
end
def model_param_name
model.class_name.underscore
end
end
end