module Quarto
module ElementWrapper # :nodoc:
# Abstract base class for your models. Put your ElementWrapper::Base subclasses inside the
# "models" directory within your project. All files in that directory will be automatically
# required.
#
# Each ElementWrapper::Base subclass corresponds to exactly one XML element.
# You can specify the model's element name by calling element_name=, but
# generally, you just let ElementWrapper use the default, which is the subclass
# name in snake_case.
#
# Inside each ElementWrapper::Base is a REXML::Element. ElementWrapper::Base implements
# method_missing, allowing you to call that REXML::Element's methods
# on the ElementWrapper::Base instance.
#
# Instance attributes corresponding to the XML attributes will automatically
# be defined. Hwoever, if you want an attribute defined by the text of a child
# element, you have to specify it yourself.
#
# For example, suppose you have an XML document like this:
#
#
#
# Linus Torvalds
#
#
#
# You could then subclass ElementWrapper like this:
#
# class Programmer < ElementWrapper::Base
# element_name = 'programmer'
# element_attrs 'name'
# end
#
# You could then do something like this in your generate.rb file:
#
# programmer = Programmer.find :first
# puts programmer.name
# puts programmer.skill
#
# Also see the documentation for ElementWrapper::Children
class Base
include InheritableAttributes
# Returns true if both instances come from the same node in the source XML document.
def ==(other_wrapped_element)
other_wrapped_element.is_a?(Quarto::ElementWrapper::Base) and @element == other_wrapped_element.element
end
# Returns the currently-loaded REXML::Document.
def self.xml_doc
Quarto.xml_doc
end
# Returns the REXML::Element from which the instance was created.
attr_reader :element
# Creates read-only attributes from the given strings. When a model is instantiated from an XML node,
# ElementWrapper will try to populate these attributes using the node's child elements.
#
# For example, if your "employee" element has a child element called "name," you can use:
#
# element_attrs 'name'
#
# ...which will then expose a #name method for every instance of your class. Also see the usage example in the class description.
#
# Remember, XML attributes will automatically have corresponding ElementWrapper attributes. You only need to tell
# ElementWrapper which child elements to use.
def self.element_attrs(*element_names)
write_inheritable_array :element_attrs, element_names.collect { |en| en.to_sym}
end
# Returns the XML element name.
def self.element_name
@element_name
end
# Overrides the XML element name. The default is the class name in snake_case.
def self.element_name=(el_name)
@element_name = el_name
end
# Searches the XML document and returns instances of the class. The first parameter must be either :first, :last, or :all.
# If it's :first or :last, the method returns a single instance or nil. If it's :all, the method returns an array (which may be empty).
#
# Options:
#
# * :xpath - An XPath expression to limit the search. If this option is not given, the default XPath is "//element_name"
def self.find(quantifier, options = {})
raise 'You must call use_xml() in generate.rb before using the models' if xml_doc.nil?
raise ArgumentError, "Quantifier must be :all, :first, or :last, but got #{quantifier.inspect}" unless [:all, :first, :last].include?(quantifier)
raise ArgumentError, "Options must be a Hash, but got #{options.inspect}" unless options.is_a?(Hash)
if options.has_key?(:xpath)
xpath = options[:xpath]
else
xpath = "//#{@element_name}"
# TODO: add support for :root and :conditions (XPath predicates)
end
all = xml_doc.elements.to_a(xpath)
case quantifier
when :all
all.collect { |el| new(el) }
when :first
all.empty? ? nil : new(all.first)
when :last
all.empty? ? nil : new(all.last)
end
end
def self.inherited(subclass) # :nodoc:
subclass.element_name = subclass.to_s.underscore
end
def initialize(el) # :nodoc:
unless el.is_a?(REXML::Element)
raise ArgumentError, "Quarto::ElementWrapper.new must be passed a REXML::Element, but got #{el.inspect}"
end
@element = el
@attributes = {}
@element.attributes.each do |a_name, value|
@attributes[a_name.to_sym] = typecast_text(value)
end
if element_attrs = self.class.read_inheritable_attribute(:element_attrs)
element_attrs.each do |el_name|
if child_el = @element.elements[el_name.to_s]
if child_el.elements.empty?
@attributes[el_name.to_sym] = typecast_text(child_el.text)
else
@attributes[el_name.to_sym] = child_el
end
else
@attributes[el_name.to_sym] = nil
end
end
end
if text_attr = self.class.read_inheritable_attribute(:text_attr)
@attributes[text_attr.to_sym] = typecast_text(@element.text)
end
end
def method_missing(meth, *args, &block) # :nodoc:
if @attributes.has_key?(meth.to_sym)
@attributes[meth.to_sym]
elsif @element.respond_to?(meth)
@element.send(meth, *args, &block)
else
super
end
end
def respond_to?(meth, include_private = false) # :nodoc:
if @element.respond_to?(meth, include_private) or @attributes.has_key?(meth.to_sym)
true
else
super
end
end
# Creates a read-only attribute from the wrapped element's text.
# +attr_name+ can be anything you want; it doesn't have to correspond
# to anything in the XML. Example:
#
#
# Shoes
#
#
# class Product < ElementWrapper
# text_attr :name
# end
def self.text_attr(attr_name)
write_inheritable_attribute(:text_attr, attr_name)
end
protected
# When an ElementWrapper is instantiated from an XML node, all values start out as strings. This method typecasts those values.
def typecast_text(t)
if t.nil? or (t.is_a?(String) and t.empty?)
nil
elsif t =~ /^-?[0-9]+$/
t.to_i
elsif t =~ /^-?[0-9]*\.[0-9]+$/
t.to_f
else
t
end
end
end
end
end