rubysync 0.0.1 → 0.0.2

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 (36) hide show
  1. data/bin/rubysync +3 -3
  2. data/examples/ims2/connectors/hr_db_connector.rb +3 -5
  3. data/examples/my_ims/connectors/my_csv_connector.rb +10 -0
  4. data/examples/my_ims/connectors/my_db_connector.rb +7 -0
  5. data/examples/my_ims/pipelines/{finance_pipeline.rb → my_pipeline.rb} +9 -9
  6. data/lib/net/ldif.rb +302 -0
  7. data/lib/ruby_sync/connectors/active_record_connector.rb +33 -32
  8. data/lib/ruby_sync/connectors/base_connector.rb +21 -10
  9. data/lib/ruby_sync/connectors/csv_file_connector.rb +17 -22
  10. data/lib/ruby_sync/connectors/file_connector.rb +11 -11
  11. data/lib/ruby_sync/connectors/ldap_connector.rb +206 -53
  12. data/lib/ruby_sync/connectors/memory_connector.rb +5 -8
  13. data/lib/ruby_sync/event.rb +11 -3
  14. data/lib/ruby_sync/operation.rb +1 -1
  15. data/lib/ruby_sync/pipelines/base_pipeline.rb +6 -0
  16. data/lib/ruby_sync/util/utilities.rb +22 -3
  17. data/lib/ruby_sync.rb +25 -2
  18. data/test/data/example1.ldif +20 -0
  19. data/test/data/example2.ldif +14 -0
  20. data/test/data/example3.ldif +13 -0
  21. data/test/data/example4.ldif +55 -0
  22. data/test/data/example5.ldif +12 -0
  23. data/test/data/example6.ldif +62 -0
  24. data/test/data/example7.ldif +8 -0
  25. data/test/test_active_record_vault.rb +3 -4
  26. data/test/test_base_pipeline.rb +58 -0
  27. data/test/test_csv_file_connector.rb +7 -7
  28. data/test/test_ldap_connector.rb +71 -13
  29. data/test/test_ldap_vault.rb +91 -0
  30. data/test/test_ldif.rb +122 -0
  31. data/test/test_utilities.rb +63 -0
  32. metadata +19 -7
  33. data/examples/my_ims/connectors/corp_directory_connector.rb +0 -12
  34. data/examples/my_ims/connectors/finance_connector.rb +0 -7
  35. data/examples/my_ims/connectors/hr_db_connector.rb +0 -7
  36. data/examples/my_ims/pipelines/hr_import_pipeline.rb +0 -29
@@ -16,21 +16,17 @@ module RubySync
16
16
  # This Connector can't act as an identity vault.
17
17
  class CsvFileConnector < RubySync::Connectors::FileConnector
18
18
 
19
- attr_accessor :field_names # A list of names representing the namesspace for this connector
20
- attr_accessor :path_field # The name of the field to use as the source_path
19
+ option :field_names, # A list of names representing the namesspace for this connector
20
+ :path_field # The name of the field to use as the source_path
21
21
 
22
-
23
- def initialize options={}
24
- super options
25
- @in_glob ||= '*.csv'
26
- @out_extension ||= '.csv'
27
- @field_names ||= []
28
- @path_field ||= (@field_names.empty?)? 'field_0': @field_names[0]
29
- end
22
+ in_glob '*.csv'
23
+ out_extension '.csv'
24
+ field_names []
25
+ path_field (get_field_names.empty?)? 'field_0': @field_names[0]
30
26
 
31
27
  # Called for each filename matching in_glob in in_path
32
28
  # Yields a modify event for each row found in the file.
33
- def check_file(filename)
29
+ def each_file_change(filename)
34
30
  CSV.open(filename, 'r') do |row|
35
31
  if defined? field_name &&row.length > field_names.length
36
32
  log.warn "#{name}: Row in file #{filename} exceeds defined field_names"
@@ -48,14 +44,13 @@ module RubySync
48
44
 
49
45
  def self.sample_config
50
46
  return <<END
