add xml_payload (+tests) from raven24's gem
and do some basic refactorings for rubocop
This commit is contained in:
parent
d75c9af784
commit
eb707c2592
5 changed files with 247 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
8
lib/diaspora_federation/salmon.rb
Normal file
8
lib/diaspora_federation/salmon.rb
Normal 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"
|
||||
123
lib/diaspora_federation/salmon/xml_payload.rb
Normal file
123
lib/diaspora_federation/salmon/xml_payload.rb
Normal 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
|
||||
113
spec/lib/diaspora_federation/salmon/xml_payload_spec.rb
Normal file
113
spec/lib/diaspora_federation/salmon/xml_payload_spec.rb
Normal 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
|
||||
Loading…
Reference in a new issue