diaspora_federation 0.0.12 → 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/lib/diaspora_federation.rb +103 -18
  3. data/lib/diaspora_federation/discovery/discovery.rb +1 -1
  4. data/lib/diaspora_federation/discovery/h_card.rb +4 -5
  5. data/lib/diaspora_federation/discovery/host_meta.rb +1 -1
  6. data/lib/diaspora_federation/discovery/web_finger.rb +8 -8
  7. data/lib/diaspora_federation/discovery/xrd_document.rb +6 -7
  8. data/lib/diaspora_federation/entities.rb +21 -10
  9. data/lib/diaspora_federation/entities/account_deletion.rb +7 -3
  10. data/lib/diaspora_federation/entities/comment.rb +13 -10
  11. data/lib/diaspora_federation/entities/contact.rb +29 -0
  12. data/lib/diaspora_federation/entities/conversation.rb +5 -6
  13. data/lib/diaspora_federation/entities/like.rb +10 -18
  14. data/lib/diaspora_federation/entities/message.rb +6 -12
  15. data/lib/diaspora_federation/entities/participation.rb +8 -16
  16. data/lib/diaspora_federation/entities/person.rb +6 -2
  17. data/lib/diaspora_federation/entities/photo.rb +3 -3
  18. data/lib/diaspora_federation/entities/poll_participation.rb +6 -12
  19. data/lib/diaspora_federation/entities/post.rb +37 -0
  20. data/lib/diaspora_federation/entities/profile.rb +7 -3
  21. data/lib/diaspora_federation/entities/relayable.rb +169 -65
  22. data/lib/diaspora_federation/entities/relayable_retraction.rb +33 -32
  23. data/lib/diaspora_federation/entities/request.rb +20 -6
  24. data/lib/diaspora_federation/entities/reshare.rb +5 -27
  25. data/lib/diaspora_federation/entities/retraction.rb +6 -6
  26. data/lib/diaspora_federation/entities/signed_retraction.rb +32 -26
  27. data/lib/diaspora_federation/entities/status_message.rb +2 -22
  28. data/lib/diaspora_federation/entity.rb +137 -38
  29. data/lib/diaspora_federation/federation.rb +9 -0
  30. data/lib/diaspora_federation/federation/fetcher.rb +26 -0
  31. data/lib/diaspora_federation/federation/receiver.rb +41 -0
  32. data/lib/diaspora_federation/federation/receiver/abstract_receiver.rb +35 -0
  33. data/lib/diaspora_federation/federation/receiver/exceptions.rb +13 -0
  34. data/lib/diaspora_federation/federation/receiver/private.rb +15 -0
  35. data/lib/diaspora_federation/federation/receiver/public.rb +9 -0
  36. data/lib/diaspora_federation/federation/sender.rb +33 -0
  37. data/lib/diaspora_federation/federation/sender/hydra_wrapper.rb +92 -0
  38. data/lib/diaspora_federation/{fetcher.rb → http_client.rb} +6 -6
  39. data/lib/diaspora_federation/properties_dsl.rb +51 -14
  40. data/lib/diaspora_federation/salmon.rb +2 -1
  41. data/lib/diaspora_federation/salmon/aes.rb +1 -1
  42. data/lib/diaspora_federation/salmon/encrypted_magic_envelope.rb +61 -0
  43. data/lib/diaspora_federation/salmon/encrypted_slap.rb +69 -50
  44. data/lib/diaspora_federation/salmon/exceptions.rb +8 -14
  45. data/lib/diaspora_federation/salmon/magic_envelope.rb +80 -39
  46. data/lib/diaspora_federation/salmon/slap.rb +20 -51
  47. data/lib/diaspora_federation/salmon/xml_payload.rb +5 -104
  48. data/lib/diaspora_federation/validators.rb +22 -16
  49. data/lib/diaspora_federation/validators/account_deletion_validator.rb +1 -1
  50. data/lib/diaspora_federation/validators/comment_validator.rb +0 -4
  51. data/lib/diaspora_federation/validators/contact_validator.rb +13 -0
  52. data/lib/diaspora_federation/validators/conversation_validator.rb +2 -2
  53. data/lib/diaspora_federation/validators/like_validator.rb +1 -3
  54. data/lib/diaspora_federation/validators/message_validator.rb +0 -4
  55. data/lib/diaspora_federation/validators/participation_validator.rb +1 -5
  56. data/lib/diaspora_federation/validators/person_validator.rb +1 -1
  57. data/lib/diaspora_federation/validators/photo_validator.rb +2 -2
  58. data/lib/diaspora_federation/validators/poll_participation_validator.rb +0 -4
  59. data/lib/diaspora_federation/validators/profile_validator.rb +1 -1
  60. data/lib/diaspora_federation/validators/relayable_retraction_validator.rb +1 -1
  61. data/lib/diaspora_federation/validators/relayable_validator.rb +2 -0
  62. data/lib/diaspora_federation/validators/request_validator.rb +3 -2
  63. data/lib/diaspora_federation/validators/reshare_validator.rb +3 -3
  64. data/lib/diaspora_federation/validators/retraction_validator.rb +2 -2
  65. data/lib/diaspora_federation/validators/rules/guid.rb +16 -7
  66. data/lib/diaspora_federation/validators/signed_retraction_validator.rb +1 -1
  67. data/lib/diaspora_federation/validators/status_message_validator.rb +2 -2
  68. data/lib/diaspora_federation/version.rb +1 -1
  69. metadata +20 -11
  70. data/lib/diaspora_federation/receiver.rb +0 -28
  71. data/lib/diaspora_federation/receiver/private.rb +0 -19
  72. data/lib/diaspora_federation/receiver/public.rb +0 -13
  73. data/lib/diaspora_federation/signing.rb +0 -56
