add additional parsed xml properties to the entity-instance

allow mapping with `name` and `xml_name`
This commit is contained in:
Benjamin Neff 2016-02-05 00:16:24 +01:00
parent cf5da6e1ab
commit 1c7a5ad3e6
13 changed files with 129 additions and 56 deletions

View file

@ -56,8 +56,8 @@ module DiasporaFederation
# #
# @see Entity#to_h # @see Entity#to_h
# @return [Hash] entity data hash with updated signatures # @return [Hash] entity data hash with updated signatures
def to_h def to_signed_h
super.tap do |hash| to_h.tap do |hash|
if author_signature.nil? if author_signature.nil?
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key_by_diaspora_id, diaspora_id) privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key_by_diaspora_id, diaspora_id)
raise AuthorPrivateKeyNotFound, "author=#{diaspora_id} guid=#{guid}" if privkey.nil? raise AuthorPrivateKeyNotFound, "author=#{diaspora_id} guid=#{guid}" if privkey.nil?
@ -74,7 +74,7 @@ module DiasporaFederation
# @return [Nokogiri::XML::Element] root element containing properties as child elements # @return [Nokogiri::XML::Element] root element containing properties as child elements
def to_xml def to_xml
entity_xml.tap do |xml| entity_xml.tap do |xml|
hash = to_h hash = to_signed_h
xml.at_xpath("author_signature").content = hash[:author_signature] xml.at_xpath("author_signature").content = hash[:author_signature]
xml.at_xpath("parent_author_signature").content = hash[:parent_author_signature] xml.at_xpath("parent_author_signature").content = hash[:parent_author_signature]
end end
@ -135,7 +135,7 @@ module DiasporaFederation
return false return false
end end
validity = pubkey.verify(DIGEST, Base64.decode64(signature), legacy_signature_data(data)) validity = pubkey.verify(DIGEST, Base64.decode64(signature), legacy_signature_data(to_h))
logger.info "event=verify_signature status=complete guid=#{guid} validity=#{validity}" logger.info "event=verify_signature status=complete guid=#{guid} validity=#{validity}"
validity validity
end end

View file

@ -35,9 +35,9 @@ module DiasporaFederation
class Entity class Entity
extend PropertiesDSL extend PropertiesDSL
# the original data hash with which the entity was created # additional properties from parsed xml
# @return [Hash] original data # @return [Hash] additional xml elements
attr_reader :data attr_reader :additional_xml_elements
# Initializes the Entity with the given attribute hash and freezes the created # Initializes the Entity with the given attribute hash and freezes the created
# instance it returns. # instance it returns.
@ -51,16 +51,17 @@ module DiasporaFederation
# @note Attributes not defined as part of the class definition ({PropertiesDSL#property}, # @note Attributes not defined as part of the class definition ({PropertiesDSL#property},
# {PropertiesDSL#entity}) get discarded silently. # {PropertiesDSL#entity}) get discarded silently.
# #
# @param [Hash] data # @param [Hash] data entity data
# @param [Hash] additional_xml_elements additional xml elements
# @return [Entity] new instance # @return [Entity] new instance
def initialize(data) def initialize(data, additional_xml_elements=nil)
raise ArgumentError, "expected a Hash" unless data.is_a?(Hash) raise ArgumentError, "expected a Hash" unless data.is_a?(Hash)
missing_props = self.class.missing_props(data) missing_props = self.class.missing_props(data)
unless missing_props.empty? unless missing_props.empty?
raise ArgumentError, "missing required properties: #{missing_props.join(', ')}" raise ArgumentError, "missing required properties: #{missing_props.join(', ')}"
end end
@data = data @additional_xml_elements = nilify(additional_xml_elements)
self.class.default_values.merge(data).each do |k, v| self.class.default_values.merge(data).each do |k, v|
instance_variable_set("@#{k}", nilify(v)) if setable?(k, v) instance_variable_set("@#{k}", nilify(v)) if setable?(k, v)

View file

@ -66,6 +66,13 @@ module DiasporaFederation
@class_prop_names ||= class_props.map {|p| p[:name] } @class_prop_names ||= class_props.map {|p| p[:name] }
end end
# finds a property by +xml_name+ or +name+
# @param [String] xml_name name of the property from the received xml
# @return [Hash] the property data
def find_property_for_xml_name(xml_name)
class_props.find {|prop| prop[:xml_name].to_s == xml_name || prop[:name].to_s == xml_name }
end
private private
def determine_xml_name(name, type, opts={}) def determine_xml_name(name, type, opts={})

