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
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.