module Blather
##
# Base XML Node
# All XML classes subclass XMPPNode
# it allows the addition of helpers
class XMPPNode < Nokogiri::XML::Node
BASE_NAMES = %w[presence message iq].freeze
@@registrations = {}
class_inheritable_accessor :registered_ns,
:registered_name
##
# Lets a subclass register itself
#
# This registers a namespace that is used when looking
# up the class name of the object to instantiate when a new
# stanza is received
def self.register(name, ns = nil)
self.registered_name = name.to_s
self.registered_ns = ns
@@registrations[[self.registered_name, self.registered_ns]] = self
end
##
# Find the class to use given the name and namespace of a stanza
def self.class_from_registration(name, xmlns)
name = name.to_s
@@registrations[[name, xmlns]] || @@registrations[[name, nil]]
end
##
# Looks up the class to use then instantiates an object
# of that class and imports all the node's attributes
# and children into it.
def self.import(node)
klass = class_from_registration(node.element_name, (node.namespace.href if node.namespace))
if klass && klass != self
klass.import(node)
else
new(node.element_name).inherit(node)
end
end
##
# Provides an attribute reader helper. Default behavior is to
# conver the values of the attribute into a symbol. This can
# be turned off by passing :to_sym => false
#
# class Node
# attribute_reader :type
# attribute_reader :name, :to_sym => false
# end
#
# n = Node.new
# n[:type] = 'foo'
# n.type == :foo
# n[:name] = 'bar'
# n.name == 'bar'
def self.attribute_reader(*syms)
opts = syms.last.is_a?(Hash) ? syms.pop : {}
convert_str = "val.#{opts[:call]} if val" if opts[:call]
syms.flatten.each do |sym|
class_eval(<<-END, __FILE__, __LINE__)
def #{sym}
val = self[:#{sym}]
#{convert_str}
end
END
end
end
##
# Provides an attribute writer helper.
#
# class Node
# attribute_writer :type
# end
#
# n = Node.new
# n.type = 'foo'
# n[:type] == 'foo'
def self.attribute_writer(*syms)
syms.flatten.each do |sym|
next if sym.is_a?(Hash)
class_eval(<<-END, __FILE__, __LINE__)
def #{sym}=(value)
self[:#{sym}] = value
end
END
end
end
##
# Provides an attribute accessor helper combining
# attribute_reader and attribute_writer
#
# class Node
# attribute_accessor :type
# attribute_accessor :name, :to_sym => false
# end
#
# n = Node.new
# n.type = 'foo'
# n.type == :foo
# n.name = 'bar'
# n.name == 'bar'
def self.attribute_accessor(*syms)
attribute_reader *syms
attribute_writer *syms
end
##
# Provides a content reader helper that returns the content of a node
# +method+ is the method to create
# +conversion+ is a method to call on the content before sending it back
# +node+ is the name of the content node (this defaults to the method name)
#
# class Node
# content_attr_reader :body
# content_attr_reader :type, :to_sym
# content_attr_reader :id, :to_i, :identity
# end
#
# n = Node.new 'foo'
# n.to_s == "foobarbazerror1000"
# n.body == 'foobarbaz'
# n.type == :error
# n.id == 1000
def self.content_attr_reader(method, conversion = nil, node = nil)
node ||= method
conversion = "val.#{conversion} if val.respond_to?(:#{conversion})" if conversion
class_eval(<<-END, __FILE__, __LINE__)
def #{method}
val = content_from :#{node}
#{conversion}
end
END
end
##
# Provides a content writer helper that creates or updates the content of a node
# +method+ is the method to create
# +node+ is the name of the node to create (defaults to the method name)
#
# class Node
# content_attr_writer :body
# content_attr_writer :id, :identity
# end
#
# n = Node.new 'foo'
# n.body = 'thebodytext'
# n.id = 'id-text'
# n.to_s == 'thebodytextid-text'
def self.content_attr_writer(method, node = nil)
node ||= method
class_eval(<<-END, __FILE__, __LINE__)
def #{method}=(val)
set_content_for :#{node}, val
end
END
end
##
# Provides a quick way of building +content_attr_reader+ and +content_attr_writer+
# for the same method and node
def self.content_attr_accessor(method, conversion = nil, node = nil)
content_attr_reader method, conversion, node
content_attr_writer method, node
end
##
# Automatically sets the namespace registered by the subclass
def self.new(name = nil, doc = nil)
name ||= self.registered_name
node = super name.to_s, (doc || Nokogiri::XML::Document.new)
node.document.root = node unless doc
node.namespace = self.registered_ns unless BASE_NAMES.include?(name.to_s)
node
end
##
# Quickway of turning itself into a proper object
def to_stanza
self.class.import self
end
alias_method :nokogiri_namespace=, :namespace=
def namespace=(namespaces)
case namespaces
when Nokogiri::XML::Namespace
self.nokogiri_namespace = namespaces
when String
self.add_namespace nil, namespaces
when Hash
if ns = namespaces.delete(nil)
self.add_namespace nil, ns
end
namespaces.each do |p, n|
ns = self.add_namespace p, n
self.nokogiri_namespace = ns
end
end
end
def namespace_href
namespace.href if namespace
end
##
# Remove a child with the name and (optionally) namespace given
def remove_child(name, ns = nil)
child = xpath(name, ns).first
child.remove if child
end
##
# Remove all children with a given name
def remove_children(name)
xpath("./*[local-name()='#{name}']").remove
end
##
# Pull the content from a child
def content_from(name, ns = nil)
child = xpath(name, ns).first
child.content if child
end
##
# Sets the content for the specified node.
# If the node exists it is updated. If not a new node is created
# If the node exists and the content is nil, the node will be removed entirely
def set_content_for(node, content = nil)
if content
child = xpath(node).first
self << (child = XMPPNode.new(node, self.document)) unless child
child.content = content
else
remove_child node
end
end
alias_method :copy, :dup
##
# Inherit all of stanza's attributes and children
def inherit(stanza)
set_namespace stanza.namespace if stanza.namespace
inherit_attrs stanza.attributes
stanza.children.each { |c| self << c.dup }
self
end
##
# Inherit only stanza's attributes
def inherit_attrs(attrs)
attrs.each { |name, value| self[name] = value }
self
end
end #XMPPNode
end