require 'resourceful/builder'
module Resourceful
# This module contains mixin modules
# used to implement the object serialization
# used for the Builder#publish method.
# They can also be used to get serialized representations of objects
# in other contexts.
#
# Serialization makes use of duck typing.
# Each class that can be serialized
# (just Array and ActiveRecord::Base by default)
# implements the +serialize+ and +to_serializable+ methods.
# These methods are implemented differently by the different classes,
# but the semantics of the implementations are consistent,
# so they can be used consistently.
#
# +to_serializable+ returns an object that can be directly serialized
# with a call to +to_xml+, +to_yaml+, or +to_json+.
# This object is either a hash or an array,
# and all the elements are either values, like strings and integers,
# or other serializable objects.
# This is useful for getting a model into a simple data structure format.
# The +attributes+ argument uses the same semantics
# as the :attributes option for Builder#publish.
# For example:
#
# c = Cake.new(:flavor => 'chocolate', :text => 'Happy birthday, Chris!')
# c.recipient = User.new(:name => 'Chris', :password => 'not very secure')
# c.to_serializable [
# :flavor, :text,
# :recipient => :name
# ]
#
# This would return the Ruby hash
#
# { :flavor => 'chocolate', :text => 'Happy birthday, Chris!',
# :user => {:name => 'Chris'} }
#
# +serialize+ takes a format (:xml, :yaml, or :json - see New Formats below)
# and a hash of options.
# The only option currently recognized is :attributes,
# which has the same semantics
# as the :attributes option for Builder#publish.
# +serialize+ returns a string containing the target
# serialized in the given format.
# For example:
#
# c = CandyBag.new(:title => 'jellybag')
# c.candies << Candy.new(:type => 'jellybean', :flavor => 'root beer')
# c.candies << Candy.new(:type => 'jellybean', :flavor => 'pear')
# c.candies << Candy.new(:type => 'licorice', :flavor => 'anisey')
# c.serialize :xml, :attributes => [:title, {:candies => [:type, :flavor]}]
#
# This would return a Ruby string containing
#
#
#
# jellybag
#
#
# jellybean
# root beer
#
#
# jellybean
# pear
#
#
# licorice
# anisey
#
#
#
#
module Serialize
# Takes an attributes option in the form passed to Builder#publish
# and returns a hash (or nil, if attributes is nil)
# containing the same data,
# but in a more consistent format.
# All keys are converted to symbols,
# and all lists are converted to hashes.
# For example:
#
# Resourceful::Serialize.normalize_attributes([:foo, :bar, {"baz" => ["boom"]}])
# #=> {"baz"=>["boom"], :foo=>nil, :bar=>nil}
#
def self.normalize_attributes(attributes) # :nodoc:
return nil if attributes.nil?
return {attributes.to_sym => nil} if String === attributes
return {attributes => nil} if !attributes.respond_to?(:inject)
attributes.inject({}) do |hash, attr|
if Array === attr
hash[attr[0]] = attr[1]
hash
else
hash.merge normalize_attributes(attr)
end
end
end
# This module contains the definitions of +serialize+ and +to_serializable+
# that are included in ActiveRecord::Base.
module Model
# :call-seq:
# serialize format, options = {}, :attributes => [ ... ]
#
# See the module documentation for Serialize for details.
def serialize(format, options)
raise "Must specify :attributes option" unless options[:attributes]
hash = self.to_serializable(options[:attributes])
root = self.class.to_s.underscore
if format == :xml
hash.send("to_#{format}", :root => root)
else
{root => hash}.send("to_#{format}")
end
end
# See the module documentation for Serialize for details.
def to_serializable(attributes)
raise "Must specify attributes for #{self.inspect}.to_serializable" if attributes.nil?
Serialize.normalize_attributes(attributes).inject({}) do |hash, (key, value)|
hash[key.to_s] = attr_hash_value(self.send(key), value)
hash
end
end
private
# Given an attribute value
# and a normalized (see above) attribute hash,
# returns the serializable form of that attribute.
def attr_hash_value(attr, sub_attributes)
if attr.respond_to?(:to_serializable)
attr.to_serializable(sub_attributes)
else
attr
end
end
end
# This module contains the definitions of +serialize+ and +to_serializable+
# that are included in ActiveRecord::Base.
module Array
# :call-seq:
# serialize format, options = {}, :attributes => [ ... ]
#
# See the module documentation for Serialize for details.
def serialize(format, options)
raise "Not all elements respond to to_serializable" unless all? { |e| e.respond_to? :to_serializable }
raise "Must specify :attributes option" unless options[:attributes]
serialized = map { |e| e.to_serializable(options[:attributes]) }
root = first.class.to_s.pluralize.underscore
if format == :xml
serialized.send("to_#{format}", :root => root)
else
{root => serialized}.send("to_#{format}")
end
end
# See the module documentation for Serialize for details.
def to_serializable(attributes)
if first.respond_to?(:to_serializable)
attributes = Serialize.normalize_attributes(attributes)
map { |e| e.to_serializable(attributes) }
else
self
end
end
end
end
end
class ActiveRecord::Base; include Resourceful::Serialize::Model; end
class Array; include Resourceful::Serialize::Array; end