@@ -38,11 +38,11 @@ module DiasporaFederation
38
38
  # @return [String] target type
39
39
  property :target_type
40
40
 
41
- # @!attribute [r] diaspora_id
41
+ # @!attribute [r] author
42
42
  # The diaspora ID of the person who deletes a relayable
43
- # @see Person#diaspora_id
43
+ # @see Person#author
44
44
  # @return [String] diaspora ID
45
- property :diaspora_id, xml_name: :sender_handle
45
+ property :author, xml_name: :sender_handle
46
46
 
47
47
  # @!attribute [r] target_author_signature
48
48
  # Contains a signature of the entity using the private key of the
@@ -53,43 +53,44 @@ module DiasporaFederation
53
53
  # @return [String] target author signature
54
54
  property :target_author_signature, default: nil
55
55
 
56
- # Generates XML and updates signatures
57
- # @see Entity#to_xml
58
- # @return [Nokogiri::XML::Element] root element containing properties as child elements
59
- def to_xml
60
- entity_xml.tap do |xml|
61
- hash = to_h
62
- RelayableRetraction.update_signatures!(hash)
63
- xml.at_xpath("target_author_signature").content = hash[:target_author_signature]
64
- xml.at_xpath("parent_author_signature").content = hash[:parent_author_signature]
65
- end
56
+ # use only {Retraction} for receive
57
+ # @return [Retraction] instance as normal retraction
58
+ def to_retraction
59
+ Retraction.new(author: author, target_guid: target_guid, target_type: target_type)
60
+ end
61
+
62
+ private
63
+
64
+ # @param [Nokogiri::XML::Element] root_node xml nodes
65
+ # @return [Retraction] instance
66
+ def self.populate_entity(root_node)
67
+ super(root_node).to_retraction
66
68
  end
69
+ private_class_method :populate_entity
67
70
 
