add xml_payload (+tests) from raven24's gem

and do some basic refactorings for rubocop
This commit is contained in:
Benjamin Neff 2015-09-24 23:25:24 +02:00
parent d75c9af784
commit eb707c2592
5 changed files with 247 additions and 2 deletions

View file

@ -10,6 +10,7 @@ require "diaspora_federation/fetcher"
require "diaspora_federation/entities"
require "diaspora_federation/discovery"
require "diaspora_federation/salmon"
# diaspora* federation library
module DiasporaFederation

View file

@ -6,7 +6,7 @@ module DiasporaFederation
#
# Any entity also provides the means to serialize itself and all nested
# entities to XML (for deserialization from XML to +Entity+ instances, see
# {XmlPayload}).
# {Salmon::XmlPayload}).
#
# @abstract Subclass and specify properties to implement various entities.
#
@ -76,7 +76,7 @@ module DiasporaFederation
# {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Element Nokogiri::XML::Element}s
#
# @see Nokogiri::XML::Node.to_xml
# @see XmlPayload.pack
# @see Salmon::XmlPayload.pack
#
# @return [Nokogiri::XML::Element] root element containing properties as child elements
def to_xml

View file

@ -0,0 +1,8 @@
module DiasporaFederation
# This module contains a Diaspora*-specific implementation of parts of the
# {http://www.salmon-protocol.org/ Salmon Protocol}.
module Salmon
end
end
require "diaspora_federation/salmon/xml_payload"

View file

@ -0,0 +1,123 @@
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...)
class 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
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 [InvalidStructure] if the XML doesn't look like the wrapper XML
# @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)
raise InvalidStructure unless wrap_valid?(xml)
data = xml.at_xpath("post/*[1]")
klass_name = entity_class(data.name)
raise UnknownEntity unless Entities.const_defined?(klass_name)
klass = Entities.const_get(klass_name)
populate_entity(klass, data)
end
private
# @param [Nokogiri::XML::Element] element
def self.wrap_valid?(element)
(element.name == "XML" && !element.at_xpath("post").nil? &&
!element.at_xpath("post").children.empty?)
end
private_class_method :wrap_valid?
# Transform the given String from the lowercase underscored version to a
# camelized variant, used later for getting the Class constant.
#
# @note some of this is from Rails "Inflector.camelize"
#
# @param [String] term "snake_case" class name
# @return [String] "CamelCase" class name
def self.entity_class(term)
string = term.to_s
string = string.sub(/^[a-z\d]*/) { $&.capitalize }
string.gsub(%r{(?:_|(\/))([a-z\d]*)}i) { Regexp.last_match[2].capitalize }.gsub("/", "::")
end
private_class_method :entity_class
# 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] node xml nodes
# @return [Entity] instance
def self.populate_entity(klass, node)
data = {}
klass.class_props.each do |prop_def|
name = prop_def[:name]
type = prop_def[:type]
if type == String
# create simple entry in data hash
n = node.xpath(name.to_s)
data[name] = n.first.text if n.any?
elsif type.instance_of?(Array)
# collect all nested children of that type and create an array in the data hash
n = node.xpath(type.first.entity_name)
data[name] = n.map {|child| populate_entity(type.first, child) }
elsif type.ancestors.include?(Entity)
# create an entry in the data hash for the nested entity
n = node.xpath(type.entity_name)
data[name] = populate_entity(type, n.first) if n.any?
end
end
klass.new(data)
end
private_class_method :populate_entity
# Raised, if the XML structure of the parsed document doesn't resemble the
# expected structure.
class InvalidStructure < RuntimeError
end
# Raised, if the entity contained within the XML cannot be mapped to a
# defined {Entity} subclass.
class UnknownEntity < RuntimeError
end
end
end
end

View file

@ -0,0 +1,113 @@
module DiasporaFederation
describe Salmon::XmlPayload do
let(:entity) { Entities::TestEntity.new(test: "asdf") }
let(:payload) { Salmon::XmlPayload.pack(entity) }
context ".pack" do
it "expects an Entity as param" do
expect {
Salmon::XmlPayload.pack(entity)
}.not_to raise_error
end
it "raises an error when the param is not an Entity" do
["asdf", 1234, true, :test, payload].each do |val|
expect {
Salmon::XmlPayload.pack(val)
}.to raise_error ArgumentError
end
end
context "returned xml" do
subject { Salmon::XmlPayload.pack(entity) }
it "returns an xml wrapper" do
expect(subject).to be_an_instance_of Nokogiri::XML::Element
expect(subject.name).to eq("XML")
expect(subject.children).to have(1).item
expect(subject.children[0].name).to eq("post")
expect(subject.children[0].children).to have(1).item
end
it "returns the entity xml inside the wrapper" do
expect(subject.children[0].children[0].name).to eq("test_entity")
expect(subject.children[0].children[0].children).to have(1).item
end
it "produces the expected XML" do
xml_str = <<-XML.strip
<XML>
<post>
<test_entity>
<test>asdf</test>
</test_entity>
</post>
</XML>
XML
expect(subject.to_xml).to eq(xml_str)
end
end
end
context ".unpack" do
context "sanity" do
it "expects an Nokogiri::XML::Element as param" do
expect {
Salmon::XmlPayload.unpack(payload)
}.not_to raise_error
end
it "raises and error when the param is not an Nokogiri::XML::Element" do
["asdf", 1234, true, :test, entity].each do |val|
expect {
Salmon::XmlPayload.unpack(val)
}.to raise_error ArgumentError, "only Nokogiri::XML::Element allowed"
end
end
it "raises an error when the xml is wrong" do
xml = <<-XML
<root>
<weird/>
</root>
XML
expect {
Salmon::XmlPayload.unpack(Nokogiri::XML::Document.parse(xml).root)
}.to raise_error Salmon::XmlPayload::InvalidStructure
end
end
context "returned object" do
subject { Salmon::XmlPayload.unpack(payload) }
it "#to_h should match entity.to_h" do
expect(subject.to_h).to eq(entity.to_h)
end
it "returns an entity instance of the original class" do
expect(subject).to be_an_instance_of Entities::TestEntity
expect(subject.test).to eq("asdf")
end
end
context "nested entities" do
let(:child_entity1) { Entities::TestEntity.new(test: "bla") }
let(:child_entity2) { Entities::OtherEntity.new(asdf: "blabla") }
let(:nested_entity) {
Entities::TestNestedEntity.new(asdf: "QWERT",
test: child_entity1,
multi: [child_entity2, child_entity2])
}
let(:nested_payload) { Salmon::XmlPayload.pack(nested_entity) }
it "parses the xml with all the nested data" do
entity = Salmon::XmlPayload.unpack(nested_payload)
expect(entity.test.to_h).to eq(child_entity1.to_h)
expect(entity.multi).to have(2).items
expect(entity.multi.first.to_h).to eq(child_entity2.to_h)
expect(entity.asdf).to eq("QWERT")
end
end
end
end
end