module ROXML
class RequiredElementMissing < ArgumentError # :nodoc:
end
#
# Internal base class that represents an XML - Class binding.
#
class XMLRef # :nodoc:
attr_reader :opts
delegate :required?, :array?, :accessor, :default, :wrapper, :to => :opts
def initialize(opts, instance)
@opts = opts
@instance = instance
end
def blocks
opts.blocks || []
end
def to_xml(instance)
val = instance.__send__(accessor)
opts.to_xml.respond_to?(:call) ? opts.to_xml.call(val) : val
end
def name
opts.name_explicit? ? opts.name : conventionize(opts.name)
end
def xpath_name
namespacify(name)
end
def value_in(xml)
xml = XML::Node.from(xml)
value = fetch_value(xml)
value = default if default && (value.nil? || value.to_s.empty?)
value = apply_blocks(value)
value = freeze(value) if value && opts.frozen?
value
end
private
def conventionize(what)
convention ||= @instance.class.respond_to?(:roxml_naming_convention) && @instance.class.roxml_naming_convention
if !what.blank? && convention.respond_to?(:call)
URI.decode_www_form_component(convention.call(URI.encode_www_form_component(what)))
else
what
end
end
def namespacify(what)
if what.to_s.present? && opts.namespace != false && ns = [opts.namespace, @instance.class.roxml_namespace, @default_namespace].compact.map(&:to_s).first
require "rexml/xpath_parser"
parser = REXML::Parsers::XPathParser.new
parsed = parser.parse what
parsed.each_cons(4).with_index.each do |a,i|
if a[0..2] == [:child, :qname, ""]
if ns == "*"
parsed[i+1,3] = [:any, :predicate, [:eq, [:function, "local-name", []], [:literal, a[3]]]] if a[3] != "*"
else
a[2].replace ns
end
end
end
parser.abbreviate parsed
else
what
end
end
def apply_blocks(val)
blocks.inject(val) {|val, block| block.call(val) }
rescue Exception => ex
raise ex, "#{accessor}: #{ex.message}"
end
def freeze(val)
val.each(&:freeze) if val.is_a?(Enumerable)
val.freeze
end
def xpath
opts.wrapper ? "#{namespacify(opts.wrapper)}/#{xpath_name}" : xpath_name.to_s
end
def auto_wrapper
namespacify(conventionize(opts.name.pluralize))
end
def auto_xpath
"#{auto_wrapper}/#{xpath_name}" if array?
end
def several?
array?
end
def wrap(xml, opts = {:always_create => false})
wrap_with = defined?(@auto_vals) && @auto_vals ? auto_wrapper : wrapper
return xml if !wrap_with || xml.name == wrap_with
wraps = wrap_with.to_s.split('/')
wraps.inject(xml) do |node,wrap|
if !opts[:always_create] && (child = node.children.find {|c| c.name == wrap })
child
else
XML.add_node(node, wrap)
end
end
end
def nodes_in(xml)
@default_namespace = XML.default_namespace(xml)
vals = XML.search(xml, xpath, @instance.class.roxml_namespaces)
if several? && vals.empty? && !wrapper && auto_xpath
vals = XML.search(xml, auto_xpath, @instance.class.roxml_namespaces)
@auto_vals = !vals.empty?
end
if vals.empty?
raise RequiredElementMissing, "#{name} from #{xml} for #{accessor}" if required?
default
elsif several?
vals.map do |val|
yield val
end
else
yield(vals.first)
end
end
end
# Interal class representing an XML attribute binding
#
# In context:
#
# XMLTextRef
#
class XMLAttributeRef < XMLRef # :nodoc:
# Updates the attribute in the given XML block to
# the value provided.
def update_xml(xml, values)
if array?
values.each do |value|
wrap(xml, :always_create => true).tap do |node|
XML.set_attribute(node, name, value.to_s)
end
end
else
wrap(xml).tap do |xml|
XML.set_attribute(xml, name, values.to_s)
end
end
end
private
def fetch_value(xml)
nodes_in(xml) do |node|
node.value
end
end
def xpath_name
"@#{name}"
end
end
# Interal class representing XML content text binding
#
# In context:
#
# XMLTextRef
#
class XMLTextRef < XMLRef # :nodoc:
delegate :cdata?, :content?, :name?, :to => :opts
# Updates the text in the given _xml_ block to
# the _value_ provided.
def update_xml(xml, value)
wrap(xml).tap do |xml|
if content?
add(xml, value)
elsif name?
xml.name = value
elsif array?
value.each do |v|
add(XML.add_node(xml, name), v)
end
else
add(XML.add_node(xml, name), value)
end
end
end
private
def fetch_value(xml)
if content? || name?
value =
if content?
xml.content.to_s
elsif name?
xml.name
end
if value.blank?
raise RequiredElementMissing, "#{name} from #{xml} for #{accessor}" if required?
default
else
value
end
else
nodes_in(xml) do |node|
node.content
end
end
end
def add(dest, value)
if cdata?
XML.add_cdata(dest, value.to_s)
else
XML.set_content(dest, value.to_s)
end
end
end
class XMLNameSpaceRef < XMLRef # :nodoc:
private
def fetch_value(xml)
xml.namespace.prefix
end
end
class XMLHashRef < XMLTextRef # :nodoc:
delegate :hash, :to => :opts
def initialize(opts, inst)
super(opts, inst)
@key = opts.hash.key.to_ref(inst)
@value = opts.hash.value.to_ref(inst)
end
def several?
true
end
# Updates the composed XML object in the given XML block to
# the value provided.
def update_xml(xml, value)
wrap(xml).tap do |xml|
value.each_pair do |k, v|
node = XML.add_node(xml, hash.wrapper)
@key.update_xml(node, k)
@value.update_xml(node, v)
end
end
end
private
def fetch_value(xml)
nodes_in(xml) do |node|
[@key.value_in(node), @value.value_in(node)]
end
end
def apply_blocks(vals)
unless blocks.empty?
vals.collect! do |kvp|
super(kvp)
end
end
to_hash(vals) if vals
end
def freeze(vals)
vals.each_pair{|k, v| k.freeze; v.freeze }
vals.freeze
end
def to_hash(array)
hash = array.inject({}) do |result, (k, v)|
result[k] ||= []
result[k] << v
result
end
hash.each_pair do |k, v|
hash[k] = v.first if v.size == 1
end
end
end
class XMLObjectRef < XMLTextRef # :nodoc:
delegate :sought_type, :to => :opts
# Updates the composed XML object in the given XML block to
# the value provided.
def update_xml(xml, value)
wrap(xml).tap do |xml|
params = {:name => name, :namespace => opts.namespace}
if array?
value.each do |v|
XML.add_child(xml, v.to_xml(params))
end
elsif value.is_a?(ROXML)
XML.add_child(xml, value.to_xml(params))
else
XML.add_node(xml, name).tap do |node|
XML.set_content(node, value.to_xml)
end
end
end
end
private
def fetch_value(xml)
nodes_in(xml) do |node|
if sought_type.respond_to? :from_xml
sought_type.from_xml(node)
else
sought_type.new(node)
end
end
end
end
end