require 'hooks/inheritable_attribute'
require 'representable/definition'
require 'representable/nokogiri_extensions'
module Representable
def self.included(base)
base.class_eval do
extend ClassMethods::Accessors, ClassMethods::Declarations
extend Hooks::InheritableAttribute
inheritable_attr :representable_attrs
self.representable_attrs = []
inheritable_attr :explicit_representation_name # FIXME: move to Accessors.
end
end
module ClassMethods # :nodoc:
module Declarations
def definition_class
Definition
end
# Declares a reference to a certain xml element, whether an attribute, a node,
# or a typed collection of nodes. This method does not add a corresponding accessor
# to the object. For that behavior see the similar methods: .xml_reader and .xml_accessor.
#
# == Sym Option
# [sym] Symbol representing the name of the accessor.
#
# === Default naming
# This name will be the default node or attribute name searched for,
# if no other is declared. For example,
#
# xml_reader :bob
# xml_accessor :pony, :from => :attr
#
# are equivalent to:
#
# xml_reader :bob, :from => 'bob'
# xml_accessor :pony, :from => '@pony'
#
# == Options
# === :as
# ==== Basic Types
# Allows you to specify one of several basic types to return the value as. For example
#
# xml_reader :count, :as => Integer
#
# is equivalent to:
#
# xml_reader(:count) {|val| Integer(val) unless val.empty? }
#
# Such block shorthands for Integer, Float, Fixnum, BigDecimal, Date, Time, and DateTime
# are currently available, but only for non-Hash declarations.
#
# To reference many elements, put the desired type in a literal array. e.g.:
#
# xml_reader :counts, :as => [Integer]
#
# Even an array of text nodes can be specified with :as => []
#
# xml_reader :quotes, :as => []
#
# === Other ROXML Class
# Declares an accessor that represents another ROXML class as child XML element
# (one-to-one or composition) or array of child elements (one-to-many or
# aggregation) of this type. Default is one-to-one. For one-to-many, simply pass the class
# as the only element in an array.
#
# Composition example:
#
#
# Pragmatic Bookshelf
#
#
#
# Can be mapped using the following code:
# class Book
# xml_reader :publisher, :as => Publisher
# end
#
# Aggregation example:
#
#
#
#
#
#
#
# Can be mapped using the following code:
# class Library
# xml_reader :books, :as => [Book], :in => "books"
# end
#
# If you don't have the tag to wrap around the list of tags:
#
# Ruby books
#
#
#
#
# You can skip the wrapper argument:
# xml_reader :books, :as => [Book]
#
# === :from
# The name by which the xml value will be found, either an attribute or tag name in XML.
# Default is sym, or the singular form of sym, in the case of arrays and hashes.
#
# This value may also include XPath notation.
#
# ==== :from => :content
# When :from is set to :content, this refers to the content of the current node,
# rather than a sub-node. It is equivalent to :from => '.'
#
# Example:
# class Contributor
# xml_reader :name, :from => :content
# xml_reader :role, :from => :attr
# end
#
# To map:
# James Wick
#
# ==== :from => :attr
# When :from is set to :attr, this refers to the content of an attribute,
# rather than a sub-node. It is equivalent to :from => '@attribute_name'
#
# Example:
# class Book
# xml_reader :isbn, :from => "@ISBN"
# xml_accessor :title, :from => :attr # :from defaults to '@title'
# end
#
# To map:
#
#
# ==== :from => :text
# The default source, if none is specified, this means the accessor
# represents a text node from XML. This is documented for completeness
# only. You should just leave this option off when you want the default behavior,
# as in the examples below.
#
# :text is equivalent to :from => accessor_name, and you should specify the
# actual node name (and, optionally, a namespace) if it differs, as in the case of :author below.
#
# Example:
# class Book
# xml_reader :author, :from => 'Author'
# xml_accessor :description, :cdata => true
# xml_reader :title
# end
#
# To map:
#
# Programming Ruby: the pragmatic programmers' guide
#
# David Thomas
#
#
# Likewise, a number of :text node values can be collected in an array like so:
#
# Example:
# class Library
# xml_reader :books, :as => []
# end
#
# To map:
#
# To kill a mockingbird
# House of Leaves
# Gödel, Escher, Bach
#
#
# === Other Options
# [:in] An optional name of a wrapping tag for this XML accessor.
# This can include other xpath values, which will be joined with :from with a '/'
# [:required] If true, throws RequiredElementMissing when the element isn't present
# [:cdata] true for values which should be input from or output as cdata elements
# [:to_xml] this proc is applied to the attributes value outputting the instance via #to_xml
#
def representable_property(*args) # TODO: make it accept 1-n props.
attr = representable_attr(*args)
add_reader(attr)
attr_writer(attr.accessor)
end
def representable_collection(name, options={})
options[:as] = [options[:as]].compact
representable_property(name, options)
end
private
def representable_attr(name, options={})
definition_class.new(name, options).tap do |attr|
representable_attrs << attr
end
end
def representable_reader(*syms, &block)
representable_attr(*syms, &block).each do |attr|
add_reader(attr)
end
end
def add_reader(attr)
define_method(attr.accessor) do
instance_variable_get(attr.instance_variable_name)
end
end
end
module Accessors
def representation_name=(name)
self.explicit_representation_name = name
end
def representation_name
explicit_representation_name or name.split('::').last.
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
downcase
end
end
end
end