51
- options(
52
- :field_names=>['names', 'of', 'the', 'columns'],
53
- :path_field=>'name_of_field_to_use_as_the_id',
54
- :in_path=>'/directory/to/read/files/from',
55
- :out_path=>'/directory/to/write/files/to',
56
- :in_glob=>'*.csv',
57
- :out_extension=>'.csv'
58
- )
47
+
48
+ field_names ['names', 'of', 'the', 'columns']
49
+ path_field 'name_of_field_to_use_as_the_id'
50
+ in_path '/directory/to/read/files/from'
51
+ out_path '/directory/to/write/files/to'
52
+ in_glob '*.csv'
53
+ out_extension '.csv'
59
54
  END
60
55
  end
61
56
 
@@ -68,7 +63,7 @@ END
68
63
 
69
64
  def write_record file, path, operations
70
65
  record = perform_operations operations
71
- line = CSV.generate_line(@field_names.map {|f| record[f]})
66
+ line = CSV.generate_line(field_names.map {|f| record[f]})
72
67
  file.puts line
73
68
  end
74
69
 
@@ -77,8 +72,8 @@ END
77
72
  # Return the value to be used as the source_path for the event given the
78
73
  # supplied row data.
79
74
  def path_for(data)
80
- if defined? @path_field
81
- return data[@path_field]
75
+ if defined? path_field
76
+ return data[path_field]
82
77
  end
83
78
  return nil
84
79
  end
@@ -14,19 +14,19 @@ module RubySync
14
14
  # and/or write received events to a file.
15
15
  class FileConnector < RubySync::Connectors::BaseConnector
16
16
 
17
- attr_accessor :in_path # scan this directory for suitable files
18
- attr_accessor :out_path # write received events to this directory
19
- attr_accessor :out_extension # the file extension of files written to out_path
20
- attr_accessor :in_glob # The filename glob for incoming files
17
+ option :in_path, # scan this directory for suitable files
18
+ :out_path, # write received events to this directory
19
+ :out_extension, # the file extension of files written to out_path
20
+ :in_glob # The filename glob for incoming files
21
21
 
22
+ out_extension ".out"
22
23
 
23
24
  def started
24
- ensure_dir_exists @in_path
25
- ensure_dir_exists @out_path
26
- @out_extension ||= ".out"
25
+ ensure_dir_exists in_path
26
+ ensure_dir_exists out_path
27
27
  end
28
28
 
29
- def check(&blk)
29
+ def each_change(&blk)
30
30
  unless in_glob
31
31
  log.error "in_glob not set on file connector. No files will be processed"
32
32
  return
@@ -35,14 +35,14 @@ module RubySync
35
35
  Dir.chdir(in_path) do |path|
36
36
  Dir.glob(in_glob) do |filename|
37
37
  log.info "#{name}: Processing '#{filename}'"
38
- check_file filename, &blk
38
+ each_file_change filename, &blk
39
39
  FileUtils.mv filename, "#{filename}.bak"
40
40
  end
41
41
  end
42
42
  end
43
43
 
44
44
  # Called for each filename matching in_glob in in_path
45
- def check_file(filename,&blk)
45
+ def each_file_change(filename,&blk)
46
46
  end
47
47
 
48
48
 
@@ -66,7 +66,7 @@ module RubySync
66
66
 
67
67
  # Generate a unique and appropriate filename within the given path
68
68
  def output_file_name
69
- File.join(@out_path, Time.now.strftime('%Y%m%d%H%M%S') + @out_extension)
69
+ File.join(out_path, Time.now.strftime('%Y%m%d%H%M%S') + out_extension)
70
70
  end
71
71
 
72
72
  end
@@ -18,11 +18,14 @@ lib_path = File.dirname(__FILE__) + '/..'
18
18
  $:.unshift lib_path unless $:.include?(lib_path) || $:.include?(File.expand_path(lib_path))
19
19
 
20
20
  require 'ruby_sync'
21
-
21
+ require 'net/ldif'
22
22
  $VERBOSE = false
23
23
  require 'net/ldap'
