require 'rocketamf/values/typed_hash'
require 'rocketamf/values/array_collection'
require 'rocketamf/values/messages'
module RocketAMF
# Handles class name mapping between actionscript and ruby and assists in
# serializing and deserializing data between them. Simply map an AS class to a
# ruby class and when the object is (de)serialized it will end up as the
# appropriate class.
#
# Example:
#
# RocketAMF::ClassMapper.define do |m|
# m.map :as => 'AsClass', :ruby => 'RubyClass'
# m.map :as => 'vo.User', :ruby => 'Model::User'
# end
#
# == Object Population/Serialization
#
# In addition to handling class name mapping, it also provides helper methods
# for populating ruby objects from AMF and extracting properties from ruby objects
# for serialization. Support for hash-like objects and objects using
# attr_accessor for properties is currently built in, but custom classes
# may need custom support. As such, it is possible to create a custom populator
# or serializer.
#
# Populators are processed in insert order and must respond to the can_handle?
# and populate methods.
#
# class CustomPopulator
# def can_handle? obj
# true
# end
#
# def populate obj, props, dynamic_props
# obj.merge! props
# obj.merge!(dynamic_props) if dynamic_props
# end
# end
# RocketAMF::ClassMapper.object_populators << CustomPopulator.new
#
#
# Serializers are also processed in insert order and must respond to the
# can_handle? and serialize methods.
#
# class CustomSerializer
# def can_handle? obj
# true
# end
#
# def serialize obj
# {}
# end
# end
# RocketAMF::ClassMapper.object_serializers << CustomSerializer.new
#
# == Complete Replacement
#
# In some cases, it may be beneficial to replace the default provider of class
# mapping completely. In this case, simply assign an instance of your own class
# mapper to RocketAMF::ClassMapper after loading RocketAMF. Through
# the magic of const_missing, ClassMapper is only defined after
# the first access by default, so you get no annoying warning messages. Custom
# class mappers must implement the following methods: get_as_class_name,
# get_ruby_obj, populate_ruby_obj, props_for_serialization.
#
# Example:
#
# require 'rubygems'
# require 'rocketamf'
#
# RocketAMF::ClassMapper = MyCustomClassMapper.new
# # No warning about already initialized constant ClassMapper
# RocketAMF::ClassMapper.class # MyCustomClassMapper
class ClassMapping
# Container for all mapped classes
class MappingSet
def initialize #:nodoc:
@as_mappings = {}
@ruby_mappings = {}
# Map defaults
map :as => 'flex.messaging.messages.AbstractMessage', :ruby => 'RocketAMF::Values::AbstractMessage'
map :as => 'flex.messaging.messages.RemotingMessage', :ruby => 'RocketAMF::Values::RemotingMessage'
map :as => 'flex.messaging.messages.AsyncMessage', :ruby => 'RocketAMF::Values::AsyncMessage'
map :as => 'flex.messaging.messages.CommandMessage', :ruby => 'RocketAMF::Values::CommandMessage'
map :as => 'flex.messaging.messages.AcknowledgeMessage', :ruby => 'RocketAMF::Values::AcknowledgeMessage'
map :as => 'flex.messaging.messages.ErrorMessage', :ruby => 'RocketAMF::Values::ErrorMessage'
map :as => 'flex.messaging.io.ArrayCollection', :ruby => 'RocketAMF::Values::ArrayCollection'
end
# Map a given AS class to a ruby class.
#
# Use fully qualified names for both.
#
# Example:
#
# m.map :as 'com.example.Date', :ruby => 'Example::Date'
def map params
[:as, :ruby].each {|k| params[k] = params[k].to_s} # Convert params to strings
@as_mappings[params[:as]] = params[:ruby]
@ruby_mappings[params[:ruby]] = params[:as]
end
# Returns the AS class name for the given ruby class name, returing nil if
# not found
def get_as_class_name class_name #:nodoc:
@ruby_mappings[class_name.to_s]
end
# Returns the ruby class name for the given AS class name, returing nil if
# not found
def get_ruby_class_name class_name #:nodoc:
@as_mappings[class_name.to_s]
end
end
# Array of custom object populators.
attr_reader :object_populators
# Array of custom object serializers.
attr_reader :object_serializers
def initialize #:nodoc:
@object_populators = []
@object_serializers = []
end
# Define class mappings in the block. Block is passed a MappingSet object as
# the first parameter.
#
# Example:
#
# RocketAMF::ClassMapper.define do |m|
# m.map :as => 'AsClass', :ruby => 'RubyClass'
# end
def define #:yields: mapping_set
yield mappings
end
# Reset all class mappings except the defaults
def reset
@mappings = nil
end
# Returns the AS class name for the given ruby object. Will also take a string
# containing the ruby class name.
def get_as_class_name obj
# Get class name
if obj.is_a?(String)
ruby_class_name = obj
elsif obj.is_a?(Values::TypedHash)
ruby_class_name = obj.type
else
ruby_class_name = obj.class.name
end
# Get mapped AS class name
mappings.get_as_class_name ruby_class_name
end
# Instantiates a ruby object using the mapping configuration based on the
# source AS class name. If there is no mapping defined, it returns a
# RocketAMF::Values::TypedHash with the serialized class name.
def get_ruby_obj as_class_name
ruby_class_name = mappings.get_ruby_class_name as_class_name
if ruby_class_name.nil?
# Populate a simple hash, since no mapping
return Values::TypedHash.new(as_class_name)
else
ruby_class = ruby_class_name.split('::').inject(Kernel) {|scope, const_name| scope.const_get(const_name)}
return ruby_class.new
end
end
# Populates the ruby object using the given properties
def populate_ruby_obj obj, props, dynamic_props=nil
# Process custom populators
@object_populators.each do |p|
next unless p.can_handle?(obj)
p.populate obj, props, dynamic_props
return obj
end
# Fallback populator
props.merge! dynamic_props if dynamic_props
hash_like = obj.respond_to?("[]=")
props.each do |key, value|
if obj.respond_to?("#{key}=")
obj.send("#{key}=", value)
elsif hash_like
obj[key.to_sym] = value
end
end
obj
end
# Extracts all exportable properties from the given ruby object and returns
# them in a hash
def props_for_serialization ruby_obj
# Proccess custom serializers
@object_serializers.each do |s|
next unless s.can_handle?(ruby_obj)
return s.serialize(ruby_obj)
end
# Handle hashes
if ruby_obj.is_a?(Hash)
# Stringify keys to make it easier later on and allow sorting
h = {}
ruby_obj.each {|k,v| h[k.to_s] = v}
return h
end
# Fallback serializer
props = {}
@ignored_props ||= Object.new.public_methods
(ruby_obj.public_methods - @ignored_props).each do |method_name|
# Add them to the prop hash if they take no arguments
method_def = ruby_obj.method(method_name)
props[method_name.to_s] = ruby_obj.send(method_name) if method_def.arity == 0
end
props
end
private
def mappings
@mappings ||= MappingSet.new
end
end
end