68
- # Adds signatures to a given hash with the keys of the author and the parent
69
- # if the signatures are not in the hash yet and if the keys are available.
71
+ # It updates also the signatures with the keys of the author and the parent
72
+ # if the signatures are not there yet and if the keys are available.
70
73
  #
71
- # @param [Hash] hash hash given for a signing
72
- def self.update_signatures!(hash)
73
- target_author = DiasporaFederation.callbacks.trigger(
74
- :fetch_entity_author_id_by_guid,
75
- hash[:target_type],
76
- hash[:target_guid]
77
- )
78
- pkey = DiasporaFederation.callbacks.trigger(:fetch_private_key_by_diaspora_id, hash[:diaspora_id])
74
+ # @return [Hash] xml elements with updated signatures
75
+ def xml_elements
76
+ target_author = DiasporaFederation.callbacks.trigger(:fetch_entity_author_id_by_guid, target_type, target_guid)
77
+ privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key_by_diaspora_id, author)
79
78
 
80
- fill_required_signature(target_author, pkey, hash) unless pkey.nil?
79
+ super.tap do |xml_elements|
80
+ fill_required_signature(target_author, privkey, xml_elements) unless privkey.nil?
81
+ end
81
82
  end
82
83
 
83
- def self.fill_required_signature(target_author, pkey, hash)
84
- if target_author == hash[:diaspora_id] && hash[:target_author_signature].nil?
85
- hash[:target_author_signature] =
86
- Signing.sign_with_key(SignedRetraction.apply_signable_exceptions(hash), pkey)
87
- elsif target_author != hash[:diaspora_id] && hash[:parent_author_signature].nil?
88
- hash[:parent_author_signature] =
89
- Signing.sign_with_key(SignedRetraction.apply_signable_exceptions(hash), pkey)
84
+ # @param [String] target_author the author of the entity to retract
85
+ # @param [OpenSSL::PKey::RSA] privkey private key of sender
86
+ # @param [Hash] hash hash given for a signing
87
+ def fill_required_signature(target_author, privkey, hash)
88
+ if target_author == author && target_author_signature.nil?
89
+ hash[:target_author_signature] = SignedRetraction.sign_with_key(privkey, self)
90
+ elsif target_author != author && parent_author_signature.nil?
91
+ hash[:parent_author_signature] = SignedRetraction.sign_with_key(privkey, self)
90
92
  end
91
93
  end
92
- private_class_method :fill_required_signature
93
94
  end
94
95
  end
95
96
  end
@@ -4,18 +4,32 @@ module DiasporaFederation
4
4
  # when he starts sharing with another user.
5
5
  #
6
6
  # @see Validators::RequestValidator
7
+ # @deprecated will be replaced with {Contact}
7
8
  class Request < Entity
8
- # @!attribute [r] sender_id
9
+ # @!attribute [r] author
9
10
  # The diaspora ID of the person who shares his profile
10
- # @see Person#diaspora_id
11
+ # @see Person#author
11
12
  # @return [String] sender ID
12
- property :sender_id, xml_name: :sender_handle
13
+ property :author, xml_name: :sender_handle
13
14
 
14
- # @!attribute [r] recipient_id
15
+ # @!attribute [r] recipient
15
16
  # The diaspora ID of the person who will be shared with
16
- # @see Person#diaspora_id
17
+ # @see Validation::Rule::DiasporaId
17
18
  # @return [String] recipient ID
18
- property :recipient_id, xml_name: :recipient_handle
19
+ property :recipient, xml_name: :recipient_handle
20
+
21
+ # use only {Contact} for receive
22
+ # @return [Contact] instance as contact
23
+ def to_contact
24
+ Contact.new(author: author, recipient: recipient)
25
+ end
26
+
27
+ # @param [Nokogiri::XML::Element] root_node xml nodes
28
+ # @return [Retraction] instance
29
+ def self.populate_entity(root_node)
30
+ super(root_node).to_contact
31
+ end
32
+ private_class_method :populate_entity
19
33
  end
20
34
  end
