rubysync 0.0.3 → 0.0.4

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 (45) hide show
  1. data/HISTORY.txt +4 -0
  2. data/Manifest.txt +25 -12
  3. data/README.txt +0 -2
  4. data/bin/rubysync +20 -6
  5. data/bin/rubysync.rb +333 -0
  6. data/docs/in_pipeline.graffle +2690 -0
  7. data/docs/init_openldap.ldif +11 -0
  8. data/docs/out_pipeline.graffle +3274 -0
  9. data/docs/schema/99rubysync.ldif +27 -0
  10. data/docs/schema/rubysync.schema +16 -0
  11. data/docs/to_sync.txt +15 -0
  12. data/docs/walkthru.txt +186 -0
  13. data/lib/ruby_sync.rb +7 -29
  14. data/lib/ruby_sync/connectors/base_connector.rb +55 -86
  15. data/lib/ruby_sync/connectors/csv_file_connector.rb +16 -4
  16. data/lib/ruby_sync/connectors/ldap_associations.rb +126 -0
  17. data/lib/ruby_sync/connectors/ldap_changelog_connector.rb +127 -0
  18. data/lib/ruby_sync/connectors/ldap_connector.rb +29 -192
  19. data/lib/ruby_sync/connectors/memory_connector.rb +1 -1
  20. data/lib/ruby_sync/connectors/xml_connector.rb +105 -32
  21. data/lib/ruby_sync/event.rb +40 -12
  22. data/lib/ruby_sync/operation.rb +18 -2
  23. data/lib/ruby_sync/pipelines/base_pipeline.rb +44 -6
  24. data/lib/ruby_sync/util/utilities.rb +97 -4
  25. data/lib/rubysync.rb +1 -1
  26. data/rubysync.tmproj +279 -59
  27. data/test/.LCKts_rubysync.rb~ +1 -0
  28. data/test/ruby_sync_test.rb +9 -4
  29. data/test/{test_active_record_vault.rb → tc_active_record_connector.rb} +11 -7
  30. data/test/{test_base_connector.rb → tc_base_connector.rb} +1 -1
  31. data/test/{test_base_pipeline.rb → tc_base_pipeline.rb} +1 -1
  32. data/test/tc_changelog_ldap_connector.rb +93 -0
  33. data/test/{test_csv_file_connector.rb → tc_csv_file_connector.rb} +14 -5
  34. data/test/{test_event.rb → tc_event.rb} +1 -1
  35. data/test/{test_ldap_changelog.rb → tc_ldap_changelog.rb} +1 -1
  36. data/test/{test_ldap_connector.rb → tc_ldap_connector.rb} +20 -22
  37. data/test/{test_ldap_vault.rb → tc_ldap_vault.rb} +2 -2
  38. data/test/{test_ldif.rb → tc_ldif.rb} +1 -1
  39. data/test/{test_memory_connectors.rb → tc_memory_connectors.rb} +10 -6
  40. data/test/{test_rubysync.rb → tc_rubysync.rb} +4 -4
  41. data/test/tc_transformation.rb +71 -0
  42. data/test/{test_utilities.rb → tc_utilities.rb} +28 -1
  43. data/test/tc_xml_connectors.rb +107 -6
  44. data/test/ts_rubysync.rb +11 -6
  45. metadata +33 -28