View file

@ -83,38 +83,41 @@ module DiasporaFederation
# @param [Nokogiri::XML::Element] root_node xml nodes # @param [Nokogiri::XML::Element] root_node xml nodes
# @return [Entity] instance # @return [Entity] instance
def self.populate_entity(klass, root_node) def self.populate_entity(klass, root_node)
# Use all known properties to build the Entity. All other elements are respected # Use all known properties to build the Entity (entity_data). All additional xml elements
# and attached to resulted hash as string. It is intended to build a hash # are respected and attached to a hash as string (additional_xml_elements). It is intended
# invariable of an Entity definition, in order to support receiving objects # 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. # from the future versions of Diaspora, where new elements may have been added.
data = Hash[root_node.element_children.map {|child| entity_data = {}
xml_name = child.name additional_xml_elements = {}
property = klass.class_props.find {|prop| prop[:xml_name].to_s == xml_name }
if property
parse_element_from_node(property[:name], property[:type], xml_name, root_node)
else
[xml_name, child.text]
end
}]
klass.new(data).tap do |entity| 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[:name]] = parse_element_from_node(property[:type], xml_name, root_node)
else
additional_xml_elements[xml_name] = child.text
end
end
klass.new(entity_data, additional_xml_elements).tap do |entity|
entity.verify_signatures if entity.respond_to? :verify_signatures entity.verify_signatures if entity.respond_to? :verify_signatures
end end
end end
private_class_method :populate_entity private_class_method :populate_entity
# @param [Symbol] name property name
# @param [Class] type target type to parse # @param [Class] type target type to parse
# @param [String] xml_name xml tag to parse # @param [String] xml_name xml tag to parse
# @param [Nokogiri::XML::Element] node XML node to parse # @param [Nokogiri::XML::Element] node XML node to parse
# @return [Array<Symbol, Object>] parsed data # @return [Object] parsed data
def self.parse_element_from_node(name, type, xml_name, node) def self.parse_element_from_node(type, xml_name, node)
if type == String if type == String
[name, parse_string_from_node(xml_name, node)] parse_string_from_node(xml_name, node)
elsif type.instance_of?(Array) elsif type.instance_of?(Array)
[name, parse_array_from_node(type, node)] parse_array_from_node(type, node)
elsif type.ancestors.include?(Entity) elsif type.ancestors.include?(Entity)
[name, parse_entity_from_node(type, node)] parse_entity_from_node(type, node)
end end
end end
private_class_method :parse_element_from_node private_class_method :parse_element_from_node

View file