24
24
  #$VERBOSE = true
25
25
 
26
+ RUBYSYNC_ASSOCIATION_ATTRIBUTE = "RubySyncAssociation"
27
+ RUBYSYNC_ASSOCIATION_CLASS = "RubySyncSynchable"
28
+
26
29
  class Net::LDAP::Entry
27
30
  def to_hash
28
31
  return @myhash.dup
@@ -32,18 +35,94 @@ end
32
35
  module RubySync::Connectors
33
36
  class LdapConnector < RubySync::Connectors::BaseConnector
34
37
 
35
- attr_accessor :host, :port, :bind_method, :username, :password,
36
- :search_filter, :search_base,
37
- :association_attribute # name of the attribute in which to store the association key(s)
38
-
38
+ option :host,
39
+ :port,
40
+ :bind_method,
41
+ :username,
42
+ :password,
43
+ :search_filter,
44
+ :search_base,
45
+ :association_attribute, # name of the attribute in which to store the association key(s)
46
+ :changelog_dn
47
+
48
+ association_attribute 'RubySyncAssociation'
49
+ bind_method :simple
50
+ host 'localhost'
51
+ port 389
52
+ search_filter "cn=*"
53
+ changelog_dn "cn=changelog"
54
+
55
+ def initialize options={}
56
+ super options
57
+ @last_change_number = 1
58
+ # TODO: Persist the current CSN, for now we'll just skip to the end of the changelog
59
+ skip_existing_changelog_entries
60
+ end
61
+
62
+
39
63
  def started
40
64
  #TODO: If vault, check the schema to make sure that the association_attribute is there
41
- @association_attribute ||= 'RubySyncAssociation'
42
65
  end
43
66
 
44
- def check
45
- Net::LDAP.open(:host=>@host, :port=>@port, :auth=>auth) do |ldap|
46
- ldap.search :base => @search_base, :filter => @search_filter do |entry|
67
+
68
+ # Look for changelog entries. This is not supported by all LDAP servers
69
+ # you may need to subclass for OpenLDAP and Active Directory
70
+ # Changelog entries have these attributes
71
+ # targetdn
72
+ # changenumber
73
+ # objectclass
74
+ # changes
75
+ # changetime
76
+ # changetype
77
+ # dn
78
+ #
79
+ # TODO: Detect presence/location of changelog from root DSE
80
+ def each_change
81
+ with_ldap do |ldap|
82
+ log.debug "@last_change_number = #{@last_change_number}"
83
+ filter = "(changenumber>=#{@last_change_number})"
84
+ first = true
85
+ @full_refresh_required = false
86
+ ldap.search :base => changelog_dn, :filter =>filter do |change|
87
+ change_number = change.changenumber[0].to_i
88
+ if first
89
+ first = false
90
+ # TODO: Persist the change_number so that we don't do a full resync everytime rubysync starts
91
+ if change_number != @last_change_number
92
+ log.warn "Earliest change number (#{change_number}) differs from that recorded (#{@last_change_number})."
93
+ log.warn "A full refresh is required."
94
+ @full_refresh_required = true
95
+ break
96
+ end
97
+ else
98
+ @last_change_number = change_number if change_number > @last_change_number
99
+ # todo: A proper DN object would be nice instead of string manipulation
100
+ target_dn = change.targetdn[0].gsub(/\s*,\s*/,',')
101
+ if target_dn =~ /#{search_base}$/oi
102
+ change_type = change.changetype[0]
103
+ event = event_for_changelog_entry(change)
104
+ yield event
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+
112
+ def skip_existing_changelog_entries
113
+ with_ldap do |ldap|
114
+ filter = "(changenumber>=#{@last_change_number})"
115
+ @full_refresh_required = false
116
+ ldap.search :base => changelog_dn, :filter =>filter do |change|
117
+ change_number = change.changenumber[0].to_i
118
+ @last_change_number = change_number if change_number > @last_change_number
119
+ end
120
+ end
121
+ end
122
+
123
+ def each_entry
124
+ Net::LDAP.open(:host=>host, :port=>port, :auth=>auth) do |ldap|
125
+ ldap.search :base => search_base, :filter => search_filter do |entry|
47
126
  operations = operations_for_entry(entry)