@@ -0,0 +1,27 @@
1
+ #
2
+ ################################################################################
3
+ #
4
+ dn: cn=schema
5
+ #
6
+ ################################################################################
7
+ #
8
+ attributeTypes: (
9
+ 1.3.6.1.4.1.28955.50.1.1
10
+ NAME 'RubySyncAssociation'
11
+ DESC 'Context:Key provided by connected system'
12
+ EQUALITY caseExactMatch
13
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
14
+ )
15
+ #
16
+ ################################################################################
17
+ #
18
+ objectClasses: (
19
+ 1.3.6.1.4.1.28955.50.2.1
20
+ NAME 'RubySyncSynchable'
21
+ DESC 'Object can preserve links to other objects via RubySync'
22
+ AUXILIARY
23
+ MAY RubySyncAssociation
24
+ )
25
+ #
26
+ ################################################################################
27
+ #
@@ -0,0 +1,16 @@
1
+ # rubysync
2
+ # Generated By LDAPStudio on : Mar 10, 2007 10:49:43 PM
3
+
4
+ attributetype ( 1.3.6.1.4.1.28955.1.1.1
5
+ NAME 'RubySyncAssociation'
6
+ DESC 'Context:Key provided by connected system'
7
+ EQUALITY caseExactMatch
8
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
9
+ )
10
+
11
+ objectclass ( 1.3.6.1.4.1.28955.1.2.1 NAME 'RubySyncSynchable'
12
+ DESC 'Object can preserve links to other objects via RubySync'
13
+ AUXILIARY
14
+ MAY RubySyncAssociation )
15
+
16
+
data/docs/to_sync.txt ADDED
@@ -0,0 +1,15 @@
1
+ Things to Sync
2
+
3
+ gmail address book
4
+ yahoo address book
5
+ msn address book
6
+ delicious links
7
+ dobot.net tasks
8
+ palm pilot tasks
9
+ palm address book
10
+ windows mobile address book
11
+ rss feeds
12
+ any HTML page using scRubyt
13
+ imdb.com
14
+
15
+
data/docs/walkthru.txt ADDED
@@ -0,0 +1,186 @@
1
+ $ mkdir x
2
+ $ cd x
3
+ $ rubysync create kaos
4
+ $ ls -l
5
+ total 0
6
+ drwxr-xr-x 7 ritchiey ritchiey 238 Aug 21 16:47 kaos
7
+ $ cd kaos
8
+ $ ls -l
9
+ total 0
10
+ drwxr-xr-x 2 ritchiey ritchiey 68 Aug 21 16:47 connectors
11
+ drwxr-xr-x 2 ritchiey ritchiey 68 Aug 21 16:47 db
12
+ drwxr-xr-x 2 ritchiey ritchiey 68 Aug 21 16:47 log
13
+ drwxr-xr-x 2 ritchiey ritchiey 68 Aug 21 16:47 pipelines
14
+ drwxr-xr-x 5 ritchiey ritchiey 170 Aug 21 16:47 shared
15
+ $ rubysync connector hr -t csv_file
16
+ $ rubysync connector kaos_vault -t xml$ mate ..
17
+ $ cat connectors/hr_connector.rb
18
+ class HrConnector < RubySync::Connectors::CsvFileConnector
19
+
20
+ field_names ['id', 'first_name', 'last_name', 'skills']
21
+ path_field 'id'
22
+ in_path '/Users/ritchiey/x/in'
23
+ out_path '/Users/ritchiey/x/out'
24
+ in_glob '*.csv'
25
+ out_extension '.csv'
26
+
27
+ end
28
+ $ cat connectors/kaos_vault_connector.rb
29
+ class KaosVaultConnector < RubySync::Connectors::XmlConnector
30
+ #
31
+ # "filename" should be the full name of the file containing
32
+ # the xml representation of the synchronized content.
33
+ # You probably want to change this:
34
+ #
35
+ filename "/Users/ritchiey/x/kaos.xml"
36
+
37
+ end
38
+ $ rubysync fields hr
39
+ id
40
+ first_name
41
+ last_name
42
+ skills
43
+ $ rubysync fields kaos_vault
44
+
45
+ $ rubysync pipeline hr_import -C hr -V kaos_vault
46
+ $ # Now edit the pipeline config
47
+ $ # Actually, here's the pipeline before editing
48
+ $ cat pipelines/hr_import_pipeline.rb
49
+ class HrImportPipeline < RubySync::Pipelines::BasePipeline
50
+
51
+ client :hr
52
+
53
+ vault :kaos_vault
54
+
55
+ # Remove any fields that you don't want to set in the client from the vault
56
+ allow_out :allow, :these, :fields, :through
57
+
58
+ # Remove any fields that you don't want to set in the vault from the client
59
+ allow_in :allow, :these, :fields, :through
60
+
61
+ # If the client and vault have different names for the same field, define the
62
+ # the mapping here. For example, if the vault has a field called "first name" and
63
+ # the client has a field called givenName you may put:
64
+ # 'first name' => 'givenName'
65
+ # separate each mapping with a comma.
66
+ # The following fields were detected on the client:
67
+ # 'id', 'first_name', 'last_name', 'skills'
68
+ map_vault_to_client({
69
+ #'allow' => 'a_client_field',
70
+ #'these' => 'a_client_field',
71
+ #'fields' => 'a_client_field',
72
+ #'through' => 'a_client_field'
73
+ })
74
+
75
+ # "in" means going from client to vault
76
+ #in_transform do
77
+ #end
78
+
79
+ # "out" means going from vault to client
80
+ #out_transform do
81
+ #end
82
+
83
+ end
84
+ $ # now edit the pipeline
85
+ $ cat pipelines/hr_import_pipeline.rb
86
+ class HrImportPipeline < RubySync::Pipelines::BasePipeline
87
+
88
+ client :hr
89
+
90
+ vault :kaos_vault
91
+
92
+ # Remove any fields that you don't want to set in the client from the vault
93
+ allow_out :id, :first_name, :last_name
94
+
95
+ # Remove any fields that you don't want to set in the vault from the client
96
+ allow_in :id, :first_name, :last_name
97
+
98
+ # If the client and vault have different names for the same field, define the
99
+ # the mapping here. For example, if the vault has a field called "first name" and
100
+ # the client has a field called givenName you may put:
101
+ # 'first name' => 'givenName'
102
+ # separate each mapping with a comma.
103
+ # The following fields were detected on the client:
104
+ # 'id', 'first_name', 'last_name', 'skills'
105
+ map_vault_to_client({
106
+ #'allow' => 'a_client_field',
107
+ #'these' => 'a_client_field',
108
+ #'fields' => 'a_client_field',
109
+ #'through' => 'a_client_field'
110
+ })
111
+
112
+ # "in" means going from client to vault
113
+ #in_transform do
114
+ #end
115
+
116
+ # "out" means going from vault to client
117
+ #out_transform do
118
+ #end
119
+
120
+ end
121
+ $ # so all we've done specified which fields to allow in an out. Notice that we left "skills" out.
122
+ $ # also note that we're exporting as well as importing for now
123
+ $ # so now we can run our sync
124
+ $ rubysync once hr_import
125
+ $ # oops. Forgot to give it any data
126
+ $ # notice, though how it created the import and export directories for us.
127
+ $ cd ../in
128
+ $ # lets make some henchmen
129
+ $ cat > henchmen.csv
130
+ bobby,BareKnuckle,Bobby,pugilism:yoga
131
+ tt,Testy,Terry,kidnapping:interrogation:juggling
132
+ $ ls -l
133
+ total 8
134
+ -rw-r--r-- 1 ritchiey ritchiey 87 Aug 21 17:11 henchmen.csv
135
+ $ # we need to run rubysync from within the configuration directory
136
+ $ cd ../kaos/
137
+ $ rubysync once hr_import
138
+ $ ls -l ../in
139
+ total 8
140
+ -rw-r--r-- 1 ritchiey ritchiey 87 Aug 21 17:11 henchmen.csv.bak
141
+ $ # note that the csv has been renamed
142
+ $ # lets have a look at the xml file thats been created
143
+ $ cat ../kaos.xml
144
+ <opt>
145
+ <tt>
146
+ <id>tt</id>
147
+ <first_name>Testy</first_name>
148
+ <last_name>Terry</last_name>
149
+ </tt>
150
+ <bobby>
151
+ <id>bobby</id>
152
+ <first_name>BareKnuckle</first_name>
153
+ <last_name>Bobby</last_name>
154
+ </bobby>
155
+ </opt>
156
+ $ # ok, great. We've got a couple of records in there.
157
+ $ # remember that we also configured an output directory
158
+ $ ls -l ../out
159
+ $ # no output. ok what if we modify the vault itself
160
+ $ cat ../kaos.xml
161
+ <opt>
162
+ <tt>
163
+ <id>tt</id>
164
+ <first_name>Testy</first_name>
165
+ <last_name>Terry</last_name>
166
+ </tt>
167
+ <bobby>
168
+ <id>bobby</id>
169
+ <first_name>BareKnuckle</first_name>
170
+ <last_name>Bobby</last_name>
171
+ </bobby>
172
+ <desd>
173
+ <id>desd</id>
174
+ <first_name>Dangerous</first_name>
175
+ <last_name>Des</last_name>
176
+ </desd>
177
+ </opt>
178
+ $ # and run it again
179
+ $ rubysync once hr_import
180
+ $ ls -l ../out
181
+ total 8
182
+ -rw-r--r-- 1 ritchiey ritchiey 20 Aug 21 17:25 20070821172521.csv
183
+ $ cat ../out/20070821172521.csv
184
+ desd,Dangerous,Des,
185
+ $ # So it just sends through the changes to the file
186
+ $ # How's it doingr
data/lib/ruby_sync.rb CHANGED
@@ -21,28 +21,9 @@ require 'rubygems'
21
21
  require 'active_support'