@ -1,7 +1,9 @@
module DiasporaFederation module DiasporaFederation
describe Entities::Comment do describe Entities::Comment do
let(:parent) { FactoryGirl.create(:post, author: bob) } let(:parent) { FactoryGirl.create(:post, author: bob) }
let(:data) { FactoryGirl.build(:comment_entity, diaspora_id: alice.diaspora_id, parent_guid: parent.guid).to_h } let(:data) {
FactoryGirl.build(:comment_entity, diaspora_id: alice.diaspora_id, parent_guid: parent.guid).to_signed_h
}
let(:xml) { let(:xml) {
<<-XML <<-XML

View file

@ -1,10 +1,14 @@
module DiasporaFederation module DiasporaFederation
describe Entities::Conversation do describe Entities::Conversation do
let(:parent) { FactoryGirl.create(:conversation, author: bob) } let(:parent) { FactoryGirl.create(:conversation, author: bob) }
let(:msg1) { FactoryGirl.build(:message_entity, diaspora_id: alice.diaspora_id, parent_guid: parent.guid).to_h } let(:signed_msg1) {
let(:msg2) { FactoryGirl.build(:message_entity, diaspora_id: alice.diaspora_id, parent_guid: parent.guid).to_h } msg = FactoryGirl.build(:message_entity, diaspora_id: alice.diaspora_id, parent_guid: parent.guid).to_signed_h
let(:signed_msg1) { Entities::Message.new(msg1) } Entities::Message.new(msg)
let(:signed_msg2) { Entities::Message.new(msg2) } }
let(:signed_msg2) {
msg = FactoryGirl.build(:message_entity, diaspora_id: alice.diaspora_id, parent_guid: parent.guid).to_signed_h
Entities::Message.new(msg)
}
let(:data) { let(:data) {
FactoryGirl.attributes_for(:conversation_entity).merge!( FactoryGirl.attributes_for(:conversation_entity).merge!(
messages: [signed_msg1, signed_msg2], messages: [signed_msg1, signed_msg2],

View file

@ -7,7 +7,7 @@ module DiasporaFederation
diaspora_id: alice.diaspora_id, diaspora_id: alice.diaspora_id,
parent_guid: parent.guid, parent_guid: parent.guid,
parent_type: parent.entity_type parent_type: parent.entity_type
).to_h ).to_signed_h
} }
let(:xml) { let(:xml) {

View file

@ -1,7 +1,9 @@
module DiasporaFederation module DiasporaFederation
describe Entities::Message do describe Entities::Message do
let(:parent) { FactoryGirl.create(:conversation, author: bob) } let(:parent) { FactoryGirl.create(:conversation, author: bob) }
let(:data) { FactoryGirl.build(:message_entity, diaspora_id: alice.diaspora_id, parent_guid: parent.guid).to_h } let(:data) {
FactoryGirl.build(:message_entity, diaspora_id: alice.diaspora_id, parent_guid: parent.guid).to_signed_h
}
let(:xml) { let(:xml) {
<<-XML <<-XML

View file

@ -7,7 +7,7 @@ module DiasporaFederation
diaspora_id: alice.diaspora_id, diaspora_id: alice.diaspora_id,
parent_guid: parent.guid, parent_guid: parent.guid,
parent_type: parent.entity_type parent_type: parent.entity_type
).to_h ).to_signed_h
} }
let(:xml) { let(:xml) {

View file

@ -2,7 +2,11 @@ module DiasporaFederation
describe Entities::PollParticipation do describe Entities::PollParticipation do
let(:parent) { FactoryGirl.create(:poll, author: bob) } let(:parent) { FactoryGirl.create(:poll, author: bob) }
let(:data) { let(:data) {
FactoryGirl.build(:poll_participation_entity, diaspora_id: alice.diaspora_id, parent_guid: parent.guid).to_h FactoryGirl.build(
:poll_participation_entity,
diaspora_id: alice.diaspora_id,
parent_guid: parent.guid
).to_signed_h
} }
let(:xml) { let(:xml) {

View file

@ -132,7 +132,7 @@ module DiasporaFederation
end end
end end
describe "#to_h" do describe "#to_signed_h" do
it "updates signatures when they were nil and keys were supplied" do it "updates signatures when they were nil and keys were supplied" do
expect(DiasporaFederation.callbacks).to receive(:trigger).with( expect(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_private_key_by_diaspora_id, hash[:diaspora_id] :fetch_private_key_by_diaspora_id, hash[:diaspora_id]
@ -144,7 +144,7 @@ module DiasporaFederation
signed_string = hash.reject {|key, _| key == :some_other_data }.values.join(";") signed_string = hash.reject {|key, _| key == :some_other_data }.values.join(";")
signed_hash = SomeRelayable.new(hash).to_h signed_hash = SomeRelayable.new(hash).to_signed_h
def verify_signature(pubkey, signature, signed_string) def verify_signature(pubkey, signature, signed_string)
pubkey.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), signed_string) pubkey.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(signature), signed_string)
@ -157,7 +157,7 @@ module DiasporaFederation
it "doesn't change signatures if they are already set" do it "doesn't change signatures if they are already set" do
hash.merge!(author_signature: "aa", parent_author_signature: "bb").delete(:some_other_data) hash.merge!(author_signature: "aa", parent_author_signature: "bb").delete(:some_other_data)
expect(SomeRelayable.new(hash).to_h).to eq(hash) expect(SomeRelayable.new(hash).to_signed_h).to eq(hash)
end end
it "raises when author_signature not set and key isn't supplied" do it "raises when author_signature not set and key isn't supplied" do
@ -166,7 +166,7 @@ module DiasporaFederation
).and_return(nil) ).and_return(nil)
expect { expect {
SomeRelayable.new(hash).to_h SomeRelayable.new(hash).to_signed_h
}.to raise_error Entities::Relayable::AuthorPrivateKeyNotFound }.to raise_error Entities::Relayable::AuthorPrivateKeyNotFound
end end
@ -179,7 +179,7 @@ module DiasporaFederation
:fetch_author_private_key_by_entity_guid, "Parent", hash[:parent_guid] :fetch_author_private_key_by_entity_guid, "Parent", hash[:parent_guid]
).and_return(nil) ).and_return(nil)
signed_hash = SomeRelayable.new(hash).to_h signed_hash = SomeRelayable.new(hash).to_signed_h
expect(signed_hash[:parent_author_signature]).to eq(nil) expect(signed_hash[:parent_author_signature]).to eq(nil)
end end

View file

@ -123,5 +123,22 @@ module DiasporaFederation
expect(Entities::TestDefaultEntity.class_prop_names).to include(:test1, :test2, :test3, :test4) expect(Entities::TestDefaultEntity.class_prop_names).to include(:test1, :test2, :test3, :test4)
end end
end end
describe ".find_property_for_xml_name" do
it "finds property by xml_name" do
dsl.property :test, xml_name: :xml_test
expect(dsl.find_property_for_xml_name("xml_test")).to eq(dsl.class_props.first)
end
it "finds property by name" do
dsl.property :test, xml_name: :xml_test
expect(dsl.find_property_for_xml_name("test")).to eq(dsl.class_props.first)
end
it "returns nil if property is not defined" do
dsl.property :test, xml_name: :xml_test
expect(dsl.find_property_for_xml_name("unknown")).to be_nil
end
end
end end
end end

View file

@ -2,6 +2,17 @@ module DiasporaFederation
describe Salmon::XmlPayload do describe Salmon::XmlPayload do
let(:entity) { Entities::TestEntity.new(test: "asdf") } let(:entity) { Entities::TestEntity.new(test: "asdf") }
let(:payload) { Salmon::XmlPayload.pack(entity) } let(:payload) { Salmon::XmlPayload.pack(entity) }
let(:entity_xml) {
<<-XML.strip
<XML>
<post>
<test_entity>
<test>asdf</test>
</test_entity>
</post>
</XML>
XML
}
describe ".pack" do describe ".pack" do
it "expects an Entity as param" do it "expects an Entity as param" do
@ -35,16 +46,7 @@ module DiasporaFederation
end end
it "produces the expected XML" do it "produces the expected XML" do
xml = <<-XML.strip expect(subject.to_xml).to eq(entity_xml)
<XML>
<post>
<test_entity>
<test>asdf</test>
</test_entity>
</post>
</XML>
XML
expect(subject.to_xml).to eq(xml)
end end
end end
end end
@ -103,7 +105,9 @@ XML
expect(subject).to be_an_instance_of Entities::TestEntity expect(subject).to be_an_instance_of Entities::TestEntity
expect(subject.test).to eq("asdf") expect(subject.test).to eq("asdf")
end end
end
context "parsing" do
it "uses xml_name for parsing" do it "uses xml_name for parsing" do
xml = <<-XML.strip xml = <<-XML.strip
<XML> <XML>
@ -122,6 +126,24 @@ XML
expect(entity.qwer).to eq("qwer") expect(entity.qwer).to eq("qwer")
end end
it "allows name for parsing even when property has a xml_name" do
xml = <<-XML.strip
<XML>
<post>
<test_entity_with_xml_name>
<test>asdf</test>
<qwer>qwer</qwer>
</test_entity>
</post>
</XML>
XML
entity = Salmon::XmlPayload.unpack(Nokogiri::XML::Document.parse(xml).root)
expect(entity).to be_an_instance_of Entities::TestEntityWithXmlName
expect(entity.test).to eq("asdf")
expect(entity.qwer).to eq("qwer")
end
it "doesn't drop unknown properties" do it "doesn't drop unknown properties" do
xml = <<-XML xml = <<-XML
<XML> <XML>
@ -134,12 +156,23 @@ XML
</post> </post>
</XML> </XML>
XML XML
expect(Entities::TestEntity).to receive(:new).with(
entity = Salmon::XmlPayload.unpack(Nokogiri::XML::Document.parse(xml).root)
expect(entity).to be_an_instance_of Entities::TestEntity
expect(entity.test).to eq("asdf")
expect(entity.additional_xml_elements).to eq(
"a_prop_from_newer_diaspora_version" => "some value", "a_prop_from_newer_diaspora_version" => "some value",
:test => "asdf",
"some_random_property" => "another value" "some_random_property" => "another value"
) )
Salmon::XmlPayload.unpack(Nokogiri::XML::Document.parse(xml).root) end
it "creates Entity with nil 'additional_xml_elements' if the xml has only known properties" do
entity = Salmon::XmlPayload.unpack(Nokogiri::XML::Document.parse(entity_xml).root)
expect(entity).to be_an_instance_of Entities::TestEntity
expect(entity.test).to eq("asdf")
expect(entity.additional_xml_elements).to be_nil
end end
end end