48
127
  association_key = (is_vault?)? nil : entry.dn
49
128
  yield RubySync::Event.add(self, entry.dn, association_key, operations)
@@ -63,26 +142,18 @@ module RubySync::Connectors
63
142
  def stopped
64
143
  end
65
144
 
66
- def initialize options
67
- super options
68
- @bind_method ||= :simple
69
- @host ||= 'localhost'
70
- @port ||= 389
71
- @search_filter ||= "cn=*"
72
- end
73
145
 
74
146
 
75
147
  def self.sample_config
76
148
  return <<END
77
- options(
78
- :host=>'localhost',
79
- :port=>10389,
80
- :username=>'uid=admin,ou=system',
81
- :password=>'secret',
82
- :search_filter=>"cn=*",
83
- :search_base=>"dc=example,dc=com"
84
- # :bind_method=>:simple,
85
- )
149
+
150
+ host 'localhost'
151
+ port 10389
152
+ username 'uid=admin,ou=system'
153
+ password 'secret'
154
+ search_filter "cn=*"
155
+ search_base "dc=example,dc=com"
156
+ #:bind_method :simple
86
157
  END
87
158
  end
88
159
 
@@ -90,7 +161,7 @@ END
90
161
 
91
162
  def add(path, operations)
92
163
  with_ldap do |ldap|
93
- return false unless ldap.add :dn=>path, :attributes=>perform_operations(operations)
164
+ ldap.add :dn=>path, :attributes=>perform_operations(operations)
94
165
  end
95
166
  return true
96
167
  rescue Net::LdapException
@@ -111,50 +182,132 @@ END
111
182
  with_ldap do |ldap|
112
183
  result = ldap.search :base=>path, :scope=>Net::LDAP::SearchScope_BaseObject, :filter=>'objectclass=*'
113
184
  return nil if !result or result.size == 0
114
- result[0].to_hash
185
+ answer = {}
186
+ result[0].attribute_names.each do |name|
187
+ answer[name.to_s] = result[0][name]
188
+ end
189
+ answer
115
190
  end
116
191
  end
117
192
 
193
+ # Called by unit tests to inject data
194
+ def test_add id, details
195
+ details << RubySync::Operation.new(:add, "objectclass", ['inetOrgPerson', 'organizationalPerson', 'person', 'top', 'RubySyncSynchable'])
196
+ add id, details
197
+ end
198
+
118
199
  def target_transform event
119
- event.add_default 'objectclass', 'inetOrgUser'
120
- # TODO: Add modifier and timestamp unless LDAP dir does this automatically
200
+ #event.add_default 'objectclass', 'inetOrgUser'
201
+ #is_vault? and event.add_value 'objectclass', RUBYSYNC_ASSOCIATION_CLASS
121
202
  end
122
203
 
123
- def associate_with_foreign_key key, path
204
+ def associate association, path
124
205
  with_ldap do |ldap|
125
- ldap.add_attribute(path, @association_attribute, key.to_s)
206
+ # todo: check and warn if path is outside of search_base
207
+ ldap.modify :dn=>path, :operations=>[
208
+ [:add, RUBYSYNC_ASSOCIATION_ATTRIBUTE, association.to_s]
209
+ ]
126
210
  end
127
211
  end
128
212
 
129
- def path_for_foreign_key key
130
- entry = entry_for_foreign_key key
131
- (entry)? entry.dn : nil
213
+ def path_for_association association
214
+ with_ldap do |ldap|
215
+ filter = "#{RUBYSYNC_ASSOCIATION_ATTRIBUTE}=#{association.to_s}"
216
+ log.debug "Searching with filter: #{filter}"
217
+ results = ldap.search :base=>@search_base,
218
+ :filter=>filter,
219
+ :attributes=>[]
220
+ results or return nil
221
+ case results.length
222
+ when 0: return nil
223
+ when 1: return results[0].dn
224
+ else
225
+ raise Exception.new("Duplicate association found for #{association.to_s}")
226
+ end
227
+ end
132
228
  end