22
22
  require 'ruby_sync/util/utilities'
23
23
  require 'ruby_sync/util/metaid'
24
- #require 'ruby_sync/connectors/base_connector'
25
- #require 'ruby_sync/pipelines/base_pipeline'
26
24
  require 'ruby_sync/operation'
27
25
  require 'ruby_sync/event'
28
26
 
29
- class Object
30
-
31
- # If not already an array, slip into one
32
- def as_array
33
- (instance_of? Array)? self : [self]
34
- end
35
-
36
- # Make the log method globally available
37
- def log
38
- unless defined? @@log
39
- @@log = Logger.new(STDOUT)
40
- #@@log.level = Logger::DEBUG
41
- @@log.datetime_format = "%H:%M:%S"
42
- end
43
- @@log
44
- end
45
- end
46
27
 
47
28
  class Module
48
29
  # Add an option that will be defined by a class method, stored in a class variable
@@ -66,17 +47,17 @@ class Configuration
66
47
  include RubySync::Utilities
67
48
 
68
49
  def initialize
69
- include_in_search_path "#{base_path}/pipelines"
70
- include_in_search_path "#{base_path}/connectors"
71
- include_in_search_path "#{base_path}/shared/connectors"
72
- include_in_search_path "#{base_path}/shared/pipelines"
73
- include_in_search_path "#{base_path}/shared/lib"
50
+ base_path and include_in_search_path "#{base_path}/pipelines"
51
+ base_path and include_in_search_path "#{base_path}/connectors"
52
+ base_path and include_in_search_path "#{base_path}/shared/connectors"
53
+ base_path and include_in_search_path "#{base_path}/shared/pipelines"
54
+ base_path and include_in_search_path "#{base_path}/shared/lib"
74
55
 
