rubysync 0.1.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|