21
35
  end
@@ -4,11 +4,13 @@ module DiasporaFederation
4
4
  #
5
5
  # @see Validators::ReshareValidator
6
6
  class Reshare < Entity
7
- # @!attribute [r] diaspora_id
7
+ include Post
8
+
9
+ # @!attribute [r] root_author
8
10
  # The diaspora ID of the person who posted the original post
9
- # @see Person#diaspora_id
11
+ # @see Person#author
10
12
  # @return [String] diaspora ID
11
- property :root_diaspora_id # inconsistent, everywhere else it's "handle"
13
+ property :root_author, xml_name: :root_diaspora_id
12
14
 
13
15
  # @!attribute [r] root_guid
14
16
  # guid of the original post
@@ -16,34 +18,10 @@ module DiasporaFederation
16
18
  # @return [String] root guid
17
19
  property :root_guid
18
20
 
19
- # @!attribute [r] guid
20
- # a random string of at least 16 chars.
21
- # @see Validation::Rule::Guid
22
- # @see StatusMessage#guid
23
- # @return [String] guid
24
- property :guid
25
-
26
- # @!attribute [r] diaspora_id
27
- # The diaspora ID of the person who reshares a post
28
- # @see Person#diaspora_id
29
- # @return [String] diaspora ID
30
- property :diaspora_id, xml_name: :diaspora_handle
31
-
32
21
  # @!attribute [r] public
33
22
  # has no meaning at the moment
34
23
  # @return [Boolean] public
35
24
  property :public, default: true # always true? (we only reshare public posts)
36
-
37
- # @!attribute [r] created_at
38
- # reshare entity creation time
39
- # @return [Time] creation time
40
- property :created_at, default: -> { Time.now.utc }
41
-
42
- # @!attribute [r] provider_display_name
43
- # a string that describes a means by which a user has posted the reshare
44
- # @see StatusMessage#provider_display_name
45
- # @return [String] provider display name
46
- property :provider_display_name, default: nil
47
25
  end
48
26
  end
49
27
  end
@@ -4,17 +4,17 @@ module DiasporaFederation
4
4
  #
5
5
  # @see Validators::RetractionValidator
6
6
  class Retraction < Entity
7
+ # @!attribute [r] author
8
+ # The diaspora ID of the person who deletes the entity
9
+ # @see Person#author
10
+ # @return [String] diaspora ID
11
+ property :author, xml_name: :diaspora_handle
12
+
7
13
  # @!attribute [r] target_guid
8
14
  # guid of the entity to be deleted
9
15
  # @return [String] target guid
10
16
  property :target_guid, xml_name: :post_guid
11
17
 
12
- # @!attribute [r] diaspora_id
13
- # The diaspora ID of the person who deletes the entity
14
- # @see Person#diaspora_id
15
- # @return [String] diaspora ID
16
- property :diaspora_id, xml_name: :diaspora_handle
17
-
18
18
  # @!attribute [r] target_type
19
19
  # A string describing the type of the target.
20
20
  # @return [String] target type
@@ -18,11 +18,11 @@ module DiasporaFederation
18
18
  # @return [String] target type
19
19
  property :target_type
20
20
 
21
- # @!attribute [r] diaspora_id
21
+ # @!attribute [r] author
22
22
  # The diaspora ID of the person who deletes a post
23
- # @see Person#diaspora_id
23
+ # @see Person#author
24
24
  # @return [String] diaspora ID
25
- property :diaspora_id, xml_name: :sender_handle
25
+ property :author, xml_name: :sender_handle
26
26
 
27
27
  # @!attribute [r] author_signature
28
28
  # Contains a signature of the entity using the private key of the author of a post
@@ -30,36 +30,42 @@ module DiasporaFederation
30
30
  # @return [String] author signature
31
31
  property :target_author_signature, default: nil
32
32
 