75
56
  lib_path = File.dirname(__FILE__)
76
57
  require_all_in_dir "#{lib_path}/ruby_sync/connectors", "*_connector.rb"
77
- require_all_in_dir "#{base_path}/shared/connectors", "*_connector.rb"
58
+ base_path and require_all_in_dir "#{base_path}/shared/connectors", "*_connector.rb"
78
59
  require_all_in_dir "#{lib_path}/ruby_sync/pipelines", "*_pipeline.rb"
79
- require_all_in_dir "#{base_path}/shared/pipelines", "*_pipeline.rb"
60
+ base_path and require_all_in_dir "#{base_path}/shared/pipelines", "*_pipeline.rb"
80
61
  end
81
62
 
82
63
  # We find the first directory in the search path that is a parent of the specified
@@ -108,6 +89,3 @@ class Configuration
108
89
  end
109
90
 
110
91
  Configuration.new
111
-
112
-
113
-
@@ -14,7 +14,8 @@
14
14
  # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
15
15
 
16
16
  require 'ruby_sync/connectors/connector_event_processing'
17
- require 'dbm'
17
+ require 'yaml'
18
+ require 'yaml/dbm'
18
19
  require 'digest/md5'
19
20
 
20
21
  module RubySync::Connectors
@@ -23,11 +24,15 @@ module RubySync::Connectors
23
24
  include RubySync::Utilities
24
25
  include ConnectorEventProcessing
25
26
 
26
- attr_accessor :once_only, :name, :is_vault
27
+ attr_accessor :once_only, :name, :is_vault, :pipeline
27
28
  option :dbm_path
28
29
 
29
30
  # set a default dbm path
30
- def dbm_path() "#{base_path}/db/#{name}"; end
31
+ def dbm_path()
32
+ p = "#{base_path}/db"
33
+ ::FileUtils.mkdir_p p
34
+ ::File.join(p,name)
35
+ end
31
36
 
32
37
  # Stores association keys indexed by path:association_context
33
38
  def path_to_association_dbm_filename
@@ -53,7 +58,7 @@ module RubySync::Connectors
53
58
  self.name = options[:name]
54
59
  self.is_vault = options[:is_vault]
