$LOAD_PATH.unshift(File.dirname(__FILE__)) unless
$LOAD_PATH.include?(File.dirname(__FILE__)) || $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__)))
require 'uri'
%w(extensions definition xml).each do |file|
require File.join('roxml', file)
end
module ROXML # :nodoc:
VERSION = '2.5.1'
def self.included(base) # :nodoc:
base.extend ClassMethods::Accessors,
ClassMethods::Declarations,
ClassMethods::Operations
base.class_eval do
include InstanceMethods::Accessors,
InstanceMethods::Construction,
InstanceMethods::Conversions
end
end
module InstanceMethods # :nodoc:
# Instance method equivalents of the Class method accessors
module Accessors # :nodoc:all
# Provides access to ROXML::ClassMethods::Accessors::tag_name directly from an instance of a ROXML class
def tag_name
self.class.tag_name
end
deprecate :tag_name => 'use class.tag_name instead'
# Provides access to ROXML::ClassMethods::Accessors::tag_refs directly from an instance of a ROXML class
def tag_refs
self.class.tag_refs_without_deprecation
end
deprecate :tag_refs => :roxml_attrs
end
module Construction
# xml_initialize is called at the end of the #from_xml operation on objects
# where xml_construct is not in place. Override xml_initialize in order to establish
# post-import behavior. For example, you can use xml_initialize to map xml attribute
# values into the object standard initialize function, thus enabling a ROXML object
# to freely be either xml-backed or instantiated directly via #new.
# An example of this follows:
#
# class Measurement
# include ROXML
#
# xml_reader :units, :from => :attr
# xml_reader :value, :from => :content
#
# def xml_initialize
# # the object is instantiated, and all xml attributes are imported
# # and available, i.e., value and units below are the same value and units
# # found in the xml via the xml_reader declarations above.
# initialize(value, units)
# end
#
# def initialize(value, units = 'pixels')
# @value = Float(value)
# @units = units.to_s
# if @units.starts_with? 'hundredths-'
# @value /= 100
# @units = @units.split('hundredths-')[1]
# end
# end
# end
#
# #xml_initialize may be written to take arguments, in which case extra arguments
# from from_xml will be passed into the function.
#
def xml_initialize # :nodoc:
end
deprecate :xml_initialize => :after_parse
end
module Conversions
# Returns a LibXML::XML::Node or a REXML::Element representing this object
def to_xml(name = nil)
returning XML::Node.new((name || self.class.tag_name).to_s) do |root|
self.class.roxml_attrs.each do |attr|
ref = attr.to_ref(self)
v = ref.to_xml
unless v.nil?
ref.update_xml(root, v)
end
end
end
end
end
end
# This class defines the annotation methods that are mixed into your
# Ruby classes for XML mapping information and behavior.
#
# See xml_name, xml_initialize, xml, xml_reader and xml_accessor for
# available annotations.
#
module ClassMethods # :nodoc:
module Declarations
# Sets the name of the XML element that represents this class. Use this
# to override the default lowercase class name.
#
# Example:
# class BookWithPublisher
# xml_name :book
# end
#
# Without the xml_name annotation, the XML mapped tag would have been "bookwithpublisher".
#
def xml_name(name)
@roxml_tag_name = name
end
# Sets the namemespace for attributes and elements of this class. You can override
# this value on individual elements via the :from option
#
# Example:
# class Book
# xml_namespace :aws
#
# xml_reader :default_namespace
# xml_reader :different_namespace, :from => 'different:namespace'
# xml_reader :no_namespace, :from => ':no_namespace'
# end
#
#
# value
# value
# value
#
#
def xml_namespace(namespace)
@roxml_namespace = namespace.to_s
end
# Most xml documents have a consistent naming convention, for example, the node and
# and attribute names might appear in CamelCase. xml_convention enables you to adapt
# the roxml default names for this object to suit this convention. For example,
# if I had a document like so:
#
#
#
#
#
#
# Then I could access it's contents by defining the following class:
#
# class XmlDoc
# include ROXML
# xml_convention :camelcase
# xml_reader :my_precious_data
# xml_reader :in_attrs, :in => 'MoreToSee'
# end
#
# You may supply a block or any #to_proc-able object as the argument,
# and it will be called against the default node and attribute names before searching
# the document. Here are some example declaration:
#
# xml_convention :upcase
# xml_convention &:camelcase
# xml_convention {|val| val.gsub('_', '').downcase }
#
# See ActiveSupport::CoreExtensions::String::Inflections for more prepackaged formats
#
# Note that the xml_convention is also applied to the default root-level tag_name,
# but in this case an underscored version of the name is applied, for convenience.
def xml_convention(to_proc_able = nil, &block)
raise ArgumentError, "conventions are already set" if @roxml_naming_convention
raise ArgumentError, "only one conventions can be set" if to_proc_able.respond_to?(:to_proc) && block_given?
@roxml_naming_convention = to_proc_able.try(:to_proc)
@roxml_naming_convention = block if block_given?
end
def roxml_naming_convention # :nodoc:
(@roxml_naming_convention || superclass.try(:roxml_naming_convention)).freeze
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'
#
# === Boolean attributes
# If the name ends in a ?, ROXML will attempt to coerce the value to true or false,
# with True, TRUE, true and 1 mapping to true and False, FALSE, false and 0 mapping
# to false, as shown below:
#
# xml_reader :desirable?
# xml_reader :bizzare?, :from => '@BIZZARE'
#
# x = #from_xml(%{
#
# })
# x.desirable?
# => false
# x.bizzare?
# => true
#
# If an unexpected value is encountered, the attribute will be set to nil,
# unless you provide a block, in which case the block will recived
# the actual unexpected value.
#
# #from_xml(%{
#
# }).desirable?
# => nil
#
# xml_reader :strange? do |val|
# val.upcase
# end
#
# #from_xml(%{
#
# }).strange?
# => DUNNO
#
# == Blocks
# You may also pass a block which manipulates the associated parsed value.
#
# class Muffins
# include ROXML
#
# xml_reader(:count, :from => 'bakers_dozens') {|val| val.to_i * 13 }
# end
#
# For hash types, the block recieves the key and value as arguments, and they should
# be returned as an array of [key, value]
#
# For array types, the entire array is passed in, and must be returned in the same fashion.
#
# == 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]
#
# ==== Hash
# Somewhere between the simplicity of a :text/:attr mapping, and the complexity of
# a full Object/Type mapping, lies the Hash mapping. It serves in the case where you have
# a collection of key-value pairs represented in your xml. You create a hash declaration by
# passing a hash mapping as the type argument. A few examples:
#
# ===== Hash of :texts
# For xml such as this:
#
#
#
#
#
#
#
#
#
#
#
#
# You can individually declare your key and value names:
# xml_reader :definitions, :as => {:key => 'word',
# :value => 'meaning'}
#
# ===== Hash of :content &c.
# For xml such as this:
#
#
# adjective: (of a geological formation) sloping downward from the center in all directions.
# To use evasions or ambiguities; equivocate.
#
#
# You can individually declare the key and value, but with the attr, you need to provide both the type
# and name of that type (i.e. {:attr => :word}), because omitting the type will result in ROXML
# defaulting to :text
# xml_reader :definitions, :as => {:key => {:attr => 'word'},
# :value => :content}
#
# ===== Hash of :name &c.
# For xml such as this:
#
#
# adjective: (of a geological formation) sloping downward from the center in all directions.
# To use evasions or ambiguities; equivocate.
#
#
# You can pick up the node names (e.g. quaquaversally) using the :name keyword:
# xml_reader :definitions, :as => {:key => :name,
# :value => :content}
#
# === :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 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 '/'
# [:else] Default value for attribute, if missing from the xml on .from_xml
# [:required] If true, throws RequiredElementMissing when the element isn't present
# [:frozen] If true, all results are frozen (using #freeze) at parse-time.
# [:cdata] True for values which should be input from or output as cdata elements
#
def xml_attr(sym, type_and_or_opts = nil, opts = nil, &block)
returning Definition.new(sym, *[type_and_or_opts, opts].compact, &block) do |attr|
if roxml_attrs.map(&:accessor).include? attr.accessor
raise "Accessor #{attr.accessor} is already defined as XML accessor in class #{self.name}"
end
@roxml_attrs << attr
end
end
def xml(sym, writable = false, type_and_or_opts = nil, opts = nil, &block) #:nodoc:
send(writable ? :xml_accessor : :xml_reader, sym, type_and_or_opts, opts, &block)
end
deprecate :xml => "use xml_attr, xml_reader, or xml_accessor instead"
# Declares a read-only xml reference. See xml_attr for details.
#
# Note that while xml_reader does not create a setter for this attribute,
# its value can be modified indirectly via methods. For more complete
# protection, consider the :frozen option.
def xml_reader(sym, type_and_or_opts = nil, opts = nil, &block)
attr = xml_attr sym, type_and_or_opts, opts, &block
add_reader(attr)
end
# Declares a writable xml reference. See xml_attr for details.
#
# Note that while xml_accessor does create a setter for this attribute,
# you can use the :frozen option to prevent its value from being
# modified indirectly via methods.
def xml_accessor(sym, type_and_or_opts = nil, opts = nil, &block)
attr = xml_attr sym, type_and_or_opts, opts, &block
add_reader(attr)
attr_writer(attr.variable_name)
end
# This method is deprecated, please use xml_initialize instead
def xml_construct(*args) # :nodoc:
present_tags = tag_refs_without_deprecation.map(&:accessor)
missing_tags = args - present_tags
unless missing_tags.empty?
raise ArgumentError, "All construction tags must be declared first using xml, " +
"xml_reader, or xml_accessor. #{missing_tags.join(', ')} is missing. " +
"#{present_tags.join(', ')} are declared."
end
@xml_construction_args = args
end
deprecate :xml_construct => :xml_initialize
private
def add_reader(attr)
define_method(attr.accessor) do
instance_variable_get("@#{attr.variable_name}")
end
end
end
module Accessors
def xml_construction_args # :nodoc:
@xml_construction_args ||= []
end
deprecate :xml_construction_args
# A helper which enables us to detect when the xml_name has been explicitly set
def xml_name? #:nodoc:
@roxml_tag_name
end
deprecate :xml_name?
# Returns the tag name (also known as xml_name) of the class.
# If no tag name is set with xml_name method, returns default class name
# in lowercase.
#
# If xml_convention is set, it is called with an *underscored* version of
# the class name. This is because active support's inflector generally expects
# an underscored version, and several operations (e.g. camelcase(:lower), dasherize)
# do not work without one.
def tag_name
return roxml_tag_name if roxml_tag_name
if tag_name = name.split('::').last
roxml_naming_convention ? roxml_naming_convention.call(tag_name.underscore) : tag_name.downcase
end
end
def roxml_tag_name # :nodoc:
@roxml_tag_name || superclass.try(:roxml_tag_name)
end
def roxml_namespace # :nodoc:
@roxml_namespace || superclass.try(:roxml_namespace)
end
# Returns array of internal reference objects, such as attributes
# and composed XML objects
def roxml_attrs
@roxml_attrs ||= []
(@roxml_attrs + (superclass.try(:roxml_attrs) || [])).freeze
end
def tag_refs # :nodoc:
roxml_attrs.map {|a| a.to_ref(nil) }
end
deprecate :tag_refs => :roxml_attrs
end
module Operations
#
# Creates a new Ruby object from XML using mapping information
# annotated in the class.
#
# The input data is either an XML::Node, String, Pathname, or File representing
# the XML document.
#
# Example
# book = Book.from_xml(File.read("book.xml"))
# or
# book = Book.from_xml("Beyond Java")
#
# _initialization_args_ passed into from_xml will be passed into
# the object's .new, prior to populating the xml_attrs.
#
# After the instatiation and xml population
#
# See also: xml_initialize
#
def from_xml(data, *initialization_args)
xml = XML::Node.from(data)
unless xml_construction_args_without_deprecation.empty?
args = xml_construction_args_without_deprecation.map do |arg|
roxml_attrs.find {|attr| attr.accessor == arg }
end.map {|attr| attr.to_ref(self).value_in(xml) }
new(*args)
else
returning new(*initialization_args) do |inst|
roxml_attrs.each do |attr|
value = attr.to_ref(inst).value_in(xml)
setter = :"#{attr.variable_name}="
inst.respond_to?(setter) \
? inst.send(setter, value) \
: inst.instance_variable_set("@#{attr.variable_name}", value)
end
inst.try(:after_parse)
end
end
rescue ArgumentError => e
raise e, e.message + " for class #{self}"
end
# Deprecated in favor of #from_xml
def parse(data) # :nodoc:
from_xml(data)
end
deprecate :parse => :from_xml
end
end
end