33
- # Generates XML and updates signatures
34
- # @see Entity#to_xml
35
- # @return [Nokogiri::XML::Element] root element containing properties as child elements
36
- def to_xml
37
- entity_xml.tap do |xml|
38
- hash = to_h
39
- SignedRetraction.update_signatures!(hash)
33
+ # use only {Retraction} for receive
34
+ # @return [Retraction] instance as normal retraction
35
+ def to_retraction
36
+ Retraction.new(author: author, target_guid: target_guid, target_type: target_type)
37
+ end
40
38
 
41
- xml.at_xpath("target_author_signature").content = hash[:target_author_signature]
42
- end
39
+ # Create signature for a retraction
40
+ # @param [OpenSSL::PKey::RSA] privkey private key of sender
41
+ # @param [SignedRetraction, RelayableRetraction] ret the retraction to sign
42
+ # @return [String] a Base64 encoded signature of the retraction with the key
43
+ def self.sign_with_key(privkey, ret)
44
+ Base64.strict_encode64(privkey.sign(Relayable::DIGEST, [ret.target_guid, ret.target_type].join(";")))
45
+ end
46
+
47
+ private
48
+
49
+ # @param [Nokogiri::XML::Element] root_node xml nodes
50
+ # @return [Retraction] instance
51
+ def self.populate_entity(root_node)
52
+ super(root_node).to_retraction
43
53
  end
54
+ private_class_method :populate_entity
44
55
 
45
- # Adds signature to a given hash with the key of the author
46
- # if the signature is not in the hash yet and if the key is available.
56
+ # It updates also the signatures with the keys of the author and the parent
57
+ # if the signatures are not there yet and if the keys are available.
47
58
  #
48
- # @param [Hash] data hash given for a signing
49
- def self.update_signatures!(data)
50
- if data[:target_author_signature].nil?
51
- pkey = DiasporaFederation.callbacks.trigger(:fetch_private_key_by_diaspora_id, data[:diaspora_id])
52
- data[:target_author_signature] = Signing.sign_with_key(apply_signable_exceptions(data), pkey) unless pkey.nil?
59
+ # @return [Hash] xml elements with updated signatures
60
+ def xml_elements
61
+ super.tap do |xml_elements|
62
+ xml_elements[:target_author_signature] = target_author_signature || sign_with_author.to_s
53
63
  end
54
64
  end
55
65
 
56
- # Deletes :diaspora_id (xml_name: sender_handle) from the hash in order to compute
57
- # a signature since it is included from signable_string for SignedRetraction and RelayableRetraction
58
- #
59
- # @param [Hash] data hash of the retraction properties
60
- # @return [Hash] hash copy without :diaspora_id member
61
- def self.apply_signable_exceptions(data)
62
- data.dup.tap {|data| data.delete(:diaspora_id) }
66
+ def sign_with_author
67
+ privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key_by_diaspora_id, author)
68
+ SignedRetraction.sign_with_key(privkey, self) unless privkey.nil?
63
69
  end
64
70
  end
65
71
  end
@@ -4,6 +4,8 @@ module DiasporaFederation
4
4
  #
5
5
  # @see Validators::StatusMessageValidator
6
6
  class StatusMessage < Entity
7
+ include Post
8
+
7
9
  # @!attribute [r] raw_message
8
10
  # text of the status message composed by the user
9
11
  # @return [String] text of the status message
@@ -24,32 +26,10 @@ module DiasporaFederation
24
26
  # @return [Entities::Poll] poll
25
27
  entity :poll, Entities::Poll, default: nil
26
28
 
27
- # @!attribute [r] guid
28
- # a random string of at least 16 chars.
29
- # @see Validation::Rule::Guid
30
- # @return [String] status message guid
31
- property :guid
32
-
33
- # @!attribute [r] diaspora_id
34
- # The diaspora ID of the person who posts the status message
35
- # @see Person#diaspora_id
36
- # @return [String] diaspora ID
37
- property :diaspora_id, xml_name: :diaspora_handle
38
-
39
29
  # @!attribute [r] public