55
60
  if is_vault && !can_act_as_vault?
56
- raise Exception.new("#{self.class.name} can't act as an identity vault.")
61
+ raise "#{self.class.name} can't act as an identity vault."
57
62
  end
58
63
  options.each do |key, value|
59
64
  if self.respond_to? "#{key}="
@@ -64,16 +69,20 @@ module RubySync::Connectors
64
69
  end
65
70
  end
66
71
 
72
+
73
+ # Subclasses must override this. Called by perform_add to actually
74
+ # store the new record in the datastore. Returned value will be used
75
+ # as the association id if this connector is acting as the client.
76
+ def add id, operations
77
+ raise "add method not implemented"
78
+ end
79
+
67
80
 
68
81
  # Override this to return a string that will be included within the class definition of
69
82
  # of configurations based on your connector.
70
83
  def self.sample_config
71
84
  end
72
85
 
73
- # Override this to perform actions that must be performed the
74
- # when the connector starts running. (Eg, opening network connections)
75
- def started
76
- end
77
86
 
78
87
  # Subclasses must override this to
79
88
  # interface with the external system and generate entries for every
@@ -107,31 +116,32 @@ module RubySync::Connectors
107
116
  unless self[key]
108
117
  yield RubySync::Event.delete(self, key)
109
118
  dbm.delete key
119
+ if is_vault? and @pipeline
120
+ association = association_for @pipeline.association_context, key
121
+ remove_association association
122
+ end
110
123
  end
111
124
  end
112
125
  end
113
126
  end
114
127
 
115
128
  def digest(o)
116
- Digest::MD5.hexdigest(Marshal.dump(o))
129
+ Digest::MD5.hexdigest(o.to_yaml)
117
130
  end
118
131
 
119
- # Override this to perform actions that must be performed when
120
- # the connector exits (eg closing network conections).
121
- def stopped; end
122
132
 
123
133
 
124
134
  # Call each_change repeatedly (or once if in once_only mode)
125
135
  # to generate events.
126
136
  # Should generally only be called by the pipeline to which it is attached.
127
137
  def start &blk
128
- log.info "#{name}: Started"
138
+ log.debug "#{name}: Started"
129
139
  @running = true
130
- started()
140
+ sync_started()
131
141
  while @running
132
142
  each_change do |event|
133
143
  if event.type == :force_resync
134
- each_entry &blk
144
+ each_entry(&blk)
135
145
  next
136
146
  end
137
147
  if is_delete_echo?(event) || is_echo?(event)
@@ -150,9 +160,26 @@ module RubySync::Connectors
150
160
  sleep 1
151
161
  end
152
162
  end
153
- stopped
163
+ sync_stopped
154
164
  end
155
165
 
166
+
167
+ # Called by start() after last call to each_change or each_entry
168
+ def sync_stopped; end
169
+
170
+ # Called by start() before first call to each_change or each_entry
171
+ def sync_started; end
172
+
173
+ # Override this to perform actions that must be performed the
174
+ # when the connector starts running. (Eg, opening network connections)
175
+ def started
176
+ end
177
+
178
+ # Override this to perform actions that must be performed when
179
+ # the connector exits (eg closing network conections).
180
+ def stopped; end
181
+
182
+
156
183
  # Politely stop the connector.
157
184
  def stop
158
185
  log.info "#{name}: Attempting to stop"
@@ -198,12 +225,6 @@ module RubySync::Connectors
198
225
  # Whether this connector is capable of acting as a vault.
199
226
  # The vault is responsible for storing the association key of the client application
200
227
  # and must be able to retrieve records for that association key.
201
- # Typically, databases and directories can act as vaults, text documents and HR or finance
202
- # applications probably can't.
203
- # To enable a connector to act as a vault, define the following methods:
204
- # => path_for_foreign_key(pipeline_id, key)
205
- # => foreign_key_for(path)
206
- # and associate_with_foreign_key(key, path).
207
228
  def can_act_as_vault?
208
229
  defined? associate and
209
230
  defined? path_for_association and
@@ -215,11 +236,10 @@ module RubySync::Connectors
215
236
 
216
237
  # Store association for the given path
217
238
  def associate association, path