133
229
 
134
- def foreign_key_for path
135
- entry = self[path]
136
- (entry)? entry.dn : nil # TODO: That doesn't look right. Should return an association key, not a path.
137
- end
138
-
139
- def remove_foreign_key key
230
+ def associations_for path
140
231
  with_ldap do |ldap|
141
- entry = entry_for_foreign_key key
142
- if entry
143
- modify :dn=>entry.dn, :operations=>[ [:delete, @association_attribute, key] ]
232
+ results = ldap.search :base=>path,
233
+ :scope=>Net::LDAP::SearchScope_BaseObject,
234
+ :attributes=>[RUBYSYNC_ASSOCIATION_ATTRIBUTE]
235
+ unless results and results.length > 0
236
+ log.warn "Attempted association lookup on non-existent LDAP entry '#{path}'"
237
+ return []
144
238
  end
239
+ associations = results[0][RUBYSYNC_ASSOCIATION_ATTRIBUTE]
240
+ return (associations)? associations.as_array : []
145
241
  end
146
242
  end
147
-
148
- def find_associated foreign_key
149
- entry = entry_for_foreign_key key
150
- (entry)? operations_for_entry(entry) : nil
151
- end
152
243
 
244
+ def remove_association association
245
+ path = path_for_association association
246
+ with_ldap do |ldap|
247
+ ldap.modify :dn=>path, :modifications=>[
248
+ [:delete, RUBYSYNC_ASSOCIATION_ATTRIBUTE, association.to_s]
249
+ ]
250
+ end
251
+ end
252
+
253
+
254
+ # def associate_with_foreign_key key, path
255
+ # with_ldap do |ldap|
256
+ # ldap.add_attribute(path, association_attribute, key.to_s)
257
+ # end
258
+ # end
259
+ #
260
+ # def path_for_foreign_key key
261
+ # entry = entry_for_foreign_key key
262
+ # (entry)? entry.dn : nil
263
+ # end
264
+ #
265
+ # def foreign_key_for path
266
+ # entry = self[path]
267
+ # (entry)? entry.dn : nil # TODO: That doesn't look right. Should return an association key, not a path.
268
+ # end
269
+ #
270
+ # def remove_foreign_key key
271
+ # with_ldap do |ldap|
272
+ # entry = entry_for_foreign_key key
273
+ # if entry
274
+ # modify :dn=>entry.dn, :operations=>[ [:delete, association_attribute, key] ]
275
+ # end
276
+ # end
277
+ # end
278
+ #
279
+ # def find_associated foreign_key
280
+ # entry = entry_for_foreign_key key
281
+ # (entry)? operations_for_entry(entry) : nil
282
+ # end
283
+
153
284
 
154
285
  private
155
286
 
287
+ def event_for_changelog_entry cle
288
+ payload = nil
289
+ dn = cle.targetdn[0]
290
+ changetype = cle.changetype[0]
291
+ if cle.attribute_names.include? :changes
292
+ payload = []
293
+ cr = Net::LDIF.parse("dn: #{dn}\nchangetype: #{changetype}\n#{cle.changes[0]}")[0]
294
+ if changetype.to_sym == :add
295
+ # cr.data will be a hash of arrays or strings (attr-name=>[value1, value2, ...])
296
+ cr.data.each do |name, values|
297
+ payload << RubySync::Operation.add(name, values)
298
+ end
299
+ else
300
+ # cr.data will be an array of arrays of form [:action, :subject, [values]]
301
+ cr.data.each do |record|
302
+ payload << RubySync::Operation.new(record[0], record[1], record[2])
303
+ end
304
+ end
305
+ end
306
+ RubySync::Event.new(changetype, self, dn, nil, payload)
307
+ end
308
+
309
+
156
310
  def operations_for_entry entry
