rubysync 0.1.1 → 0.2.1
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.
- data.tar.gz.sig +0 -0
- data/HISTORY.txt +11 -0
- data/Manifest.txt +4 -1
- data/Rakefile +3 -4
- data/bin/rubysync +242 -152
- data/bin/rubysync.rb +242 -152
- data/lib/ruby_sync.rb +1 -1
- data/lib/ruby_sync/connectors/base_connector.rb +286 -378
- data/lib/ruby_sync/connectors/connector_event_processing.rb +24 -20
- data/lib/ruby_sync/connectors/csv_file_connector.rb +3 -5
- data/lib/ruby_sync/connectors/dbm_association_tracking.rb +88 -0
- data/lib/ruby_sync/connectors/dbm_change_tracking.rb +110 -0
- data/lib/ruby_sync/connectors/ldap_changelog_connector.rb +79 -78
- data/lib/ruby_sync/connectors/ldap_connector.rb +84 -57
- data/lib/ruby_sync/connectors/memory_association_tracking.rb +60 -0
- data/lib/ruby_sync/connectors/memory_change_tracking.rb +84 -0
- data/lib/ruby_sync/connectors/memory_connector.rb +3 -0
- data/lib/ruby_sync/connectors/xml_connector.rb +6 -2
- data/lib/ruby_sync/event.rb +73 -50
- data/lib/ruby_sync/operation.rb +3 -3
- data/lib/ruby_sync/pipelines/base_pipeline.rb +76 -48
- data/lib/ruby_sync/util/utilities.rb +72 -29
- data/test/tc_csv_file_connector.rb +2 -2
- data/test/tc_ldap_connector.rb +2 -2
- data/test/tc_memory_connectors.rb +0 -2
- data/test/tc_transformation.rb +14 -13
- data/test/tc_xml_connectors.rb +4 -2
- data/test/ts_rubysync.rb +1 -1
- metadata +102 -84
- metadata.gz.sig +0 -0
- data/lib/ruby_sync/connectors/ldap_associations.rb +0 -126
@@ -3,19 +3,26 @@
|
|
3
3
|
# Copyright (c) 2007 Ritchie Young. All rights reserved.
|
4
4
|
#
|
5
5
|
# This file is part of RubySync.
|
6
|
-
#
|
7
|
-
# RubySync is free software; you can redistribute it and/or modify it
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
6
|
+
#
|
7
|
+
# RubySync is free software; you can redistribute it and/or modify it
|
8
|
+
# under the terms of the GNU General Public License
|
9
|
+
# as published by the Free Software Foundation; either version 2 of
|
10
|
+
# the License, or (at your option) any later version.
|
11
|
+
#
|
12
|
+
# RubySync is distributed in the hope that it will be useful, but
|
13
|
+
# WITHOUT ANY WARRANTY; without even the implied
|
14
|
+
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
15
|
+
# the GNU General Public License for more details.
|
16
|
+
#
|
17
|
+
# You should have received a copy of the GNU General Public License
|
18
|
+
# along with RubySync; if not, write to the
|
19
|
+
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
20
|
+
# Boston, MA 02110-1301, USA
|
15
21
|
|
16
22
|
|
17
23
|
lib_path = File.dirname(__FILE__) + '/..'
|
18
|
-
$:.unshift lib_path unless $:.include?(lib_path) ||
|
24
|
+
$:.unshift lib_path unless $:.include?(lib_path) ||
|
25
|
+
$:.include?(File.expand_path(lib_path))
|
19
26
|
|
20
27
|
require 'ruby_sync'
|
21
28
|
require 'net/ldif'
|
@@ -26,27 +33,29 @@ require 'net/ldap'
|
|
26
33
|
|
27
34
|
class Net::LDAP::Entry
|
28
35
|
def to_hash
|
29
|
-
|
36
|
+
@myhash.dup
|
30
37
|
end
|
31
38
|
end
|
32
39
|
|
33
40
|
module RubySync::Connectors
|
34
41
|
class LdapConnector < RubySync::Connectors::BaseConnector
|
35
|
-
|
42
|
+
|
36
43
|
option :host,
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
44
|
+
:port,
|
45
|
+
:bind_method,
|
46
|
+
:encryption,
|
47
|
+
:username,
|
48
|
+
:password,
|
49
|
+
:search_filter,
|
50
|
+
:search_base,
|
51
|
+
:association_attribute # name of the attribute in which to store the association key(s)
|
52
|
+
|
45
53
|
association_attribute 'RubySyncAssociation'
|
46
54
|
bind_method :simple
|
47
55
|
host 'localhost'
|
48
56
|
port 389
|
49
57
|
search_filter "cn=*"
|
58
|
+
encryption nil
|
50
59
|
|
51
60
|
def initialize options={}
|
52
61
|
super options
|
@@ -55,17 +64,19 @@ module RubySync::Connectors
|
|
55
64
|
|
56
65
|
def started
|
57
66
|
#TODO: If vault, check the schema to make sure that the association_attribute is there
|
67
|
+
@connections = []
|
68
|
+
@connection_index = 0
|
58
69
|
end
|
59
|
-
|
70
|
+
|
60
71
|
|
61
72
|
def each_entry
|
62
73
|
Net::LDAP.open(:host=>host, :port=>port, :auth=>auth) do |ldap|
|
63
|
-
|
64
|
-
|
65
|
-
|
74
|
+
ldap.search :base => search_base, :filter => search_filter, :return_result => false do |ldap_entry|
|
75
|
+
yield ldap_entry.dn, to_entry(ldap_entry)
|
76
|
+
end
|
66
77
|
end
|
67
78
|
end
|
68
|
-
|
79
|
+
|
69
80
|
# Runs the query specified by the config, gets the objectclass of the first
|
70
81
|
# returned object and returns a list of its allowed attributes
|
71
82
|
def self.fields
|
@@ -73,19 +84,30 @@ module RubySync::Connectors
|
|
73
84
|
log.warn "Returning a likely sample set."
|
74
85
|
%w{ cn givenName sn }
|
75
86
|
end
|
76
|
-
|
87
|
+
|
77
88
|
|
78
89
|
|
79
90
|
def self.sample_config
|
80
91
|
return <<END
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
92
|
+
|
93
|
+
# Using :memory is ok for testing.
|
94
|
+
# For production, you will need to change to a persistent form of tracking
|
95
|
+
# such as :dbm or :ldap.
|
96
|
+
track_changes_with :memory
|
97
|
+
track_associations_with :memory
|
98
|
+
|
99
|
+
host 'localhost'
|
100
|
+
port 389
|
101
|
+
username 'cn=Manager,dc=my-domain,dc=com'
|
102
|
+
password 'secret'
|
103
|
+
search_filter "cn=*"
|
104
|
+
search_base "ou=users,o=my-organization,dc=my-domain,dc=com"
|
105
|
+
#bind_method :simple
|
106
|
+
|
107
|
+
#Uncomment the following for LDAPS. If you do, make sure that
|
108
|
+
#you're using the LDAPS port (probably 636) and be aware that
|
109
|
+
#the server's certificate WON'T be checked for validity.
|
110
|
+
#encryption :simple_tls
|
89
111
|
END
|
90
112
|
end
|
91
113
|
|
@@ -94,11 +116,12 @@ END
|
|
94
116
|
def add(path, operations)
|
95
117
|
result = nil
|
96
118
|
with_ldap do |ldap|
|
97
|
-
|
98
|
-
|
119
|
+
attributes = perform_operations(operations)
|
120
|
+
attributes['objectclass'] || log.warn("Add without objectclass attribute is unlikely to work.")
|
121
|
+
result = ldap.add :dn=>path, :attributes=>attributes
|
99
122
|
end
|
100
123
|
log.debug("ldap.add returned '#{result}'")
|
101
|
-
return
|
124
|
+
return result
|
102
125
|
rescue Exception
|
103
126
|
log.warn "Exception occurred while adding LDAP record"
|
104
127
|
log.debug $!
|
@@ -116,23 +139,23 @@ END
|
|
116
139
|
|
117
140
|
def [](path)
|
118
141
|
with_ldap do |ldap|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
142
|
+
result = ldap.search :base=>path, :scope=>Net::LDAP::SearchScope_BaseObject, :filter=>'objectclass=*'
|
143
|
+
return nil if !result or result.size == 0
|
144
|
+
answer = {}
|
145
|
+
result[0].attribute_names.each do |name|
|
146
|
+
name = name.to_s.downcase
|
147
|
+
answer[name] = result[0][name] unless name == 'dn'
|
148
|
+
end
|
149
|
+
answer
|
127
150
|
end
|
128
151
|
end
|
129
|
-
|
152
|
+
|
130
153
|
# Called by unit tests to inject data
|
131
154
|
def test_add id, details
|
132
155
|
details << RubySync::Operation.new(:add, "objectclass", ['inetOrgPerson'])
|
133
156
|
add id, details
|
134
157
|
end
|
135
|
-
|
158
|
+
|
136
159
|
def target_transform event
|
137
160
|
#event.add_default 'objectclass', 'inetOrgUser'
|
138
161
|
#is_vault? and event.add_value 'objectclass', RUBYSYNC_ASSOCIATION_CLASS
|
@@ -145,7 +168,7 @@ END
|
|
145
168
|
def to_entry ldap_entry
|
146
169
|
entry = {}
|
147
170
|
ldap_entry.each do |name, values|
|
148
|
-
|
171
|
+
entry[name.to_s] = values.map {|v| String.new(v)}
|
149
172
|
end
|
150
173
|
entry
|
151
174
|
end
|
@@ -153,31 +176,35 @@ END
|
|
153
176
|
def operations_for_entry entry
|
154
177
|
ops = []
|
155
178
|
entry.each do |name, values|
|
156
|
-
|
179
|
+
ops << RubySync::Operation.add(name, values)
|
157
180
|
end
|
158
181
|
ops
|
159
182
|
end
|
160
183
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
184
|
|
165
185
|
def with_ldap
|
166
186
|
result = nil
|
167
|
-
|
168
|
-
|
187
|
+
connection_options = {:host=>host, :port=>port, :auth=>auth}
|
188
|
+
connection_options[:encryption] = encryption if encryption
|
189
|
+
@connections[@connection_index] = Net::LDAP.new(connection_options) unless @connections[@connection_index]
|
190
|
+
if @connections[@connection_index]
|
191
|
+
ldap = @connections[@connection_index]
|
192
|
+
@connection_index += 1
|
193
|
+
result = yield ldap
|
194
|
+
@connection_index -= 1
|
169
195
|
end
|
170
196
|
result
|
171
197
|
end
|
172
|
-
|
198
|
+
|
199
|
+
|
173
200
|
def auth
|
174
201
|
{:method=>bind_method, :username=>username, :password=>password}
|
175
202
|
end
|
176
|
-
|
203
|
+
|
177
204
|
# Produce an array of operation arrays suitable for the LDAP library
|
178
205
|
def to_ldap_operations operations
|
179
206
|
operations.map {|op| [op.type, op.subject, op.values]}
|
180
207
|
end
|
181
|
-
|
208
|
+
|
182
209
|
end
|
183
210
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (c) 2007 Ritchie Young. All rights reserved.
|
4
|
+
#
|
5
|
+
# This file is part of RubySync.
|
6
|
+
#
|
7
|
+
# RubySync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
|
8
|
+
# as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# RubySync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
11
|
+
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License along with RubySync; if not, write to the
|
14
|
+
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
module RubySync::Connectors::MemoryAssociationTracking
|
19
|
+
|
20
|
+
# Returns an instance based hash association=>path
|
21
|
+
def paths_by_association
|
22
|
+
@paths_by_association ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns an instance based hash path=>{context=>key}
|
26
|
+
def associations_by_path
|
27
|
+
@assocications_by_path ||= {}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Store association for the given path
|
31
|
+
def associate association, path
|
32
|
+
paths_by_association[association.to_s] = path
|
33
|
+
associations_for(path)[association.context] = association.key
|
34
|
+
end
|
35
|
+
|
36
|
+
def path_for_association association
|
37
|
+
paths_by_association[association.to_s]
|
38
|
+
end
|
39
|
+
|
40
|
+
def associations_for path
|
41
|
+
associations_by_path[path] ||= {}
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove_association association
|
45
|
+
path = paths_by_association[association]
|
46
|
+
if path
|
47
|
+
paths_by_association.delete(association)
|
48
|
+
associations_for(path).delete(association.context)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def association_key_for context, path
|
53
|
+
associations_for(path)[context]
|
54
|
+
end
|
55
|
+
|
56
|
+
def remove_associations
|
57
|
+
@paths_by_association = nil
|
58
|
+
@associations_by_path = nil
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Copyright (c) 2007 Ritchie Young. All rights reserved.
|
4
|
+
#
|
5
|
+
# This file is part of RubySync.
|
6
|
+
#
|
7
|
+
# RubySync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License
|
8
|
+
# as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# RubySync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
11
|
+
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License along with RubySync; if not, write to the
|
14
|
+
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
15
|
+
|
16
|
+
|
17
|
+
require 'digest/md5'
|
18
|
+
|
19
|
+
# When included by a connector, tracks changes to the connector using
|
20
|
+
# a dbm database of the form path:digest. Digest is a MD5 hash of the
|
21
|
+
# record so we can tell if the record has changed. We can't, however,
|
22
|
+
# tell how the record has changed so change events generated will be
|
23
|
+
# for the entire record.
|
24
|
+
module RubySync::Connectors::MemoryChangeTracking
|
25
|
+
|
26
|
+
|
27
|
+
def shadow
|
28
|
+
@shadow ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
# Subclasses MAY override this to interface with the external system
|
32
|
+
# and generate an event for every change that affects items within
|
33
|
+
# the scope of this connector.
|
34
|
+
#
|
35
|
+
# The default behaviour is to compare a hash of each entry in the
|
36
|
+
# database with a stored hash of its previous value and generate
|
37
|
+
# add, modify and delete events appropriately. This is normally a very
|
38
|
+
# inefficient way to operate so overriding this method is highly
|
39
|
+
# recommended if you can detect changes in a more efficient manner.
|
40
|
+
#
|
41
|
+
# This method will be called repeatedly until the connector is
|
42
|
+
# stopped.
|
43
|
+
def each_change
|
44
|
+
# scan existing entries to see if any new or modified
|
45
|
+
each_entry do |path, entry|
|
46
|
+
digest = digest(entry)
|
47
|
+
unless stored_digest = shadow[path.to_s] and digest == stored_digest
|
48
|
+
operations = create_operations_for(entry)
|
49
|
+
yield RubySync::Event.add(self, path, nil, operations)
|
50
|
+
shadow[path.to_s] = digest
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# scan shadow to find deleted
|
55
|
+
shadow.each do |key, stored_hash|
|
56
|
+
unless self[key]
|
57
|
+
yield RubySync::Event.delete(self, key)
|
58
|
+
shadow.delete key
|
59
|
+
if is_vault? and @pipeline
|
60
|
+
association = association_for @pipeline.association_context, key
|
61
|
+
remove_association association
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def digest(o)
|
68
|
+
Digest::MD5.hexdigest(o.to_yaml)
|
69
|
+
end
|
70
|
+
|
71
|
+
def remove_mirror
|
72
|
+
@shadow = nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def delete_from_mirror path
|
76
|
+
shadow.delete(path.to_s)
|
77
|
+
end
|
78
|
+
|
79
|
+
def update_mirror path
|
80
|
+
entry = self[path.to_s]
|
81
|
+
shadow[path.to_s] = digest(entry)
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -20,6 +20,9 @@ defined?(RubySync::Connectors::BaseConnector) or require 'ruby_sync/connectors/b
|
|
20
20
|
module RubySync::Connectors
|
21
21
|
class MemoryConnector < RubySync::Connectors::BaseConnector
|
22
22
|
|
23
|
+
include MemoryAssociationTracking
|
24
|
+
include MemoryChangeTracking
|
25
|
+
|
23
26
|
|
24
27
|
def each_entry
|
25
28
|
@data.each do |key, entry|
|
@@ -146,9 +146,13 @@ filename "/tmp/rubysync.xml"
|
|
146
146
|
unless @with_xml_invoked
|
147
147
|
begin
|
148
148
|
@with_xml_invoked = true
|
149
|
-
|
149
|
+
File.exist?(filename) or File.open(filename,'w') {|file| file.write('<entries/>')}
|
150
150
|
File.open(filename, "r") do |file|
|
151
|
-
|
151
|
+
# flock behaves differently on
|
152
|
+
# Windows. Seems to prevent us from reopening the file for
|
153
|
+
# writing even within the same process. Temporarily removed.
|
154
|
+
# TODO: add this again but only if not executing on Windows
|
155
|
+
#file.flock(File::LOCK_EX)
|
152
156
|
@xml = Document.new(file)
|
153
157
|
begin
|
154
158
|
yield @xml
|
data/lib/ruby_sync/event.rb
CHANGED
@@ -17,11 +17,11 @@
|
|
17
17
|
|
18
18
|
module RubySync
|
19
19
|
|
20
|
-
|
20
|
+
class Association
|
21
21
|
attr_accessor :context, # many associations will share the same context
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
# it is a function of pipeline and the client connector
|
23
|
+
# to which the association applies
|
24
|
+
:key # the key is unique within the context and vault
|
25
25
|
|
26
26
|
|
27
27
|
def self.delimiter; '$'; end
|
@@ -47,12 +47,12 @@ module RubySync
|
|
47
47
|
include RubySync::Utilities
|
48
48
|
|
49
49
|
attr_accessor :type, # :delete, :add, :modify, :disassociate
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
50
|
+
:source,
|
51
|
+
:target,
|
52
|
+
:payload,
|
53
|
+
:source_path,
|
54
|
+
:target_path,
|
55
|
+
:association
|
56
56
|
|
57
57
|
def self.force_resync source
|
58
58
|
self.new(:force_resync, source)
|
@@ -105,9 +105,7 @@ module RubySync
|
|
105
105
|
# Reduces the operations in this event to those that will
|
106
106
|
# alter the target record
|
107
107
|
def merge other
|
108
|
-
|
109
|
-
# record = perform_operations(other.payload)
|
110
|
-
payload = effective_operations(@payload, other)
|
108
|
+
@payload = effective_operations(@payload, other)
|
111
109
|
end
|
112
110
|
|
113
111
|
# Retrieves all known values for the record affected by this event and
|
@@ -123,17 +121,31 @@ module RubySync
|
|
123
121
|
@type = :add
|
124
122
|
end
|
125
123
|
|
126
|
-
|
124
|
+
|
125
|
+
def convert_to_modify(other, filter)
|
127
126
|
log.info "Converting '#{type}' event to modify"
|
127
|
+
|
128
|
+
# The add event contained an operation for each attribute of the source record.
|
129
|
+
# Therefore, we should delete any attributes in the target record that don't appear
|
130
|
+
# in the event.
|
131
|
+
affected = affected_subjects
|
132
|
+
other.each do |key, value|
|
133
|
+
if filter.include?(key) and !affected.include?(key)
|
134
|
+
log.info "Adding delete operation for #{key}"
|
135
|
+
@payload << Operation.delete(key)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
128
139
|
@type = :modify
|
129
|
-
@payload.each do |op|
|
130
|
-
op.type = :replace
|
131
|
-
end
|
132
140
|
end
|
133
141
|
|
142
|
+
# Return a list of subjects that this event affects
|
143
|
+
def affected_subjects
|
144
|
+
@payload.map {|op| op.subject}.uniq
|
145
|
+
end
|
134
146
|
|
135
147
|
def hint
|
136
|
-
|
148
|
+
"(#{source.name} => #{target.name}) #{source_path}"
|
137
149
|
end
|
138
150
|
|
139
151
|
|
@@ -163,35 +175,39 @@ module RubySync
|
|
163
175
|
@uncommitted_operations = @uncommitted_operations.delete_if {|op| subjects.include? op.subject }
|
164
176
|
end
|
165
177
|
|
178
|
+
def downcase_subjects
|
179
|
+
@uncommitted_operations = uncommitted_operations.map {|op| Operation.new(op.type, op.subject.downcase, op.values)}
|
180
|
+
end
|
181
|
+
|
166
182
|
def drop_all_but_changes_to *subjects
|
167
183
|
subjects = subjects.flatten.collect {|s| s.to_s}
|
168
184
|
@uncommitted_operations = uncommitted_operations.delete_if {|op| !subjects.include?(op.subject.to_s)}
|
169
185
|
end
|
170
186
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
187
|
+
def delete_when_blank
|
188
|
+
@uncommitted_operations = uncommitted_operations.map do |op|
|
189
|
+
if op.sets_blank?
|
190
|
+
@type == :modify ? op.same_but_as(:delete) : nil
|
191
|
+
else
|
192
|
+
op
|
193
|
+
end
|
194
|
+
end.compact
|
195
|
+
end
|
180
196
|
|
181
197
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
198
|
+
# Add a value to a given subject unless it already sets a value
|
199
|
+
def add_default field_name, value
|
200
|
+
add_value(field_name.to_s, value) unless sets_value? field_name.to_s
|
201
|
+
end
|
186
202
|
|
187
203
|
|
188
|
-
|
189
|
-
|
190
|
-
|
204
|
+
def add_value field_name, value
|
205
|
+
uncommitted_operations << Operation.new(:add, field_name.to_s, as_array(value))
|
206
|
+
end
|
191
207
|
|
192
|
-
|
193
|
-
|
194
|
-
|
208
|
+
def set_value field_name, value
|
209
|
+
uncommitted_operations << Operation.new(:replace, field_name.to_s, as_array(value))
|
210
|
+
end
|
195
211
|
|
196
212
|
def values_for field_name, default=[]
|
197
213
|
values = perform_operations @payload, {}, :subjects=>[field_name.to_s]
|
@@ -252,21 +268,28 @@ module RubySync
|
|
252
268
|
def place(&blk)
|
253
269
|
self.target_path = blk.call
|
254
270
|
end
|
271
|
+
|
272
|
+
# true unless this event in its current state would have no impact
|
273
|
+
def effective_operation?
|
274
|
+
!(
|
275
|
+
@type == :modify && @payload.empty?
|
276
|
+
)
|
277
|
+
end
|
255
278
|
|
256
|
-
|
279
|
+
protected
|
257
280
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
281
|
+
# Try to make a sensible association from the passed in object
|
282
|
+
def make_association o
|
283
|
+
if o.kind_of?(Array) and o.size == 2
|
284
|
+
return Association.new(o[0],o[1])
|
285
|
+
elsif o.kind_of? RubySync::Association
|
286
|
+
return o
|
287
|
+
elsif o
|
288
|
+
return Association.new(nil, o)
|
289
|
+
else
|
290
|
+
nil
|
291
|
+
end
|
292
|
+
end
|
270
293
|
|
271
294
|
|
272
295
|
|