218
- DBM.open(path_to_association_dbm_filename) do |dbm|
219
- assocs_string = dbm[path.to_s]
220
- assocs = (assocs_string)? Marshal.load(assocs_string) : {}
221
- assocs[association.context] = association.key
222
- dbm[path.to_s] = Marshal.dump(assocs)
239
+ YAML::DBM.open(path_to_association_dbm_filename) do |dbm|
240
+ assocs = dbm[path.to_s] || {}
241
+ assocs[association.context.to_s] = association.key.to_s
242
+ dbm[path.to_s] = assocs
223
243
  end
224
244
  DBM.open(association_to_path_dbm_filename) do |dbm|
225
245
  dbm[association.to_s] = path
@@ -233,45 +253,28 @@ module RubySync::Connectors
233
253
  end
234
254
  end
235
255
 
236
- # Default implementation does nothing
237
256
  def associations_for path
238
- DBM.open(path_to_association_dbm_filename) do |dbm|
239
- assocs_string = dbm[path.to_s]
240
- assocs = (assocs_string)? Marshal.load(assocs_string) : {}
257
+ YAML::DBM.open(path_to_association_dbm_filename) do |dbm|
258
+ assocs = dbm[path.to_s]
241
259
  assocs.values
242
260
  end
243
261
  end
244
262
 
245
- # Default implementation does nothing
263
+
246
264
  def remove_association association
247
265
  path = nil
248
266
  DBM.open(association_to_path_dbm_filename) do |dbm|
249
267
  return unless path =dbm.delete(association.to_s)
250
268
  end
251
- DBM.open(path_to_association_dbm_filename) do |dbm|
252
- assocs_string = dbm[path]
253
- assocs = (assocs_string)? Marshal.load(assocs_string) : {}
254
- assocs.delete(association.context) and dbm[path.to_s] = Marshal.dump(assocs)
269
+ YAML::DBM.open(path_to_association_dbm_filename) do |dbm|
270
+ assocs = dbm[path.to_s]
271
+ assocs.delete(association.context) and dbm[path.to_s] = assocs
255
272
  end
256
273
  end
257
274
 
258
- # Could be more efficient for the default case where the
259
- # associations are actually stored as a serialized hash but
260
- # then it wouldn't be as generic and other implementations would
261
- # have to reimplement it.
262
- # def association_key_for context, path
263
- # raise "#{name} is not a vault." unless is_vault?
264
- # associations_for(path).each do |assoc|
265
- # (c, key) = assoc.split(RubySync::Association.delimiter, 2)
266
- # return key if c == context
267
- # end
268
- # return nil
269
- # end
270
-
271
275
  def association_key_for context, path
272
- DBM.open(path_to_association_dbm_filename) do |dbm|
273
- assocs_string = dbm[path.to_s]
274
- assocs = (assocs_string)? Marshal.load(assocs_string) : {}
276
+ YAML::DBM.open(path_to_association_dbm_filename) do |dbm|
277
+ assocs = dbm[path.to_s] || {}
275
278
  assocs[context.to_s]
276
279
  end
277
280
  end
@@ -356,40 +359,6 @@ module RubySync::Connectors
356
359
  end
357
360
 
358
361
 
359
- # Performs the given operations on the given record. The record is a
360
- # Hash in which each key is a field name and each value is an array of
361
- # values for that field.
362
- # Operations is an Array of RubySync::Operation objects to be performed on the record.
363
- def perform_operations operations, record={}
364
- operations.each do |op|
365
- unless op.instance_of? RubySync::Operation
366
- log.warn "!!!!!!!!!! PROBLEM, DUMP FOLLOWS: !!!!!!!!!!!!!!"
367
- p op
368
- end
369
- case op.type
370
- when :add
371
- if record[op.subject]
372
- existing = record[op.subject].as_array
373
- (existing & op.values).empty? or
374
- raise Exception.new("Attempt to add duplicate elements to #{name}")
375
- record[op.subject] = existing + op.values
376
- else
377
- record[op.subject] = op.values
378
- end
379
- when :replace
380
- record[op.subject] = op.values
381
- when :delete
382
- if value == nil || value == "" || value == []
383
- record.delete(op.subject)
384
- else
385
- record[op.subject] -= values
386
- end
387
- else
388
- raise Exception.new("Unknown operation '#{op.type}'")
389
- end
390
- end
391
- return record
392
- end
393
362
 
394
363
 
395
364
  # Return an array of possible fields for this connector.