157
- # TODO: This could probably be done better by mixing Enumerable into Entry and then calling collect
158
311
  ops = []
159
312
  entry.each do |name, values|
160
313
  ops << RubySync::Operation.add(name, values)
@@ -164,7 +317,7 @@ private
164
317
 
165
318
  def entry_for_foreign_key key
166
319
  with_ldap do |ldap|
167
- result = ldap.search :base=>@search_base, :filter=>"#{@association_attribute}=#{key}"
320
+ result = ldap.search :base=>search_base, :filter=>"#{association_attribute}=#{key}"
168
321
  return nil if !result or result.size == 0
169
322
  result[0]
170
323
  end
@@ -173,14 +326,14 @@ private
173
326
 
174
327
  def with_ldap
175
328
  result = nil
176
- Net::LDAP.open(:host=>@host, :port=>@port, :auth=>auth) do |ldap|
329
+ Net::LDAP.open(:host=>host, :port=>port, :auth=>auth) do |ldap|
177
330
  result = yield ldap
178
331
  end
179
332
  result
180
333
  end
181
334
 
182
335
  def auth
183
- {:method=>@bind_method, :username=>@username, :password=>@password}
336
+ {:method=>bind_method, :username=>username, :password=>password}
184
337
  end
185
338
 
186
339
  # Produce an array of operation arrays suitable for the LDAP library
@@ -189,4 +342,4 @@ private
189
342
  end
190
343
 
191
344
  end
192
- end
345
+ end
@@ -16,23 +16,20 @@
16
16
 
17
17
  require "yaml"
18
18
 
19
- class Object
20
-
21
- # If not already an array, slip into one
22
- def as_array
23
- (instance_of? Array)? self : [self]
24
- end
25
- end
26
19
 
27
20
  module RubySync::Connectors
28
21
  class MemoryConnector < RubySync::Connectors::BaseConnector
29
22
 
30
- def check
23
+ def each_change
31
24
  while event = @events.shift
32
25
  yield event
33
26
  end
34
27
  end
35
28
 
29
+ def each_entry
30
+ # todo implement
31
+ end
32
+
36
33
  def is_echo? event
37
34
  event.sets_value?(:modifier, 'rubysync')
38
35
  end
@@ -17,23 +17,31 @@
17
17
 
18
18
  module RubySync
19
19
 
20
- class Association
20
+ class Association
21
21
  attr_accessor :context, # many associations will share the same context
22
22
  # it is a function of pipeline and the client connector
23
23
  # to which the association applies
24
24
  :key # the key is unique within the context and vault
25
25
 
26
+
27
+ def self.delimiter; '$'; end
28
+
26
29
  def initialize(context, key)
27
30
  @context = context
28
31
  @key = key
29
32
  end
30
33
 
31
34
  def to_s
32
- "#{context}:#{key}"
35
+ "#{context}#{self.class.delimiter}#{key}"
33
36
  end
34
37
 
35
38
  end
36
39
 
40
+
41
+ # Represents a change of some type to a record in the source datastore.
42
+ # If the event type is :add or :modify then the payload will be an
43
+ # array of RubySync::Operations describing changes to the attributes of the
44
+ # record.
37
45
  class Event
38
46
 
39
47
  attr_accessor :type, # delete, add, modify ...
@@ -199,7 +207,7 @@ module RubySync
199
207
 
200
208
 
201
209
 
202
- # Yield to block for each operation in the payload for which the the subject is
210
+ # Yield to block for each operation in the payload for which the subject is
203
211
  # the specified subject
204
212
  def each_operation_on subject
205
213
  return unless payload
@@ -43,7 +43,7 @@ module RubySync
43
43
  remove_method :type=
44
44
  def type=(type)
45
45
  unless [:add, :delete, :replace].include? type.to_sym
46
- raise Exception.new("Invalid operation type '#{value}'")
46
+ raise Exception.new("Invalid operation type '#{type}'")
47
47
  end
48
48
  @type = type
49
49
  end
@@ -104,6 +104,12 @@ module RubySync
104
104
  end
105
105
  end
106
106
 
