diaspora_federation/lib/diaspora_federation/salmon/xml_payload.rb
2016-02-19 03:35:01 +01:00

162 lines
6.4 KiB
Ruby

module DiasporaFederation
module Salmon
# +XmlPayload+ provides methods to wrap a XML-serialized {Entity} inside a
# common XML structure that will become the payload for federation messages.
#
# The wrapper looks like so:
# <XML>
# <post>
# {data}
# </post>
# </XML>
#
# (The +post+ element is there for historic reasons...)
module XmlPayload
# Encapsulates an Entity inside the wrapping xml structure
# and returns the XML Object.
#
# @param [Entity] entity subject
# @return [Nokogiri::XML::Element] XML root node
# @raise [ArgumentError] if the argument is not an Entity subclass
# @deprecated
def self.pack(entity)
raise ArgumentError, "only instances of DiasporaFederation::Entity allowed" unless entity.is_a?(Entity)
entity_xml = entity.to_xml
doc = entity_xml.document
wrap = Nokogiri::XML::Element.new("XML", doc)
wrap_post = Nokogiri::XML::Element.new("post", doc)
entity_xml.parent = wrap_post
wrap << wrap_post
wrap
end
# Extracts the Entity XML from the wrapping XML structure, parses the entity
# XML and returns a new instance of the Entity that was packed inside the
# given payload.
#
# @param [Nokogiri::XML::Element] xml payload XML root node
# @return [Entity] re-constructed Entity instance
# @raise [ArgumentError] if the argument is not an
# {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Element Nokogiri::XML::Element}
# @raise [UnknownEntity] if the class for the entity contained inside the
# XML can't be found
def self.unpack(xml)
raise ArgumentError, "only Nokogiri::XML::Element allowed" unless xml.instance_of?(Nokogiri::XML::Element)
data = xml_wrapped?(xml) ? xml.at_xpath("post/*[1]") : xml
klass_name = entity_class_name(data.name)
raise Salmon::UnknownEntity, "'#{klass_name}' not found" unless Entities.const_defined?(klass_name)
klass = Entities.const_get(klass_name)
populate_entity(klass, data)
end
# @param [Nokogiri::XML::Element] element
# @deprecated
def self.xml_wrapped?(element)
(element.name == "XML" && !element.at_xpath("post").nil? &&
!element.at_xpath("post").children.empty?)
end
private_class_method :xml_wrapped?
# Transform the given String from the lowercase underscored version to a
# camelized variant, used later for getting the Class constant.
#
# @param [String] term "snake_case" class name
# @return [String] "CamelCase" class name
def self.entity_class_name(term)
term.to_s.tap do |string|
raise Salmon::InvalidEntityName, "'#{string}' is invalid" unless string =~ /^[a-z]*(_[a-z]*)*$/
string.sub!(/^[a-z]/, &:upcase)
string.gsub!(/_([a-z])/) { Regexp.last_match[1].upcase }
end
end
private_class_method :entity_class_name
# Construct a new instance of the given Entity and populate the properties
# with the attributes found in the XML.
# Works recursively on nested Entities and Arrays thereof.
#
# @param [Class] klass entity class
# @param [Nokogiri::XML::Element] root_node xml nodes
# @return [Entity] instance
def self.populate_entity(klass, root_node)
# Use all known properties to build the Entity (entity_data). All additional xml elements
# are respected and attached to a hash as string (additional_xml_elements). It is intended
# to build a hash invariable of an Entity definition, in order to support receiving objects
# from the future versions of Diaspora, where new elements may have been added.
entity_data = {}
xml_order = []
additional_xml_elements = {}
root_node.element_children.each do |child|
xml_name = child.name
property = klass.find_property_for_xml_name(xml_name)
if property
entity_data[property] = parse_element_from_node(klass.class_props[property], xml_name, root_node)
xml_order << property
else
additional_xml_elements[xml_name] = child.text
xml_order << xml_name
end
end
klass.new(entity_data, xml_order, additional_xml_elements).tap do |entity|
entity.verify_signatures if entity.respond_to? :verify_signatures
end
end
private_class_method :populate_entity
# @param [Class] type target type to parse
# @param [String] xml_name xml tag to parse
# @param [Nokogiri::XML::Element] node XML node to parse
# @return [Object] parsed data
def self.parse_element_from_node(type, xml_name, node)
if type == String
parse_string_from_node(xml_name, node)
elsif type.instance_of?(Array)
parse_array_from_node(type, node)
elsif type.ancestors.include?(Entity)
parse_entity_from_node(type, node)
end
end
private_class_method :parse_element_from_node
# create simple entry in data hash
#
# @param [String] name xml tag to parse
# @param [Nokogiri::XML::Element] root_node XML root_node to parse
# @return [String] data
def self.parse_string_from_node(name, root_node)
node = root_node.xpath(name.to_s)
node.first.text if node.any?
end
private_class_method :parse_string_from_node
# create an entry in the data hash for the nested entity
#
# @param [Class] type target type to parse
# @param [Nokogiri::XML::Element] root_node XML node to parse
# @return [Entity] parsed child entity
def self.parse_entity_from_node(type, root_node)
node = root_node.xpath(type.entity_name)
populate_entity(type, node.first) if node.any?
end
private_class_method :parse_entity_from_node
# collect all nested children of that type and create an array in the data hash
#
# @param [Array<Class>] type target type to parse
# @param [Nokogiri::XML::Element] root_node XML node to parse
# @return [Array<Entity>] array with parsed child entities
def self.parse_array_from_node(type, root_node)
node = root_node.xpath(type.first.entity_name)
node.map {|child| populate_entity(type.first, child) }
end
private_class_method :parse_array_from_node
end
end
end