diaspora_federation 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/lib/diaspora_federation.rb +66 -1
  4. data/lib/diaspora_federation/discovery/h_card.rb +2 -3
  5. data/lib/diaspora_federation/discovery/web_finger.rb +3 -6
  6. data/lib/diaspora_federation/entities.rb +18 -0
  7. data/lib/diaspora_federation/entities/account_deletion.rb +14 -0
  8. data/lib/diaspora_federation/entities/comment.rb +26 -0
  9. data/lib/diaspora_federation/entities/conversation.rb +38 -0
  10. data/lib/diaspora_federation/entities/like.rb +35 -0
  11. data/lib/diaspora_federation/entities/location.rb +23 -0
  12. data/lib/diaspora_federation/entities/message.rb +38 -0
  13. data/lib/diaspora_federation/entities/participation.rb +28 -0
  14. data/lib/diaspora_federation/entities/person.rb +6 -3
  15. data/lib/diaspora_federation/entities/photo.rb +59 -0
  16. data/lib/diaspora_federation/entities/poll.rb +24 -0
  17. data/lib/diaspora_federation/entities/poll_answer.rb +19 -0
  18. data/lib/diaspora_federation/entities/poll_participation.rb +28 -0
  19. data/lib/diaspora_federation/entities/profile.rb +10 -8
  20. data/lib/diaspora_federation/entities/relayable.rb +101 -0
  21. data/lib/diaspora_federation/entities/relayable_retraction.rb +95 -0
  22. data/lib/diaspora_federation/entities/request.rb +21 -0
  23. data/lib/diaspora_federation/entities/reshare.rb +49 -0
  24. data/lib/diaspora_federation/entities/retraction.rb +24 -0
  25. data/lib/diaspora_federation/entities/signed_retraction.rb +66 -0
  26. data/lib/diaspora_federation/entities/status_message.rb +55 -0
  27. data/lib/diaspora_federation/entity.rb +5 -6
  28. data/lib/diaspora_federation/fetcher.rb +1 -2
  29. data/lib/diaspora_federation/properties_dsl.rb +18 -8
  30. data/lib/diaspora_federation/salmon.rb +17 -0
  31. data/lib/diaspora_federation/salmon/aes.rb +58 -0
  32. data/lib/diaspora_federation/salmon/encrypted_slap.rb +187 -0
  33. data/lib/diaspora_federation/salmon/exceptions.rb +50 -0
  34. data/lib/diaspora_federation/salmon/magic_envelope.rb +191 -0
  35. data/lib/diaspora_federation/salmon/slap.rb +128 -0
  36. data/lib/diaspora_federation/salmon/xml_payload.rb +158 -0
  37. data/lib/diaspora_federation/signing.rb +56 -0
  38. data/lib/diaspora_federation/validators.rb +20 -0
  39. data/lib/diaspora_federation/validators/account_deletion_validator.rb +10 -0
  40. data/lib/diaspora_federation/validators/comment_validator.rb +17 -0
  41. data/lib/diaspora_federation/validators/conversation_validator.rb +14 -0
  42. data/lib/diaspora_federation/validators/like_validator.rb +14 -0
  43. data/lib/diaspora_federation/validators/location_validator.rb +11 -0
  44. data/lib/diaspora_federation/validators/message_validator.rb +16 -0
  45. data/lib/diaspora_federation/validators/participation_validator.rb +16 -0
  46. data/lib/diaspora_federation/validators/photo_validator.rb +24 -0
  47. data/lib/diaspora_federation/validators/poll_answer_validator.rb +11 -0
  48. data/lib/diaspora_federation/validators/poll_participation_validator.rb +16 -0
  49. data/lib/diaspora_federation/validators/poll_validator.rb +11 -0
  50. data/lib/diaspora_federation/validators/relayable_retraction_validator.rb +15 -0
  51. data/lib/diaspora_federation/validators/relayable_validator.rb +14 -0
  52. data/lib/diaspora_federation/validators/request_validator.rb +11 -0
  53. data/lib/diaspora_federation/validators/reshare_validator.rb +18 -0
  54. data/lib/diaspora_federation/validators/retraction_validator.rb +14 -0
  55. data/lib/diaspora_federation/validators/rules/diaspora_id_count.rb +37 -0
  56. data/lib/diaspora_federation/validators/signed_retraction_validator.rb +15 -0
  57. data/lib/diaspora_federation/validators/status_message_validator.rb +14 -0
  58. data/lib/diaspora_federation/version.rb +1 -1
  59. metadata +49 -4
