diaspora_federation 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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