rubysync 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
data/bin/rubysync CHANGED
@@ -224,14 +224,14 @@ puts <<"END"
224
224
  connectors/my_db_connector.rb ;how to connect to your DB or Rails app.
225
225
 
226
226
  And enter:
227
- $ rubysync pipeline my_pipeline -C my_csv -V my_db
227
+ $ rubysync pipeline my -C my_csv -V my_db
228
228
 
229
- You would then edit the file +pipelines/my_pipeline.rb+ to configure the
229
+ You would then edit the file pipelines/my_pipeline.rb to configure the
230
230
  policy for synchronizing between the two connectors.
231
231
 
232
232
  You may then execute the pipeline in one-shot mode (daemon mode is coming):
233
233
 
234
- $ rubysync once my_pipeline
234
+ $ rubysync once my
235
235
  END
236
236
  end
237
237
 
@@ -1,8 +1,6 @@
1
1
  class HrDbConnector < RubySync::Connectors::ActiveRecordConnector
2
- options(
3
- :name=>"HR Database",
4
- :application=>"#{File.dirname(__FILE__)}/../../ar_webapp",
5
- :model=>:person
6
- )
2
+ name "HR Database",
3
+ application "#{File.dirname(__FILE__)}/../../ar_webapp",
4
+ model :person
7
5
 
8
6
  end
@@ -0,0 +1,10 @@
1
+ class MyCsvConnector < RubySync::Connectors::CsvFileConnector
2
+
3
+ field_names ['id', 'given_name', 'surname']
4
+ path_field :id
5
+ in_path '/tmp/rubysync/in'
6
+ out_path '/tmp/rubysync/out'
7
+ in_glob '*.csv'
8
+ out_extension '.csv'
9
+
10
+ end
@@ -0,0 +1,7 @@
1
+ class MyDbConnector < RubySync::Connectors::ActiveRecordConnector
2
+
3
+ application "#{File.dirname(__FILE__)}/../../ar_webapp"
4
+ model 'person'
5
+
6
+
7
+ end
@@ -1,11 +1,11 @@
1
- class FinancePipeline < RubySync::Pipelines::BasePipeline
1
+ class MyPipeline < RubySync::Pipelines::BasePipeline
2
2
 
3
- client :finance
3
+ client :my_csv
4
4
 
5
- vault :hr_db
5
+ vault :my_db
6
6
 
7
7
  # Remove any fields that you don't want to set in the client from the vault
8
- allow_out :first_name
8
+ allow_out :first_name, :last_name
9
9
 
10
10
  # Remove any fields that you don't want to set in the vault from the client
11
11
  allow_in :first_name, :last_name
@@ -16,17 +16,17 @@ class FinancePipeline < RubySync::Pipelines::BasePipeline
16
16
  # 'first name' => 'givenName'
17
17
  # separate each mapping with a comma.
18
18
  # The following fields were detected on the client:
19
- # 'username', 'name', 'email'
19
+ # 'id', 'given_name', 'surname'
20
20
  map_vault_to_client(
21
- 'first_name' => 'name'
22
- #'last_name' => 'a_client_field'
21
+ 'first_name' => 'given_name',
22
+ 'last_name' => 'surname'
23
23
  )
24
24
 
25
- # in means going from client to vault
25
+ # "in" means going from client to vault
26
26
  #in_transform do
27
27
  #end
28
28
 
29
- # out means going from vault to client
29
+ # "out" means going from vault to client
30
30
  #out_transform do
31
31
  #end
32
32
 