@@ -0,0 +1,191 @@
1
+ module DiasporaFederation
2
+ module Salmon
3
+ # Represents a Magic Envelope for Diaspora* federation messages.
4
+ #
5
+ # When generating a Magic Envelope, an instance of this class is created and
6
+ # the contents are specified on initialization. Optionally, the payload can be
7
+ # encrypted ({MagicEnvelope#encrypt!}), before the XML is returned
8
+ # ({MagicEnvelope#envelop}).
9
+ #
10
+ # The generated XML appears like so:
11
+ #
12
+ # <me:env>
13
+ # <me:data type="application/xml">{data}</me:data>
14
+ # <me:encoding>base64url</me:encoding>
15
+ # <me:alg>RSA-SHA256</me:alg>
16
+ # <me:sig>{signature}</me:sig>
17
+ # </me:env>
18
+ #
19
+ # When parsing the XML of an incoming Magic Envelope {MagicEnvelope.unenvelop}
20
+ # is used.
21
+ #
22
+ # @see http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-magicsig-01.html
23
+ class MagicEnvelope
24
+ # returns the payload (only used for testing purposes)
25
+ attr_reader :payload
26
+
27
+ # encoding used for the payload data
28
+ ENCODING = "base64url"
29
+
30
+ # algorithm used for signing the payload data
31
+ ALGORITHM = "RSA-SHA256"
32
+
33
+ # mime type describing the payload data
34
+ DATA_TYPE = "application/xml"
35
+
36
+ # digest instance used for signing
37
+ DIGEST = OpenSSL::Digest::SHA256.new
38
+
39
+ # XML namespace url
40
+ XMLNS = "http://salmon-protocol.org/ns/magic-env"
41
+
42
+ # Creates a new instance of MagicEnvelope.
43
+ #
44
+ # @param [OpenSSL::PKey::RSA] rsa_pkey private key used for signing
45
+ # @param [Entity] payload Entity instance
46
+ # @raise [ArgumentError] if either argument is not of the right type
47
+ def initialize(rsa_pkey, payload)
48
+ raise ArgumentError unless rsa_pkey.instance_of?(OpenSSL::PKey::RSA) &&
49
+ payload.is_a?(Entity)
50
+
51
+ @rsa_pkey = rsa_pkey
52
+ @payload = XmlPayload.pack(payload).to_xml.strip
53
+ end
54
+
55
+ # Builds the XML structure for the magic envelope, inserts the {ENCODING}
56
+ # encoded data and signs the envelope using {DIGEST}.
57
+ #
58
+ # @param [Nokogiri::XML::Builder] xml Salmon XML builder
59
+ def envelop(xml)
60
+ xml["me"].env {
61
+ xml["me"].data(Base64.urlsafe_encode64(@payload), type: DATA_TYPE)
62
+ xml["me"].encoding(ENCODING)
63
+ xml["me"].alg(ALGORITHM)
64
+ xml["me"].sig(Base64.urlsafe_encode64(signature))
65
+ }
66
+ end
67
+
68
+ # Encrypts the payload with a new, random AES cipher and returns the cipher
69
+ # params that were used.
70
+ #
71
+ # This must happen after the MagicEnvelope instance was created and before
72
+ # {MagicEnvelope#envelop} is called.
73
+ #
74
+ # @see AES#generate_key_and_iv
75
+ # @see AES#encrypt
76
+ #
77
+ # @return [Hash] AES key and iv. E.g.: { key: "...", iv: "..." }
78
+ def encrypt!
79
+ AES.generate_key_and_iv.tap do |key|
80
+ @payload = AES.encrypt(@payload, key[:key], key[:iv])
81
+ end
82
+ end
83
+
84
+ # Extracts the entity encoded in the magic envelope data, if the signature
85
+ # is valid. If +cipher_params+ is given, also attempts to decrypt the payload first.
86
+ #
87
+ # Does some sanity checking to avoid bad surprises...
88
+ #
89
+ # @see XmlPayload#unpack
90
+ # @see AES#decrypt
91
+ #
92
+ # @param [Nokogiri::XML::Element] magic_env XML root node of a magic envelope
93
+ # @param [OpenSSL::PKey::RSA] rsa_pubkey public key to verify the signature
94
+ # @param [Hash] cipher_params hash containing the key and iv for
95
+ # AES-decrypting previously encrypted data. E.g.: { iv: "...", key: "..." }
96
+ #
97
+ # @return [Entity] reconstructed entity instance
98
+ #
99
+ # @raise [ArgumentError] if any of the arguments is of invalid type
100
+ # @raise [InvalidEnvelope] if the envelope XML structure is malformed
101
+ # @raise [InvalidSignature] if the signature can't be verified
102
+ # @raise [InvalidEncoding] if the data is wrongly encoded
103
+ # @raise [InvalidAlgorithm] if the algorithm used doesn't match
104
+ def self.unenvelop(magic_env, rsa_pubkey, cipher_params=nil)
105
+ raise ArgumentError unless rsa_pubkey.instance_of?(OpenSSL::PKey::RSA) &&
106
+ magic_env.instance_of?(Nokogiri::XML::Element)
107
+
108
+ raise InvalidEnvelope unless envelope_valid?(magic_env)
109
+ raise InvalidSignature unless signature_valid?(magic_env, rsa_pubkey)
110
+
111
+ raise InvalidEncoding unless encoding_valid?(magic_env)
112
+ raise InvalidAlgorithm unless algorithm_valid?(magic_env)
113
+
114
+ data = read_and_decrypt_data(magic_env, cipher_params)
115
+
116
+ XmlPayload.unpack(Nokogiri::XML::Document.parse(data).root)
117
+ end
118
+
119
+ private
120
+
121
+ # create the signature for all fields according to specification
122
+ #
123
+ # @return [String] the signature
124
+ def signature
125
+ subject = self.class.sig_subject([@payload,
126
+ DATA_TYPE,
127
+ ENCODING,
128
+ ALGORITHM])
129
+ @rsa_pkey.sign(DIGEST, subject)
130
+ end
131
+
132
+ # @param [Nokogiri::XML::Element] env magic envelope XML
133
+ def self.envelope_valid?(env)
134
+ (env.instance_of?(Nokogiri::XML::Element) &&
135
+ env.name == "env" &&
136
+ !env.at_xpath("me:data").content.empty? &&
137
+ !env.at_xpath("me:encoding").content.empty? &&
138
+ !env.at_xpath("me:alg").content.empty? &&
139
+ !env.at_xpath("me:sig").content.empty?)
140
+ end
141
+ private_class_method :envelope_valid?
142
+
143
+ # @param [Nokogiri::XML::Element] env magic envelope XML
144
+ # @param [OpenSSL::PKey::RSA] pkey public key
145
+ # @return [Boolean]
146
+ def self.signature_valid?(env, pkey)
147
+ subject = sig_subject([Base64.urlsafe_decode64(env.at_xpath("me:data").content),
148
+ env.at_xpath("me:data")["type"],
149
+ env.at_xpath("me:encoding").content,
150
+ env.at_xpath("me:alg").content])
151
+
152
+ sig = Base64.urlsafe_decode64(env.at_xpath("me:sig").content)
153
+ pkey.verify(DIGEST, sig, subject)
154
+ end
155
+ private_class_method :signature_valid?
156
+
157
+ # constructs the signature subject.
158
+ # the given array should consist of the data, data_type (mimetype), encoding
159
+ # and the algorithm
160
+ # @param [Array<String>] data_arr
161
+ # @return [String] signature subject
162
+ def self.sig_subject(data_arr)
163
+ data_arr.map {|i| Base64.urlsafe_encode64(i) }.join(".")
164
+ end
165
+
166
+ # @param [Nokogiri::XML::Element] magic_env magic envelope XML
167
+ # @return [Boolean]
168
+ def self.encoding_valid?(magic_env)
169
+ magic_env.at_xpath("me:encoding").content == ENCODING
170
+ end
171
+ private_class_method :encoding_valid?
172
+
173
+ # @param [Nokogiri::XML::Element] magic_env magic envelope XML
174
+ # @return [Boolean]
175
+ def self.algorithm_valid?(magic_env)
176
+ magic_env.at_xpath("me:alg").content == ALGORITHM
177
+ end
178
+ private_class_method :algorithm_valid?
179
+
180
+ # @param [Nokogiri::XML::Element] magic_env magic envelope XML
181
+ # @param [Hash] cipher_params hash containing the key and iv
182
+ # @return [String] data
183
+ def self.read_and_decrypt_data(magic_env, cipher_params)
184
+ data = Base64.urlsafe_decode64(magic_env.at_xpath("me:data").content)
185
+ data = AES.decrypt(data, cipher_params[:key], cipher_params[:iv]) unless cipher_params.nil?
186
+ data
187
+ end
188
+ private_class_method :read_and_decrypt_data
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,128 @@
1
+ module DiasporaFederation
2
+ module Salmon
3
+ # +Slap+ provides class methods to create unencrypted Slap XML from payload
4
+ # data and parse incoming XML into a Slap instance.
5
+ #
6
+ # A Diaspora*-flavored magic-enveloped XML message looks like the following:
7
+ #
8
+ # <?xml version="1.0" encoding="UTF-8"?>
9
+ # <diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
10
+ # <header>
11
+ # <author_id>{author}</author_id>
12
+ # </header>
13
+ # {magic_envelope}
14
+ # </diaspora>
15
+ #
16
+ # @example Generating a Salmon Slap
17
+ # author_id = "author@pod.example.tld"
18
+ # author_privkey = however_you_retrieve_the_authors_private_key(author_id)
19
+ # entity = YourEntity.new(attr: "val")
20
+ #
21
+ # slap_xml = Slap.generate_xml(author_id, author_privkey, entity)
22
+ #
23
+ # @example Parsing a Salmon Slap
24
+ # slap = Slap.from_xml(slap_xml)
25
+ # author_pubkey = however_you_retrieve_the_authors_public_key(slap.author_id)
26
+ #
27
+ # entity = slap.entity(author_pubkey)
28
+ class Slap
29
+ # the author of the slap
30
+ # @overload author_id
31
+ # @return [String] the author diaspora id
32
+ # @overload author_id=
33
+ # @param [String] the author diaspora id
34
+ attr_accessor :author_id
35
+
36
+ # the key and iv if it is an encrypted slap
37
+ # @param [Hash] value hash containing the key and iv
38
+ attr_writer :cipher_params
39
+
40
+ # Namespaces
41
+ NS = {d: Salmon::XMLNS, me: MagicEnvelope::XMLNS}
42
+
43
+ # Returns new instance of the Entity that is contained within the XML of
44
+ # this Slap.
45
+ #
46
+ # The first time this is called, a public key has to be specified to verify
47
+ # the Magic Envelope signature. On repeated calls, the key may be omitted.
48
+ #
49
+ # @see MagicEnvelope.unenvelop
50
+ #
51
+ # @param [OpenSSL::PKey::RSA] pubkey public key for validating the signature
52
+ # @return [Entity] entity instance from the XML
53
+ # @raise [ArgumentError] if the public key is of the wrong type
54
+ def entity(pubkey=nil)
55
+ return @entity unless @entity.nil?
56
+
57
+ raise ArgumentError unless pubkey.instance_of?(OpenSSL::PKey::RSA)
58
+ @entity = MagicEnvelope.unenvelop(@magic_envelope, pubkey, @cipher_params)
59
+ @entity
60
+ end
61
+
62
+ # Parses an unencrypted Salmon XML string and returns a new instance of
63
+ # {Slap} populated with the XML data.
64
+ #
65
+ # @param [String] slap_xml Salmon XML
66
+ # @return [Slap] new Slap instance
67
+ # @raise [ArgumentError] if the argument is not a String
68
+ # @raise [MissingAuthor] if the +author_id+ element is missing from the XML
69
+ # @raise [MissingMagicEnvelope] if the +me:env+ element is missing from the XML
70
+ def self.from_xml(slap_xml)
71
+ raise ArgumentError unless slap_xml.instance_of?(String)
72
+ doc = Nokogiri::XML::Document.parse(slap_xml)
73
+
74
+ Slap.new.tap do |slap|
75
+ author_elem = doc.at_xpath("d:diaspora/d:header/d:author_id", Slap::NS)
76
+ raise MissingAuthor if author_elem.nil? || author_elem.content.empty?
77
+ slap.author_id = author_elem.content
78
+
79
+ slap.add_magic_env_from_doc(doc)
80
+ end
81
+ end
82
+
83
+ # Creates an unencrypted Salmon Slap and returns the XML string.
84
+ #
85
+ # @param [String] author_id Diaspora* handle of the author
86
+ # @param [OpenSSL::PKey::RSA] pkey sender private_key for signing the magic envelope
87
+ # @param [Entity] entity payload
88
+ # @return [String] Salmon XML string
89
+ # @raise [ArgumentError] if any of the arguments is not the correct type
90
+ def self.generate_xml(author_id, pkey, entity)
91
+ raise ArgumentError unless author_id.instance_of?(String) &&
92
+ pkey.instance_of?(OpenSSL::PKey::RSA) &&
93
+ entity.is_a?(Entity)
94
+
95
+ build_xml do |xml|
96
+ xml.header {
97
+ xml.author_id(author_id)
98
+ }
99
+
100
+ MagicEnvelope.new(pkey, entity).envelop(xml)
101
+ end
102
+ end
103
+
104
+ # Builds the xml for the Salmon Slap.
105
+ #
106
+ # @yield [xml] Invokes the block with the
107
+ # {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Builder Nokogiri::XML::Builder}
108
+ # @return [String] Slap XML
109
+ def self.build_xml
110
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
111
+ xml.diaspora("xmlns" => Salmon::XMLNS, "xmlns:me" => MagicEnvelope::XMLNS) {
112
+ yield xml
113
+ }
114
+ end
115
+ builder.to_xml
116
+ end
117
+
118
+ # Parses the magic envelop from the document.
119
+ #
120
+ # @param [Nokogiri::XML::Document] doc Salmon XML Document
121
+ def add_magic_env_from_doc(doc)
122
+ @magic_envelope = doc.at_xpath("d:diaspora/me:env", Slap::NS).tap do |env|
123
+ raise MissingMagicEnvelope if env.nil?
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,158 @@
1
+ module DiasporaFederation
2
+ module Salmon
3
+ # +XmlPayload+ provides methods to wrap a XML-serialized {Entity} inside a
4
+ # common XML structure that will become the payload for federation messages.
5
+ #
6
+ # The wrapper looks like so:
7
+ # <XML>
8
+ # <post>
9
+ # {data}
10
+ # </post>
11
+ # </XML>
12
+ #
13
+ # (The +post+ element is there for historic reasons...)
14
+ module XmlPayload
15
+ # Encapsulates an Entity inside the wrapping xml structure
16
+ # and returns the XML Object.
17
+ #
18
+ # @param [Entity] entity subject
19
+ # @return [Nokogiri::XML::Element] XML root node
20
+ # @raise [ArgumentError] if the argument is not an Entity subclass
21
+ def self.pack(entity)
22
+ raise ArgumentError, "only instances of DiasporaFederation::Entity allowed" unless entity.is_a?(Entity)
23
+
24
+ entity_xml = entity.to_xml
25
+ doc = entity_xml.document
26
+ wrap = Nokogiri::XML::Element.new("XML", doc)
27
+ wrap_post = Nokogiri::XML::Element.new("post", doc)
28
+ entity_xml.parent = wrap_post
29
+ wrap << wrap_post
30
+
31
+ wrap
32
+ end
33
+
34
+ # Extracts the Entity XML from the wrapping XML structure, parses the entity
35
+ # XML and returns a new instance of the Entity that was packed inside the
36
+ # given payload.
37
+ #
38
+ # @param [Nokogiri::XML::Element] xml payload XML root node
39
+ # @return [Entity] re-constructed Entity instance
40
+ # @raise [ArgumentError] if the argument is not an
41
+ # {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Element Nokogiri::XML::Element}
42
+ # @raise [InvalidStructure] if the XML doesn't look like the wrapper XML
43
+ # @raise [UnknownEntity] if the class for the entity contained inside the
44
+ # XML can't be found
45
+ def self.unpack(xml)
46
+ raise ArgumentError, "only Nokogiri::XML::Element allowed" unless xml.instance_of?(Nokogiri::XML::Element)
47
+ raise Salmon::InvalidStructure unless wrap_valid?(xml)
48
+
49
+ data = xml.at_xpath("post/*[1]")
50
+ klass_name = entity_class_name(data.name)
51
+ raise Salmon::UnknownEntity, "'#{klass_name}' not found" unless Entities.const_defined?(klass_name)
52
+
53
+ klass = Entities.const_get(klass_name)
54
+ populate_entity(klass, data)
55
+ end
56
+
57
+ private
58
+
59
+ # @param [Nokogiri::XML::Element] element
60
+ def self.wrap_valid?(element)
61
+ (element.name == "XML" && !element.at_xpath("post").nil? &&
62
+ !element.at_xpath("post").children.empty?)
63
+ end
64
+ private_class_method :wrap_valid?
65
+
66
+ # Transform the given String from the lowercase underscored version to a
67
+ # camelized variant, used later for getting the Class constant.
68
+ #
69
+ # @param [String] term "snake_case" class name
70
+ # @return [String] "CamelCase" class name
71
+ def self.entity_class_name(term)
72
+ term.to_s.tap do |string|
73
+ raise Salmon::InvalidEntityName, "'#{string}' is invalid" unless string =~ /^[a-z]*(_[a-z]*)*$/
74
+ string.sub!(/^[a-z]/, &:upcase)
75
+ string.gsub!(/_([a-z])/) { Regexp.last_match[1].upcase }
76
+ end
77
+ end
78
+ private_class_method :entity_class_name
79
+
80
+ # Construct a new instance of the given Entity and populate the properties
81
+ # with the attributes found in the XML.
82
+ # Works recursively on nested Entities and Arrays thereof.
83
+ #
84
+ # @param [Class] klass entity class
85
+ # @param [Nokogiri::XML::Element] root_node xml nodes
86
+ # @return [Entity] instance
87
+ def self.populate_entity(klass, root_node)
88
+ # Use all known properties to build the Entity. All other elements are respected
89
+ # and attached to resulted hash as string. It is intended to build a hash
90
+ # invariable of an Entity definition, in order to support receiving objects
91
+ # from the future versions of Diaspora, where new elements may have been added.
92
+ data = Hash[root_node.element_children.map { |child|
93
+ xml_name = child.name
94
+ property = klass.class_props.find {|prop| prop[:xml_name].to_s == xml_name }
95
+ if property
96
+ parse_element_from_node(property[:name], property[:type], xml_name, root_node)
97
+ else
98
+ [xml_name, child.text]
99
+ end
100
+ }]
101
+
102
+ Entities::Relayable.verify_signatures(data) if klass.included_modules.include?(Entities::Relayable)
103
+
104
+ klass.new(data)
105
+ end
106
+ private_class_method :populate_entity
107
+
108
+ # @param [Symbol] name property name
109
+ # @param [Class] type target type to parse
110
+ # @param [String] xml_name xml tag to parse
111
+ # @param [Nokogiri::XML::Element] node XML node to parse
112
+ # @return [Array<Symbol, Object>] parsed data
113
+ def self.parse_element_from_node(name, type, xml_name, node)
114
+ if type == String
115
+ [name, parse_string_from_node(xml_name, node)]
116
+ elsif type.instance_of?(Array)
117
+ [name, parse_array_from_node(type, node)]
118
+ elsif type.ancestors.include?(Entity)
119
+ [name, parse_entity_from_node(type, node)]
120
+ end
121
+ end
122
+ private_class_method :parse_element_from_node
123
+
124
+ # create simple entry in data hash
125
+ #
126
+ # @param [String] name xml tag to parse
127
+ # @param [Nokogiri::XML::Element] root_node XML root_node to parse
128
+ # @return [String] data
129
+ def self.parse_string_from_node(name, root_node)
130
+ node = root_node.xpath(name.to_s)
131
+ node.first.text if node.any?
132
+ end
133
+ private_class_method :parse_string_from_node
134
+
135
+ # create an entry in the data hash for the nested entity
136
+ #
137
+ # @param [Class] type target type to parse
138
+ # @param [Nokogiri::XML::Element] root_node XML node to parse
139
+ # @return [Entity] parsed child entity
140
+ def self.parse_entity_from_node(type, root_node)
141
+ node = root_node.xpath(type.entity_name)
142
+ populate_entity(type, node.first) if node.any?
143
+ end
144
+ private_class_method :parse_entity_from_node
145
+
146
+ # collect all nested children of that type and create an array in the data hash
147
+ #
148
+ # @param [Array<Class>] type target type to parse
149
+ # @param [Nokogiri::XML::Element] root_node XML node to parse
150
+ # @return [Array<Entity>] array with parsed child entities
151
+ def self.parse_array_from_node(type, root_node)
152
+ node = root_node.xpath(type.first.entity_name)
153
+ node.map {|child| populate_entity(type.first, child) }
154
+ end
155
+ private_class_method :parse_array_from_node
156
+ end
157
+ end
158
+ end