diaspora_federation 0.0.12 → 0.0.13
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.
- checksums.yaml +4 -4
- data/lib/diaspora_federation.rb +103 -18
- data/lib/diaspora_federation/discovery/discovery.rb +1 -1
- data/lib/diaspora_federation/discovery/h_card.rb +4 -5
- data/lib/diaspora_federation/discovery/host_meta.rb +1 -1
- data/lib/diaspora_federation/discovery/web_finger.rb +8 -8
- data/lib/diaspora_federation/discovery/xrd_document.rb +6 -7
- data/lib/diaspora_federation/entities.rb +21 -10
- data/lib/diaspora_federation/entities/account_deletion.rb +7 -3
- data/lib/diaspora_federation/entities/comment.rb +13 -10
- data/lib/diaspora_federation/entities/contact.rb +29 -0
- data/lib/diaspora_federation/entities/conversation.rb +5 -6
- data/lib/diaspora_federation/entities/like.rb +10 -18
- data/lib/diaspora_federation/entities/message.rb +6 -12
- data/lib/diaspora_federation/entities/participation.rb +8 -16
- data/lib/diaspora_federation/entities/person.rb +6 -2
- data/lib/diaspora_federation/entities/photo.rb +3 -3
- data/lib/diaspora_federation/entities/poll_participation.rb +6 -12
- data/lib/diaspora_federation/entities/post.rb +37 -0
- data/lib/diaspora_federation/entities/profile.rb +7 -3
- data/lib/diaspora_federation/entities/relayable.rb +169 -65
- data/lib/diaspora_federation/entities/relayable_retraction.rb +33 -32
- data/lib/diaspora_federation/entities/request.rb +20 -6
- data/lib/diaspora_federation/entities/reshare.rb +5 -27
- data/lib/diaspora_federation/entities/retraction.rb +6 -6
- data/lib/diaspora_federation/entities/signed_retraction.rb +32 -26
- data/lib/diaspora_federation/entities/status_message.rb +2 -22
- data/lib/diaspora_federation/entity.rb +137 -38
- data/lib/diaspora_federation/federation.rb +9 -0
- data/lib/diaspora_federation/federation/fetcher.rb +26 -0
- data/lib/diaspora_federation/federation/receiver.rb +41 -0
- data/lib/diaspora_federation/federation/receiver/abstract_receiver.rb +35 -0
- data/lib/diaspora_federation/federation/receiver/exceptions.rb +13 -0
- data/lib/diaspora_federation/federation/receiver/private.rb +15 -0
- data/lib/diaspora_federation/federation/receiver/public.rb +9 -0
- data/lib/diaspora_federation/federation/sender.rb +33 -0
- data/lib/diaspora_federation/federation/sender/hydra_wrapper.rb +92 -0
- data/lib/diaspora_federation/{fetcher.rb → http_client.rb} +6 -6
- data/lib/diaspora_federation/properties_dsl.rb +51 -14
- data/lib/diaspora_federation/salmon.rb +2 -1
- data/lib/diaspora_federation/salmon/aes.rb +1 -1
- data/lib/diaspora_federation/salmon/encrypted_magic_envelope.rb +61 -0
- data/lib/diaspora_federation/salmon/encrypted_slap.rb +69 -50
- data/lib/diaspora_federation/salmon/exceptions.rb +8 -14
- data/lib/diaspora_federation/salmon/magic_envelope.rb +80 -39
- data/lib/diaspora_federation/salmon/slap.rb +20 -51
- data/lib/diaspora_federation/salmon/xml_payload.rb +5 -104
- data/lib/diaspora_federation/validators.rb +22 -16
- data/lib/diaspora_federation/validators/account_deletion_validator.rb +1 -1
- data/lib/diaspora_federation/validators/comment_validator.rb +0 -4
- data/lib/diaspora_federation/validators/contact_validator.rb +13 -0
- data/lib/diaspora_federation/validators/conversation_validator.rb +2 -2
- data/lib/diaspora_federation/validators/like_validator.rb +1 -3
- data/lib/diaspora_federation/validators/message_validator.rb +0 -4
- data/lib/diaspora_federation/validators/participation_validator.rb +1 -5
- data/lib/diaspora_federation/validators/person_validator.rb +1 -1
- data/lib/diaspora_federation/validators/photo_validator.rb +2 -2
- data/lib/diaspora_federation/validators/poll_participation_validator.rb +0 -4
- data/lib/diaspora_federation/validators/profile_validator.rb +1 -1
- data/lib/diaspora_federation/validators/relayable_retraction_validator.rb +1 -1
- data/lib/diaspora_federation/validators/relayable_validator.rb +2 -0
- data/lib/diaspora_federation/validators/request_validator.rb +3 -2
- data/lib/diaspora_federation/validators/reshare_validator.rb +3 -3
- data/lib/diaspora_federation/validators/retraction_validator.rb +2 -2
- data/lib/diaspora_federation/validators/rules/guid.rb +16 -7
- data/lib/diaspora_federation/validators/signed_retraction_validator.rb +1 -1
- data/lib/diaspora_federation/validators/status_message_validator.rb +2 -2
- data/lib/diaspora_federation/version.rb +1 -1
- metadata +20 -11
- data/lib/diaspora_federation/receiver.rb +0 -28
- data/lib/diaspora_federation/receiver/private.rb +0 -19
- data/lib/diaspora_federation/receiver/public.rb +0 -13
- data/lib/diaspora_federation/signing.rb +0 -56
@@ -0,0 +1,26 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module Federation
|
3
|
+
# this module is for fetching entities from other pods
|
4
|
+
module Fetcher
|
5
|
+
# fetches a public entity from a remote pod
|
6
|
+
# @param [String] author the diaspora ID of the author of the entity
|
7
|
+
# @param [Symbol, String] entity_type snake_case version of the entity class
|
8
|
+
# @param [String] guid guid of the entity to fetch
|
9
|
+
def self.fetch_public(author, entity_type, guid)
|
10
|
+
url = DiasporaFederation.callbacks.trigger(:fetch_person_url_to, author, "/fetch/#{entity_type}/#{guid}")
|
11
|
+
response = HttpClient.get(url)
|
12
|
+
raise "Failed to fetch #{url}: #{response.status}" unless response.success?
|
13
|
+
|
14
|
+
magic_env_xml = Nokogiri::XML::Document.parse(response.body).root
|
15
|
+
magic_env = Salmon::MagicEnvelope.unenvelop(magic_env_xml)
|
16
|
+
Receiver::Public.new(magic_env).receive
|
17
|
+
rescue => e
|
18
|
+
raise NotFetchable, "Failed to fetch #{entity_type}:#{guid} from #{author}: #{e.class}: #{e.message}"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Raised, if the entity is not fetchable
|
22
|
+
class NotFetchable < RuntimeError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module Federation
|
3
|
+
# this module is for parse and receive entities.
|
4
|
+
module Receiver
|
5
|
+
# receive a public message
|
6
|
+
# @param [String] data message to receive
|
7
|
+
# @param [Boolean] legacy use old slap parser
|
8
|
+
def self.receive_public(data, legacy=false)
|
9
|
+
magic_env = if legacy
|
10
|
+
Salmon::Slap.from_xml(data)
|
11
|
+
else
|
12
|
+
magic_env_xml = Nokogiri::XML::Document.parse(data).root
|
13
|
+
Salmon::MagicEnvelope.unenvelop(magic_env_xml)
|
14
|
+
end
|
15
|
+
Public.new(magic_env).receive
|
16
|
+
end
|
17
|
+
|
18
|
+
# receive a private message
|
19
|
+
# @param [String] data message to receive
|
20
|
+
# @param [OpenSSL::PKey::RSA] recipient_private_key recipient private key to decrypt the message
|
21
|
+
# @param [Object] recipient_id the identifier to persist the entity for the correct user,
|
22
|
+
# see +receive_entity+ callback
|
23
|
+
# @param [Boolean] legacy use old slap parser
|
24
|
+
def self.receive_private(data, recipient_private_key, recipient_id, legacy=false)
|
25
|
+
raise ArgumentError, "no recipient key provided" unless recipient_private_key.instance_of?(OpenSSL::PKey::RSA)
|
26
|
+
magic_env = if legacy
|
27
|
+
Salmon::EncryptedSlap.from_xml(data, recipient_private_key)
|
28
|
+
else
|
29
|
+
magic_env_xml = Salmon::EncryptedMagicEnvelope.decrypt(data, recipient_private_key)
|
30
|
+
Salmon::MagicEnvelope.unenvelop(magic_env_xml)
|
31
|
+
end
|
32
|
+
Private.new(magic_env, recipient_id).receive
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
require "diaspora_federation/federation/receiver/exceptions"
|
39
|
+
require "diaspora_federation/federation/receiver/abstract_receiver"
|
40
|
+
require "diaspora_federation/federation/receiver/public"
|
41
|
+
require "diaspora_federation/federation/receiver/private"
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module Federation
|
3
|
+
module Receiver
|
4
|
+
# common functionality for receivers
|
5
|
+
class AbstractReceiver
|
6
|
+
# create a new receiver
|
7
|
+
# @param [MagicEnvelope] magic_envelope the received magic envelope
|
8
|
+
# @param [Object] recipient_id the identifier of the recipient of a private message
|
9
|
+
def initialize(magic_envelope, recipient_id=nil)
|
10
|
+
@entity = magic_envelope.payload
|
11
|
+
@sender = magic_envelope.sender
|
12
|
+
@recipient_id = recipient_id
|
13
|
+
end
|
14
|
+
|
15
|
+
# validate and receive the entity
|
16
|
+
def receive
|
17
|
+
validate
|
18
|
+
DiasporaFederation.callbacks.trigger(:receive_entity, entity, recipient_id)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :entity, :sender, :recipient_id
|
24
|
+
|
25
|
+
def validate
|
26
|
+
raise InvalidSender unless sender_valid?
|
27
|
+
end
|
28
|
+
|
29
|
+
def sender_valid?
|
30
|
+
sender == entity.author # TODO: handle sender of relayables
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module Federation
|
3
|
+
module Receiver
|
4
|
+
# Raised, if the sender of the {Salmon::MagicEnvelope} is not allowed to send the entity.
|
5
|
+
class InvalidSender < RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Raised, if receiving a private message without recipient.
|
9
|
+
class RecipientRequired < RuntimeError
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module Federation
|
3
|
+
# Federation logic to send messages to other pods
|
4
|
+
module Sender
|
5
|
+
# Send a public message to all urls
|
6
|
+
#
|
7
|
+
# @param [String] sender_id sender diaspora-ID
|
8
|
+
# @param [String] obj_str object string representation for logging (e.g. type@guid)
|
9
|
+
# @param [Array<String>] urls receive-urls from pods
|
10
|
+
# @param [String] xml salmon-xml
|
11
|
+
# @return [Array<String>] url to retry
|
12
|
+
def self.public(sender_id, obj_str, urls, xml)
|
13
|
+
hydra = HydraWrapper.new(sender_id, obj_str)
|
14
|
+
urls.each {|url| hydra.insert_job(url, xml) }
|
15
|
+
hydra.send
|
16
|
+
end
|
17
|
+
|
18
|
+
# Send a private message to receive-urls
|
19
|
+
#
|
20
|
+
# @param [String] sender_id sender diaspora-ID
|
21
|
+
# @param [String] obj_str object string representation for logging (e.g. type@guid)
|
22
|
+
# @param [Hash] targets Hash with receive-urls (key) of peoples with encrypted salmon-xml for them (value)
|
23
|
+
# @return [Hash] targets to retry
|
24
|
+
def self.private(sender_id, obj_str, targets)
|
25
|
+
hydra = HydraWrapper.new(sender_id, obj_str)
|
26
|
+
targets.each {|url, xml| hydra.insert_job(url, xml) }
|
27
|
+
Hash[hydra.send.map {|url| [url, targets[url]] }]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require "diaspora_federation/federation/sender/hydra_wrapper"
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module DiasporaFederation
|
2
|
+
module Federation
|
3
|
+
module Sender
|
4
|
+
# A wrapper for [Typhoeus::Hydra]
|
5
|
+
#
|
6
|
+
# Uses parallel http requests to send out the salmon-messages
|
7
|
+
class HydraWrapper
|
8
|
+
include Logging
|
9
|
+
|
10
|
+
# Hydra default opts
|
11
|
+
# @return [Hash] hydra opts
|
12
|
+
def self.hydra_opts
|
13
|
+
@hydra_opts ||= {
|
14
|
+
maxredirs: DiasporaFederation.http_redirect_limit,
|
15
|
+
timeout: DiasporaFederation.http_timeout,
|
16
|
+
method: :post,
|
17
|
+
verbose: DiasporaFederation.http_verbose,
|
18
|
+
cainfo: DiasporaFederation.certificate_authorities,
|
19
|
+
headers: {
|
20
|
+
"Expect" => "",
|
21
|
+
"Transfer-Encoding" => "",
|
22
|
+
"User-Agent" => DiasporaFederation.http_user_agent
|
23
|
+
}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Create a new instance for a message
|
28
|
+
#
|
29
|
+
# @param [String] sender_id sender diaspora-ID
|
30
|
+
# @param [String] obj_str object string representation for logging (e.g. type@guid)
|
31
|
+
def initialize(sender_id, obj_str)
|
32
|
+
@sender_id = sender_id
|
33
|
+
@obj_str = obj_str
|
34
|
+
@urls_to_retry = []
|
35
|
+
end
|
36
|
+
|
37
|
+
# Prepares and inserts job into the hydra queue
|
38
|
+
# @param [String] url the receive-url for the xml
|
39
|
+
# @param [String] xml xml salmon message
|
40
|
+
def insert_job(url, xml)
|
41
|
+
request = Typhoeus::Request.new(url, HydraWrapper.hydra_opts.merge(body: {xml: xml}))
|
42
|
+
prepare_request(request)
|
43
|
+
hydra.queue(request)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Sends all queued messages
|
47
|
+
# @return [Array<String>] urls to retry
|
48
|
+
def send
|
49
|
+
hydra.run
|
50
|
+
@urls_to_retry
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# @return [Typhoeus::Hydra] hydra
|
56
|
+
def hydra
|
57
|
+
@hydra ||= Typhoeus::Hydra.new(max_concurrency: DiasporaFederation.http_concurrency)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Logic for after complete
|
61
|
+
# @param [Typhoeus::Request] request
|
62
|
+
def prepare_request(request)
|
63
|
+
request.on_complete do |response|
|
64
|
+
DiasporaFederation.callbacks.trigger(:update_pod, pod_url(response.effective_url), status(response))
|
65
|
+
|
66
|
+
success = response.success?
|
67
|
+
log_line = "success=#{success} sender=#{@sender_id} obj=#{@obj_str} url=#{response.effective_url} " \
|
68
|
+
"message=#{response.return_code} code=#{response.response_code} time=#{response.total_time}"
|
69
|
+
if success
|
70
|
+
logger.info(log_line)
|
71
|
+
else
|
72
|
+
logger.warn(log_line)
|
73
|
+
|
74
|
+
@urls_to_retry << request.url
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Get the pod root-url from the send-url
|
80
|
+
# @param [String] url
|
81
|
+
# @return [String] pod root-url
|
82
|
+
def pod_url(url)
|
83
|
+
URI.parse(url).tap {|uri| uri.path = "/" }.to_s
|
84
|
+
end
|
85
|
+
|
86
|
+
def status(res)
|
87
|
+
res.return_code == :ok ? res.response_code : res.return_code
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -2,11 +2,11 @@ require "faraday"
|
|
2
2
|
require "faraday_middleware/response/follow_redirects"
|
3
3
|
|
4
4
|
module DiasporaFederation
|
5
|
-
# A wrapper for {https://github.com/lostisland/faraday Faraday}
|
6
|
-
# fetching
|
5
|
+
# A wrapper for {https://github.com/lostisland/faraday Faraday}.
|
7
6
|
#
|
8
7
|
# @see Discovery::Discovery
|
9
|
-
|
8
|
+
# @see Federation::Fetcher
|
9
|
+
class HttpClient
|
10
10
|
# Perform a GET request
|
11
11
|
#
|
12
12
|
# @param [String] uri the URI
|
@@ -25,16 +25,16 @@ module DiasporaFederation
|
|
25
25
|
|
26
26
|
def self.create_default_connection
|
27
27
|
options = {
|
28
|
-
request: {timeout:
|
28
|
+
request: {timeout: DiasporaFederation.http_timeout},
|
29
29
|
ssl: {ca_file: DiasporaFederation.certificate_authorities}
|
30
30
|
}
|
31
31
|
|
32
32
|
@connection = Faraday::Connection.new(options) do |builder|
|
33
|
-
builder.use FaradayMiddleware::FollowRedirects, limit:
|
33
|
+
builder.use FaradayMiddleware::FollowRedirects, limit: DiasporaFederation.http_redirect_limit
|
34
34
|
builder.adapter Faraday.default_adapter
|
35
35
|
end
|
36
36
|
|
37
|
-
@connection.headers["User-Agent"] =
|
37
|
+
@connection.headers["User-Agent"] = DiasporaFederation.http_user_agent
|
38
38
|
end
|
39
39
|
private_class_method :create_default_connection
|
40
40
|
end
|
@@ -7,12 +7,13 @@ module DiasporaFederation
|
|
7
7
|
# property :optional, default: false
|
8
8
|
# property :dynamic_default, default: -> { Time.now }
|
9
9
|
# property :another_prop, xml_name: :another_name
|
10
|
+
# property :original_prop, alias: :alias_prop
|
10
11
|
# entity :nested, NestedEntity
|
11
12
|
# entity :multiple, [OtherEntity]
|
12
13
|
module PropertiesDSL
|
13
|
-
# @return [
|
14
|
+
# @return [Hash] hash of declared entity properties
|
14
15
|
def class_props
|
15
|
-
@class_props ||=
|
16
|
+
@class_props ||= {}
|
16
17
|
end
|
17
18
|
|
18
19
|
# Define a generic (string-type) property
|
@@ -42,32 +43,48 @@ module DiasporaFederation
|
|
42
43
|
# Return array of missing required property names
|
43
44
|
# @return [Array<Symbol>] missing required property names
|
44
45
|
def missing_props(args)
|
45
|
-
|
46
|
+
class_props.keys - default_props.keys - args.keys
|
46
47
|
end
|
47
48
|
|
48
49
|
# Return a new hash of default values, with dynamic values
|
49
50
|
# resolved on each call
|
50
51
|
# @return [Hash] default values
|
51
52
|
def default_values
|
52
|
-
default_props.each_with_object({}) {
|
53
|
+
default_props.each_with_object({}) {|(name, prop), hash|
|
53
54
|
hash[name] = prop.respond_to?(:call) ? prop.call : prop
|
54
55
|
}
|
55
56
|
end
|
56
57
|
|
57
|
-
#
|
58
|
-
# @return [
|
59
|
-
def
|
60
|
-
|
58
|
+
# @param [Hash] data entity data
|
59
|
+
# @return [Hash] hash with resolved aliases
|
60
|
+
def resolv_aliases(data)
|
61
|
+
Hash[data.map {|name, value|
|
62
|
+
if class_prop_aliases.has_key? name
|
63
|
+
prop_name = class_prop_aliases[name]
|
64
|
+
raise InvalidData, "only use '#{name}' OR '#{prop_name}'" if data.has_key? prop_name
|
65
|
+
[prop_name, value]
|
66
|
+
else
|
67
|
+
[name, value]
|
68
|
+
end
|
69
|
+
}]
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [Symbol] alias for the xml-generation/parsing
|
73
|
+
# @deprecated
|
74
|
+
def xml_names
|
75
|
+
@xml_names ||= {}
|
61
76
|
end
|
62
77
|
|
63
|
-
#
|
64
|
-
# @
|
65
|
-
|
66
|
-
|
78
|
+
# finds a property by +xml_name+ or +name+
|
79
|
+
# @param [String] xml_name name of the property from the received xml
|
80
|
+
# @return [Hash] the property data
|
81
|
+
def find_property_for_xml_name(xml_name)
|
82
|
+
class_props.keys.find {|name| name.to_s == xml_name || xml_names[name].to_s == xml_name }
|
67
83
|
end
|
68
84
|
|
69
85
|
private
|
70
86
|
|
87
|
+
# @deprecated
|
71
88
|
def determine_xml_name(name, type, opts={})
|
72
89
|
raise ArgumentError, "xml_name is not supported for nested entities" if type != String && opts.has_key?(:xml_name)
|
73
90
|
|
@@ -88,10 +105,13 @@ module DiasporaFederation
|
|
88
105
|
def define_property(name, type, opts={})
|
89
106
|
raise InvalidName unless name_valid?(name)
|
90
107
|
|
91
|
-
class_props
|
108
|
+
class_props[name] = type
|
92
109
|
default_props[name] = opts[:default] if opts.has_key? :default
|
110
|
+
xml_names[name] = determine_xml_name(name, type, opts)
|
93
111
|
|
94
112
|
instance_eval { attr_reader name }
|
113
|
+
|
114
|
+
define_alias(name, opts[:alias]) if opts.has_key? :alias
|
95
115
|
end
|
96
116
|
|
97
117
|
# checks if the name is a +Symbol+ or a +String+
|
@@ -105,7 +125,7 @@ module DiasporaFederation
|
|
105
125
|
# @param [Class] type the type to check
|
106
126
|
# @return [Boolean]
|
107
127
|
def type_valid?(type)
|
108
|
-
[type].flatten.all? {
|
128
|
+
[type].flatten.all? {|type|
|
109
129
|
type.respond_to?(:ancestors) && type.ancestors.include?(Entity)
|
110
130
|
}
|
111
131
|
end
|
@@ -114,6 +134,19 @@ module DiasporaFederation
|
|
114
134
|
@default_props ||= {}
|
115
135
|
end
|
116
136
|
|
137
|
+
# Returns all alias mappings
|
138
|
+
# @return [Hash] alias properties
|
139
|
+
def class_prop_aliases
|
140
|
+
@class_prop_aliases ||= {}
|
141
|
+
end
|
142
|
+
|
143
|
+
# @param [Symbol] name property name
|
144
|
+
# @param [Symbol] alias_name alias name
|
145
|
+
def define_alias(name, alias_name)
|
146
|
+
class_prop_aliases[alias_name] = name
|
147
|
+
instance_eval { alias_method alias_name, name }
|
148
|
+
end
|
149
|
+
|
117
150
|
# Raised, if the name is of an unexpected type
|
118
151
|
class InvalidName < RuntimeError
|
119
152
|
end
|
@@ -121,5 +154,9 @@ module DiasporaFederation
|
|
121
154
|
# Raised, if the type is of an unexpected type
|
122
155
|
class InvalidType < RuntimeError
|
123
156
|
end
|
157
|
+
|
158
|
+
# Raised, if the data contains property twice (with name AND alias)
|
159
|
+
class InvalidData < RuntimeError
|
160
|
+
end
|
124
161
|
end
|
125
162
|
end
|