40
30
  # shows whether the status message is visible to everyone or only to some aspects
41
31
  # @return [Boolean] is it public
42
32
  property :public, default: false
43
-
44
- # @!attribute [r] created_at
45
- # status message entity creation time
46
- # @return [Time] creation time
47
- property :created_at, default: -> { Time.now.utc }
48
-
49
- # @!attribute [r] provider_display_name
50
- # a string that describes a means by which a user has posted the status message
51
- # @return [String] provider display name
52
- property :provider_display_name, default: nil
53
33
  end
54
34
  end
55
35
  end
@@ -47,17 +47,18 @@ module DiasporaFederation
47
47
  # @note Attributes not defined as part of the class definition ({PropertiesDSL#property},
48
48
  # {PropertiesDSL#entity}) get discarded silently.
49
49
  #
50
- # @param [Hash] data
50
+ # @param [Hash] data entity data
51
51
  # @return [Entity] new instance
52
52
  def initialize(data)
53
53
  raise ArgumentError, "expected a Hash" unless data.is_a?(Hash)
54
- missing_props = self.class.missing_props(data)
54
+ entity_data = self.class.resolv_aliases(data)
55
+ missing_props = self.class.missing_props(entity_data)
55
56
  unless missing_props.empty?
56
57
  raise ArgumentError, "missing required properties: #{missing_props.join(', ')}"
57
58
  end
58
59
 
59
- self.class.default_values.merge(data).each do |k, v|
60
- instance_variable_set("@#{k}", nilify(v)) if setable?(k, v)
60
+ self.class.default_values.merge(entity_data).each do |name, value|
61
+ instance_variable_set("@#{name}", nilify(value)) if setable?(name, value)
61
62
  end
62
63
 
63
64
  freeze
@@ -67,7 +68,7 @@ module DiasporaFederation
67
68
  # Returns a Hash representing this Entity (attributes => values)
68
69
  # @return [Hash] entity data (mostly equal to the hash used for initialization).
69
70
  def to_h
70
- self.class.class_prop_names.each_with_object({}) do |prop, hash|
71
+ self.class.class_props.keys.each_with_object({}) do |prop, hash|
71
72
  hash[prop] = public_send(prop)
72
73
  end
73
74
  end
@@ -80,10 +81,31 @@ module DiasporaFederation
80
81
  #
81
82
  # @return [Nokogiri::XML::Element] root element containing properties as child elements
82
83
  def to_xml
83
- entity_xml
84
+ doc = Nokogiri::XML::DocumentFragment.new(Nokogiri::XML::Document.new)
85
+ Nokogiri::XML::Element.new(self.class.entity_name, doc).tap do |root_element|
86
+ xml_elements.each do |name, value|
87
+ add_property_to_xml(doc, root_element, name, value)
88
+ end
89
+ end
90
+ end
91
+
92
+ # Construct a new instance of the given Entity and populate the properties
93
+ # with the attributes found in the XML.
94
+ # Works recursively on nested Entities and Arrays thereof.
95
+ #
96
+ # @param [Nokogiri::XML::Element] root_node xml nodes
97
+ # @return [Entity] instance
98
+ def self.from_xml(root_node)
99
+ raise ArgumentError, "only Nokogiri::XML::Element allowed" unless root_node.instance_of?(Nokogiri::XML::Element)
100
+ raise InvalidRootNode, "'#{root_node.name}' can't be parsed by #{name}" unless root_node.name == entity_name
101
+
102
+ populate_entity(root_node)
84
103
  end
85
104
 
86
105
  # Makes an underscored, lowercase form of the class name
106
+ #
107
+ # @see .entity_class
108
+ #
87
109
  # @return [String] entity name
88
110
  def self.entity_name
89
111
  name.rpartition("::").last.tap do |word|