data/lib/net/ldif.rb ADDED
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env ruby -w
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
+ require 'base64'
17
+
18
+ module Net
19
+
20
+ class ParsingError < StandardError; end
21
+
22
+
23
+
24
+ # Represents a mod-spec structure from RFC2849
25
+ class LDIFModSpec # :nodoc:
26
+ attr_accessor :type, :attribute, :values
27
+ def initialize(type, attribute)
28
+ self.type = type
29
+ self.attribute = attribute
30
+ self.values = nil
31
+ end
32
+
33
+ def add_change name, value
34
+ if @values
35
+ if @values.kind_of? Array
36
+ @values << value
37
+ else
38
+ @values = [@values, value]
39
+ end
40
+ else
41
+ @values = value
42
+ end
43
+ end
44
+
45
+ def changes() [@type.to_sym, @attribute, @values] end
46
+ end
47
+
48
+ # Represents an LDIF change record as defined in RFC2849.
49
+ # The RFC specifies that an LDIF file can contain either
50
+ # attrval records (which are just content) or change records
51
+ # which represent changes to the content.
52
+ # This parser puts both types into the change record structure
53
+ # Ldif attrval records simply become change records of type
54
+ # 'add'.
55
+ class ChangeRecord
56
+ attr_accessor :dn, :changetype, :data
57
+
58
+ def initialize(dn, changetype='add')
59
+ self.dn = dn
60
+ self.changetype = changetype
61
+ @mod_spec = nil
62
+ @data = nil
63
+ end
64
+
65
+ def to_s
66
+ "#{@dn}\n#{@changetype}\n" +
67
+ @data.inspect
68
+ end
69
+
70
+
71
+ def add_value(name, value) # :nodoc:
72
+ # Changetype specified before any other fields
73
+ if name == 'changetype' and !@data
74
+ @changetype = value
75
+ return
76
+ end
77
+
78
+ if name == '-' and @changetype != 'modify'
79
+ raise ParsingError.new("'-' is only valid in LDIF change records when changetype is modify")
80
+ end
81
+
82
+ # Just an ordinary name value pair
83
+ case changetype
84
+ when 'add': add_content(name, value)
85
+ when 'delete':
86
+ raise ParsingError.new("Didn't expect content when changetype was 'delete', (#{name}:#{value})")
87
+ when 'modify': add_modification(name, value)
88
+ when 'modrdn': add_moddn_value(name,value)
89
+ when 'moddn': add_moddn_value(name, value)
90
+ else
91
+ raise ParsingError.new("Unknown changetype: '#{changetype}'")
92
+ end
93
+ end
94
+
95
+
96
+ def add_modification(name, value)
97
+ @data ||= []
98
+ if name == '-'
99
+ @mod_spec or
100
+ raise ParsingError.new("'-' in LDIF modify record before any actual changes")
101
+ @data << @mod_spec.changes
102
+ @mod_spec = nil
103
+ return
104
+ end
105
+
106
+ if @mod_spec
107
+ @mod_spec.add_change name,value
108
+ elsif %w{add delete replace}.include? name
109
+ @mod_spec = LDIFModSpec.new(name, value)
110
+ end
111
+ end
112
+
113
+ def add_moddn_value(name, value)
114
+ #TODO: implement
115
+ #raise Exception.new("Sorry, not yet implemented")
116
+ end
117
+
118
+ def add_content(key, value)
119
+ @data ||= {}
120
+ if @data[key]
121
+ if @data[key].kind_of? Array
122
+ @data[key] << value
123
+ else
124
+ @data[key] = [@data[key], value]
125
+ end
126
+ else
127
+ @data[key] = value
128
+ end
129
+ end
130
+
131
+ end # of class ChangeRecord
132
+
133
+
134
+ class URLForValue < String # :nodoc:
135
+ def to_s
136
+ filename = self.sub(/^file:\/\//oi, '')
137
+ File.read(filename)
138
+ end
139
+ end
140
+
141
+ class Base64EncodedString < String # :nodoc:
142
+ def to_s() Base64.decode64 self; end
143
+ end
144
+
145
+ class LDIF
146
+
147
+ #FILL = '\s*'
148
+ # TODO: Attribute description should be more structured than this
149
+ ATTRIBUTE_DESCRIPTION = '[a-zA-Z0-9.;-]+'
150
+ SAFE_INIT_CHAR = '[\x01-\x09\x0b-\x0c\x0e-\x1f\x21-\x39\x3b\x3d-\x7f]'
151
+ SAFE_CHAR = '[\x01-\x09\x0b-\x0c\x0e-\x7f]'
152
+ SAFE_STRING = "#{SAFE_INIT_CHAR}#{SAFE_CHAR}*"
153
+ BASE64_STRING = '[\x2b\x2f\x30-\x39\x3d\x41-\x5a\x61-\x7a]*'
154
+
155
+
156
+ # Yields Net::ChangeRecord for each LDIF record in the file.
157
+ # If the file contains attr-val (content) records, they are
158
+ # yielded as Net::ChangeRecords of type 'add'.
159
+ # If no block is given, then returns an array of the
160
+ # Net::ChangeRecord objects.
161
+ def self.parse(stream)
162
+ return parse_to_array(stream) unless block_given?
163
+ type = nil
164
+ record_number = 0
165
+ record = nil
166
+ tokenize(stream) do |name, value|
167
+
168
+ # version-spec
169
+ if (name == 'version' and record_number == 0)
170
+ value == '1' or raise ParsingError.new("Don't know how to parse LDIF version '#{value}'")
171
+ next
172
+ end
173
+
174
+ # Blank line
175
+ # Unless I'm reading the spec wrong, blank lines don't seem to mean much
176
+ # Yet in all the examples, the records seem to be separated by blank lines.
177
+ # TODO: Check whether blank lines mean anything
178
+ next if (name == nil)
179
+
180
+ name.downcase!
181
+ # DN - start a new record
182
+ if name == 'dn'
183
+ # Process existing record
184
+ yield(record) if record
185
+ record = ChangeRecord.new(value)
186
+ next
187
+ end
188
+
189
+ record or raise ParsingException.new("Expecting a dn, got #{name}: #{value}")
190
+ record.add_value name, value
191
+ end # of tokens
192
+ yield(record) if record
193
+ end
194
+
195
+ def self.parse_to_array(stream)
196
+ changes = []
197
+ parse(stream) do |change|
198
+ changes << change
199
+ end
200
+ changes
201
+ end
202
+
203
+ # Yields a series of pairs of the form name, value found in the
204
+ # given stream. Comments (lines starting with #) are removed,
205
+ # base64 values are decoded and folded lines are unfolded.
206
+ # Blank lines are yielded with a nil name, nil value pair.
207
+ # Lines containing only a hyphen are yielded as a name="-",
208
+ # value="-" pair.
209
+ # Values specified as file:// urls as described in RFC2849 are
210
+ # replaced with the contents of the specified file.
211
+ def self.tokenize(stream)
212
+
213
+ foldable = false
214
+ comment = false
215
+ name = nil
216
+ value = ""
217
+ line_number = 0
218
+ stream.each_line do |line|
219
+ line_number += 1
220
+
221
+ # Blank line
222
+ if line.strip.length == 0
223
+ yield(name, value.to_s) if name;name = nil;value = ""
224
+ yield nil,nil
225
+ foldable = false
226
+ comment = false
227
+ next
228
+ end
229
+
230
+ # Line extension
231
+ if foldable and line[0,1] == ' '
232
+ value << line.chop[1..-1] unless comment
233
+ next
234
+ end
235
+
236
+ # Comment
237
+ if line[0,1] == '#'
238
+ yield(name, value.to_s) if name;name = nil;value = ""
239
+ comment = true
240
+ foldable = true
241
+ next
242
+ end
243
+
244
+ # Base64 Encoded name:value pair
245
+ if line =~ /^(#{ATTRIBUTE_DESCRIPTION})::\s*(#{BASE64_STRING})/oi
246
+ yield(name, value.to_s) if name
247
+ name = $1
248
+ value = Base64EncodedString.new($2)
249
+ comment = false
250
+ foldable = false # It is but we've got a separate rule for it
251
+ next
252
+ end
253
+
254
+ # URL value
255
+ if line =~ /^(#{ATTRIBUTE_DESCRIPTION}):<\s*(#{SAFE_STRING})/oi
256
+ yield(name, value.to_s) if name
257
+ name = $1
258
+ value = URLForValue.new($2)
259
+ comment = false
260
+ foldable = true
261
+ next
262
+ end
263
+
264
+ # Name:Value pair
265
+ if line =~ /^(#{ATTRIBUTE_DESCRIPTION}):\s*(#{SAFE_STRING})/oi
266
+ yield(name, value.to_s) if name
267
+ name = $1; value = $2
268
+ foldable = true
269
+ comment = false
270
+ next
271
+ end
272
+
273
+ # Hyphen
274
+ if line =~ /^-/o
275
+ yield(name, value.to_s) if name;name = nil;value = ""
276
+ yield('-','-')
277
+ foldable = false
278
+ comment = false
279
+ next
280
+ end
281
+
282
+ # Continuation of Base64 Encoded value?
283
+ if value.kind_of?(Base64EncodedString) and line =~ /^ (#{BASE64_STRING})/oi
284
+ value << $1
285
+ next
286
+ end
287
+
288
+ raise ParsingError.new("Unexpected LDIF at line: #{line_number}")
289
+ end # of file
290
+ yield(name, value.to_s) if name
291
+ line_number
292
+ end
293
+
294
+ private
295
+
296
+
297
+ def process(record, type) # :nodoc:
298
+
299
+ end
300
+
301
+ end
302
+ end
@@ -27,27 +27,29 @@ module RubySync::Connectors
27
27
  class ActiveRecordConnector < RubySync::Connectors::BaseConnector
28
28
 
29
29
 
30
- attr_accessor :ar_class, :model, :application, :rails_env, :db_type, :db_host, :db_name
31
-
30
+ option :ar_class, :model, :application, :rails_env, :db_type, :db_host, :db_name, :db_config
31
+ rails_env 'development'
32
+ db_type 'mysql'
33
+ db_host 'localhost'
34
+ db_name "rubysync_#{get_rails_env}"
35
+ # Default db_config in case we're not sucking the config out of a rails app
36
+ db_config(
37
+ :adapter=>get_db_type,
38
+ :host=>get_db_host,
39
+ :database=>get_db_name
40
+ )
41
+ model :user
42
+
43
+
32
44
  def initialize options={}
33
45
  super options
34
- @rails_env ||= 'development'
35
- @db_type ||= 'mysql'
36
- @db_host ||= 'localhost'
37
- @db_name ||= "rubysync_#{@rails_env}"
38
- # Default db_config in case we're not sucking the config out of a rails app
39
- @db_config = {
40
- :adapter=>@db_type,
41
- :host=>@db_host,
42
- :database=>@db_name
43
- }
44
46
 
45
47
  # Rails app specified, use it to configure
46
- if defined? @application
48
+ if application
47
49
  # Load the database configuration
48
- rails_app_path = File.expand_path(@application, File.dirname(__FILE__))
50
+ rails_app_path = File.expand_path(application, File.dirname(__FILE__))
49
51
  db_config_filename = File.join(rails_app_path, 'config', 'database.yml')
50
- @db_config = YAML::load(ERB.new(IO.read(db_config_filename)).result)[@rails_env]
52
+ db_config = YAML::load(ERB.new(IO.read(db_config_filename)).result)[rails_env]
51
53
  # Require the models
52
54
  log.debug "Loading the models for #{self.class.name}:"
53
55
  Dir.chdir(File.join(rails_app_path,'app','models')) do
@@ -56,13 +58,12 @@ module RubySync::Connectors
56
58
  require filename
57
59
  class_name = filename[0..-4].camelize
58
60
  klass = class_name.constantize
59
- klass.establish_connection @db_config if defined? klass.establish_connection
61
+ klass.establish_connection db_config if defined? klass.establish_connection
60
62
  end
61
63
  end
62
64
  end
63
65
 
64
- @model ||= :user
65
- @ar_class ||= @model.to_s.camelize.constantize
66
+ self.class.ar_class model.to_s.camelize.constantize
66
67
  end
67
68
 
68
69
 
@@ -73,16 +74,16 @@ module RubySync::Connectors
73
74
 
74
75
  def self.sample_config
75
76
  return <<END
76
- options(
77
- #:application=>'/path/to/a/rails/application',
78
- #:model=>'name_of_model_to_sync'
79
- )
77
+
78
+ application '/path/to/a/rails/application'
79
+ model 'name_of_model_to_sync'
80
+
80
81
  END
81
82
  end
82
83
 
83
84
 
84
85
  # Process each RubySyncEvent and then delete it from the db.
85
- def check
86
+ def each_change
86
87
  ::RubySyncEvent.find(:all).each do |rse|
87
88
  event = RubySync::Event.new(rse.event_type, self, rse.trackable_id, nil, to_payload(rse))
88
89
  yield event
@@ -102,7 +103,7 @@ END
102
103
  # the id on creation.
103
104
  def perform_add event
104
105
  log.info "Adding '#{event.target_path}' to '#{name}'"
105
- @ar_class.new() do |record|
106
+ ar_class.new() do |record|
106
107
  populate(record, perform_operations(event.payload))
107
108
  log.info(record.inspect)
108
109
  record.save!
@@ -119,21 +120,21 @@ END
119
120
 
120
121
 
121
122
  def modify(path, operations)
122
- @ar_class.find(path) do |record|
123
+ ar_class.find(path) do |record|
123
124
  populate(record, perform_operations(operations))
124
125
  record.save
125
126
  end
126
127
  end
127
128
 
128
129
  def delete(path)
129
- @ar_class.destroy path
130
+ ar_class.destroy path
130
131
  end
131
132
 
132
133
  # Implement vault functionality
133
134
 
134
135
  def associate association, path
135
136
  log.debug "Associating '#{association}' with '#{path}'"
136
- ruby_sync_association.create :synchronizable_id=>path, :synchronizable_type=>@ar_class.name,
137
+ ruby_sync_association.create :synchronizable_id=>path, :synchronizable_type=>ar_class.name,
137
138
  :context=>association.context, :key=>association.key
138
139
  end
139
140
 
@@ -147,12 +148,12 @@ END
147
148
  end
148
149
 
149
150
  def association_key_for context, path
150
- record = ruby_sync_association.find_by_synchronizable_id_and_synchronizable_type_and_context path, @model.to_s, context
151
+ record = ruby_sync_association.find_by_synchronizable_id_and_synchronizable_type_and_context path, model.to_s, context
151
152
  record and record.key
152
153
  end
153
154
 
154
155
  def associations_for(path)
155
- ruby_sync_association.find_by_synchronizable_id_and_synchronizable_type(path, @model.to_s)
156
+ ruby_sync_association.find_by_synchronizable_id_and_synchronizable_type(path, model.to_s)
156
157
  rescue ActiveRecord::RecordNotFound
157
158
  return nil
158
159
  end
@@ -165,7 +166,7 @@ END
165
166
 
166
167
 
167
168
  def [](path)
168
- @ar_class.find(path)
169
+ ar_class.find(path)
169
170
  rescue ActiveRecord::RecordNotFound
170
171
  return nil
171
172
  end
@@ -175,13 +176,13 @@ private
175
176
  def ruby_sync_association
176
177
  unless @ruby_sync_association
177
178
  @ruby_sync_association = ::RubySyncAssociation
178
- ::RubySyncAssociation.establish_connection(@db_config)
179
+ ::RubySyncAssociation.establish_connection(db_config)
179
180
  end
180
181
  @ruby_sync_association
181
182
  end
182
183
 
183
184
  def populate record, content
184
- @ar_class.content_columns.each do |c|
185
+ ar_class.content_columns.each do |c|
185
186
  record[c.name] = content[c.name][0] if content[c.name]
186
187
  end
187
188
  end
@@ -23,7 +23,6 @@ module RubySync::Connectors
23
23
 
24
24
  attr_accessor :once_only, :name, :is_vault
25
25
 
26
-
27
26
  def initialize options={}
28
27
  options = self.class.default_options.merge(options)
29
28
  once_only = false
@@ -52,18 +51,24 @@ module RubySync::Connectors
52
51
  def started; end
53
52
 
54
53
  # Subclasses must override this to
55
- # interface with the external system and generate events.
54
+ # interface with the external system and generate events for every
55
+ # entry in the scope.
56
56
  # These events are yielded to the passed in block to process.
57
57
  # This method will be called repeatedly until the connector is
58
58
  # stopped.
59
- def check; end
59
+ def each_entry; end
60
+
61
+ # Subclasses must override this to interface with the external system
62
+ # and generate an event for every change that affects items within
63
+ # the scope of this connector.
64
+ def each_change; end
60
65
 
61
66
  # Override this to perform actions that must be performed when
62
67
  # the connector exits (eg closing network conections).
63
68
  def stopped; end
64
69
 
65
70
 
66
- # Call check repeatedly (or once if in once_only mode)
71
+ # Call each_change repeatedly (or once if in once_only mode)
67
72
  # to generate events.
68
73
  # Should generally only be called by the pipeline to which it is attached.
69
74
  def start
@@ -71,7 +76,7 @@ module RubySync::Connectors
71
76
  @running = true
72
77
  started()
73
78
  while @running
74
- check do |event|
79
+ each_change do |event|
75
80
  if is_delete_echo?(event) || is_echo?(event)
76
81
  log.debug "Ignoring echoed event"
77
82
  else
@@ -156,9 +161,7 @@ module RubySync::Connectors
156
161
  defined? associations_for
157
162
  end
158
163
 
159
- # TODO: These method signatures need to change to include a connector or pipeline id so that
160
- # we can distinguish between foreign keys for the same record but different
161
- # connectors/pipelines.
164
+ # Implement these if you want your connector to be able to act as a vault
162
165
 
163
166
  # def associate association, path
164
167
  # end
@@ -166,14 +169,22 @@ module RubySync::Connectors
166
169
  # def path_for_association association
167
170
  # end
168
171
  #
169
- # def association_key_for context, path
170
- # end
171
172
  #
172
173
  # def associations_for path
173
174
  # end
174
175
  #
175
176
  # def remove_association association
176
177
  # end
178
+
179
+
180
+ def association_key_for context, path
181
+ raise "#{name} is not a vault." unless is_vault?
182
+ associations_for(path).each do |assoc|
183
+ (c, key) = assoc.split(RubySync::Association.delimiter, 2)
184
+ return key if c == context
185
+ end
186
+ return nil
187
+ end
177
188
 
178
189
  # Return the association object given the association context and path.
179
190
  # This should only be called on the vault.