107
+ def self.in_transform &blk
108
+ define_method :in_transform do |event|
109
+ event.meta_def :transform, &blk
110
+ event.transform
111
+ end
112
+ end
107
113
 
108
114
 
109
115
  # Called by the identity-vault connector in the 'out' thread to process events generated
@@ -108,8 +108,27 @@ module RubySync
108
108
  end
109
109
  return false
110
110
  end
111
-
112
-
113
-
111
+
112
+ # Make and instance method _name_ that returns the value set by the
113
+ # class method _name_.
114
+ # def self.class_option name
115
+ # self.class_eval "def #{name}() self.class.instance_variable_get :#{name}; end"
116
+ # self.instance_eval "def #{name}(value) @#{name}=value; end"
117
+ # end
118
+
119
+ def get_preference(name, file_name=nil)
120
+ class_name ||= get_preference_file
121
+ end
122
+
123
+ def set_preference(name)
124
+
125
+ end
126
+
127
+ def get_preference_file_path name
128
+ dir = "#{ENV[HOME]}/.rubysync"
129
+ Dir.mkdir(dir)
130
+ "#{dir}#{file}"
131
+ end
132
+
114
133
  end
115
134
  end
data/lib/ruby_sync.rb CHANGED
@@ -19,14 +19,20 @@ $:.unshift lib_path unless $:.include?(lib_path) || $:.include?(File.expand_path
19
19
  require 'rubygems'
20
20
  require 'active_support'
21
21
  require 'ruby_sync/util/utilities'
22
+ require 'ruby_sync/util/metaid'
22
23
  #require 'ruby_sync/connectors/base_connector'
23
24
  #require 'ruby_sync/pipelines/base_pipeline'
24
25
  require 'ruby_sync/operation'
25
26
  require 'ruby_sync/event'
26
27
 
27
- # Make the log method globally available
28
28
  class Object
29
29
 
30
+ # If not already an array, slip into one
31
+ def as_array
32
+ (instance_of? Array)? self : [self]
33
+ end
34
+
35
+ # Make the log method globally available
30
36
  def log
31
37
  unless defined? @@log
32
38
  @@log = Logger.new(STDOUT)
@@ -35,7 +41,24 @@ class Object
35
41
  end
36
42
  @@log
37
43
  end
38
- end
44
+ end
45
+
46
+ class Module
47
+ # Add an option that will be defined by a class method, stored in a class variable
48
+ # and accessible as an instance method
49
+ def option *names
50
+ names.each do |name|
51
+ meta_def name do |value|
52
+ class_def name do
53
+ value
54
+ end
55
+ meta_def "get_#{name}" do
56
+ value
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
39
62
 
40
63
  class Configuration
41
64
 
@@ -0,0 +1,20 @@
1
+ version: 1
2
+ dn: cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
3
+ objectclass: top
4
+ objectclass: person
5
+ objectclass: organizationalPerson
6
+ cn: Barbara Jensen
7
+ cn: Barbara J Jensen
8
+ cn: Babs Jensen
9
+ sn: Jensen
10
+ uid: bjensen
11
+ telephonenumber: +1 408 555 1212
12
+ description: A big sailing fan.
13
+
14
+ dn: cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com
15
+ objectclass: top
16
+ objectclass: person
17
+ objectclass: organizationalPerson
18
+ cn: Bjorn Jensen
19
+ sn: Jensen
20
+ telephonenumber: +1 408 555 1212
@@ -0,0 +1,14 @@
1
+ version: 1
2
+ dn:cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com
3
+ objectclass:top
4
+ objectclass:person
5
+ objectclass:organizationalPerson
6
+ cn:Barbara Jensen
7
+ cn:Barbara J Jensen
8
+ cn:Babs Jensen
9
+ sn:Jensen
10
+ uid:bjensen
11
+ telephonenumber:+1 408 555 1212
12
+ description:Babs is a big sailing fan, and travels extensively in sea
13
+ rch of perfect sailing conditions.
14
+ title:Product Manager, Rod and Reel Division