@@ -92,29 +114,42 @@ module DiasporaFederation
92
114
  end
93
115
  end
94
116
 
117
+ # Transform the given String from the lowercase underscored version to a
118
+ # camelized variant and returns the Class constant.
119
+ #
120
+ # @see .entity_name
121
+ #
122
+ # @param [String] entity_name "snake_case" class name
123
+ # @return [Class] entity class
124
+ def self.entity_class(entity_name)
125
+ raise InvalidEntityName, "'#{entity_name}' is invalid" unless entity_name =~ /^[a-z]*(_[a-z]*)*$/
126
+ class_name = entity_name.sub(/^[a-z]/, &:upcase)
127
+ class_name.gsub!(/_([a-z])/) { Regexp.last_match[1].upcase }
128
+
129
+ raise UnknownEntity, "'#{class_name}' not found" unless Entities.const_defined?(class_name)
130
+
131
+ Entities.const_get(class_name)
132
+ end
133
+
95
134
  private
96
135
 
97
136
  def setable?(name, val)
98
- prop_def = self.class.class_props.find {|p| p[:name] == name }
99
- return false if prop_def.nil? # property undefined
137
+ type = self.class.class_props[name]
138
+ return false if type.nil? # property undefined
100
139
 
101
- setable_string?(prop_def, val) || setable_nested?(prop_def, val) || setable_multi?(prop_def, val)
140
+ setable_string?(type, val) || setable_nested?(type, val) || setable_multi?(type, val)
102
141
  end
103
142
 
104
- def setable_string?(definition, val)
105
- (definition[:type] == String && val.respond_to?(:to_s))
143
+ def setable_string?(type, val)
144
+ type == String && val.respond_to?(:to_s)
106
145
  end
107
146
 
108
- def setable_nested?(definition, val)
109
- t = definition[:type]
110
- (t.is_a?(Class) && t.ancestors.include?(Entity) && val.is_a?(Entity))
147
+ def setable_nested?(type, val)
148
+ type.is_a?(Class) && type.ancestors.include?(Entity) && val.is_a?(Entity)
111
149
  end
112
150
 
113
- def setable_multi?(definition, val)
114
- t = definition[:type]
115
- (t.instance_of?(Array) &&
116
- val.instance_of?(Array) &&
117
- val.all? {|v| v.instance_of?(t.first) })
151
+ def setable_multi?(type, val)
152
+ type.instance_of?(Array) && val.instance_of?(Array) && val.all? {|v| v.instance_of?(type.first) }
118
153
  end
119
154
 
120
155
  def nilify(value)
@@ -138,40 +173,104 @@ module DiasporaFederation
138
173
  "Failed validation for properties: #{errors.join(' | ')}"
139
174
  end
140
175
 
141
- # Serialize the Entity into XML elements
142
- # @return [Nokogiri::XML::Element] root node
143
- def entity_xml
144
- doc = Nokogiri::XML::DocumentFragment.new(Nokogiri::XML::Document.new)
145
- Nokogiri::XML::Element.new(self.class.entity_name, doc).tap do |root_element|
146
- self.class.class_props.each do |prop_def|
147
- add_property_to_xml(doc, prop_def, root_element)
148
- end
149
- end
176
+ def xml_elements
177
+ Hash[to_h.map {|name, value| [name, self.class.class_props[name] == String ? value.to_s : value] }]
150
178
  end
151
179
 
152
- def add_property_to_xml(doc, prop_def, root_element)
153
- property = prop_def[:name]
154
- type = prop_def[:type]
155
- if type == String
156
- root_element << simple_node(doc, prop_def[:xml_name], property)
180
+ def add_property_to_xml(doc, root_element, name, value)
181
+ if value.is_a? String
182
+ root_element << simple_node(doc, name, value)
157
183
  else
158
184
  # call #to_xml for each item and append to root
159
- [*public_send(property)].compact.each do |item|
185
+ [*value].compact.each do |item|
160
186
  root_element << item.to_xml
161
187
  end
162
188
  end
163
189
  end
164
190
 
165
191
  # create simple node, fill it with text and append to root
166
- def simple_node(doc, name, property)
167
- Nokogiri::XML::Element.new(name.to_s, doc).tap do |node|
168
- data = public_send(property).to_s
169
- node.content = data unless data.empty?
192
+ def simple_node(doc, name, value)
193
+ xml_name = self.class.xml_names[name]
194
+ Nokogiri::XML::Element.new(xml_name ? xml_name.to_s : name, doc).tap do |node|
195
+ node.content = value unless value.empty?
170
196
  end
171
197
  end
172
198
 
199
+ # @param [Nokogiri::XML::Element] root_node xml nodes
200
+ # @return [Entity] instance
201
+ def self.populate_entity(root_node)
202
+ entity_data = Hash[class_props.map {|name, type|
203
+ [name, parse_element_from_node(name, type, root_node)]
204
+ }]
205
+
206
+ new(entity_data)
207
+ end
208
+ private_class_method :populate_entity
209
+
210
+ # @param [String] name property name to parse
211
+ # @param [Class] type target type to parse
212
+ # @param [Nokogiri::XML::Element] root_node XML node to parse
213
+ # @return [Object] parsed data
214
+ def self.parse_element_from_node(name, type, root_node)
215
+ if type == String
216
+ parse_string_from_node(name, root_node)
217
+ elsif type.instance_of?(Array)
218
+ parse_array_from_node(type.first, root_node)
219
+ elsif type.ancestors.include?(Entity)
220
+ parse_entity_from_node(type, root_node)
221
+ end
222
+ end
223
+ private_class_method :parse_element_from_node
224
+
225
+ # create simple entry in data hash
226
+ #
227
+ # @param [String] name xml tag to parse
228
+ # @param [Nokogiri::XML::Element] root_node XML root_node to parse
229
+ # @return [String] data
230
+ def self.parse_string_from_node(name, root_node)
231
+ node = root_node.xpath(name.to_s)
232
+ node = root_node.xpath(xml_names[name].to_s) if node.empty?
233
+ node.first.text if node.any?
234
+ end
235
+ private_class_method :parse_string_from_node
236
+
237
+ # create an entry in the data hash for the nested entity
238
+ #
239
+ # @param [Class] type target type to parse
240
+ # @param [Nokogiri::XML::Element] root_node XML node to parse
241
+ # @return [Entity] parsed child entity
242
+ def self.parse_entity_from_node(type, root_node)
243
+ node = root_node.xpath(type.entity_name)
244
+ type.from_xml(node.first) if node.any?
245
+ end
246
+ private_class_method :parse_entity_from_node
247
+
248
+ # collect all nested children of that type and create an array in the data hash
249
+ #
250
+ # @param [Class] type target type to parse
251
+ # @param [Nokogiri::XML::Element] root_node XML node to parse
252
+ # @return [Array<Entity>] array with parsed child entities
253
+ def self.parse_array_from_node(type, root_node)
254
+ node = root_node.xpath(type.entity_name)
255
+ node.map {|child| type.from_xml(child) }
256
+ end
257
+ private_class_method :parse_array_from_node
258
+
173
259
  # Raised, if entity is not valid
174
260
  class ValidationError < RuntimeError
175
261
  end
262
+
263
+ # Raised, if the root node doesn't match the class name
264
+ class InvalidRootNode < RuntimeError
265
+ end
266
+
267
+ # Raised, if the entity name in the XML is invalid
268
+ class InvalidEntityName < RuntimeError
269
+ end
270
+
271
+ # Raised, if the entity contained within the XML cannot be mapped to a
272
+ # defined {Entity} subclass.
273
+ class UnknownEntity < RuntimeError
274
+ end
176
275
  end
177
276
  end