cassandra_mapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/README.rdoc +98 -0
  2. data/Rakefile.rb +11 -0
  3. data/lib/cassandra_mapper.rb +5 -0
  4. data/lib/cassandra_mapper/base.rb +19 -0
  5. data/lib/cassandra_mapper/connection.rb +9 -0
  6. data/lib/cassandra_mapper/core_ext/array/extract_options.rb +29 -0
  7. data/lib/cassandra_mapper/core_ext/array/wrap.rb +22 -0
  8. data/lib/cassandra_mapper/core_ext/class/inheritable_attributes.rb +232 -0
  9. data/lib/cassandra_mapper/core_ext/kernel/reporting.rb +62 -0
  10. data/lib/cassandra_mapper/core_ext/kernel/singleton_class.rb +13 -0
  11. data/lib/cassandra_mapper/core_ext/module/aliasing.rb +70 -0
  12. data/lib/cassandra_mapper/core_ext/module/attribute_accessors.rb +66 -0
  13. data/lib/cassandra_mapper/core_ext/object/duplicable.rb +65 -0
  14. data/lib/cassandra_mapper/core_ext/string/inflections.rb +160 -0
  15. data/lib/cassandra_mapper/core_ext/string/multibyte.rb +72 -0
  16. data/lib/cassandra_mapper/exceptions.rb +10 -0
  17. data/lib/cassandra_mapper/identity.rb +29 -0
  18. data/lib/cassandra_mapper/indexing.rb +465 -0
  19. data/lib/cassandra_mapper/observable.rb +36 -0
  20. data/lib/cassandra_mapper/persistence.rb +309 -0
  21. data/lib/cassandra_mapper/support/callbacks.rb +136 -0
  22. data/lib/cassandra_mapper/support/concern.rb +31 -0
  23. data/lib/cassandra_mapper/support/dependencies.rb +60 -0
  24. data/lib/cassandra_mapper/support/descendants_tracker.rb +41 -0
  25. data/lib/cassandra_mapper/support/inflections.rb +58 -0
  26. data/lib/cassandra_mapper/support/inflector.rb +7 -0
  27. data/lib/cassandra_mapper/support/inflector/inflections.rb +213 -0
  28. data/lib/cassandra_mapper/support/inflector/methods.rb +143 -0
  29. data/lib/cassandra_mapper/support/inflector/transliterate.rb +99 -0
  30. data/lib/cassandra_mapper/support/multibyte.rb +46 -0
  31. data/lib/cassandra_mapper/support/multibyte/utils.rb +62 -0
  32. data/lib/cassandra_mapper/support/observing.rb +218 -0
  33. data/lib/cassandra_mapper/support/support_callbacks.rb +593 -0
  34. data/test/test_helper.rb +11 -0
  35. data/test/unit/callbacks_test.rb +100 -0
  36. data/test/unit/identity_test.rb +51 -0
  37. data/test/unit/indexing_test.rb +406 -0
  38. data/test/unit/observer_test.rb +56 -0
  39. data/test/unit/persistence_test.rb +561 -0
  40. metadata +192 -0
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.setup
5
+
6
+ require 'cassandra_mapper'
7
+
8
+ require 'test/unit'
9
+ require 'shoulda'
10
+ require 'mocha'
11
+
@@ -0,0 +1,100 @@
1
+ require 'test_helper'
2
+
3
+ class CallbacksTest < Test::Unit::TestCase
4
+ context 'A CassandraModel::Base-derived instance' do
5
+ setup do
6
+ @class = Class.new(CassandraMapper::Base) do
7
+ before_save :do_before_save
8
+ after_save :do_after_save
9
+ before_create :do_before_create
10
+ after_create :do_after_create
11
+ before_update :do_before_update
12
+ after_update :do_after_update
13
+ before_destroy :do_before_destroy
14
+ after_destroy :do_after_destroy
15
+ end
16
+ @instance = @class.new
17
+ @connection = stub('connection')
18
+ @instance.stubs(:connection).returns(@connection)
19
+ @sequence = sequence('invocation_sequence')
20
+ @simple_representation = {}
21
+ @instance.stubs(:to_simple).returns(@simple_representation)
22
+ @instance.stubs(:key).returns(:foo)
23
+ # intercept freeze invocations to prevent Mocha teardown errors
24
+ @instance.stubs(:freeze)
25
+ end
26
+
27
+ context 'first save' do
28
+ setup do
29
+ @instance.stubs(:new_record?).returns(true)
30
+ end
31
+
32
+ should 'invoke the before_save, before_create, after_create, after_save callbacks' do
33
+ @instance.expects(:do_before_save).in_sequence(@sequence).returns(true)
34
+ @instance.expects(:do_before_create).in_sequence(@sequence).returns(true)
35
+ @connection.expects(:insert).in_sequence(@sequence).returns(@instance)
36
+ @instance.expects(:do_after_create).in_sequence(@sequence).returns(true)
37
+ @instance.expects(:do_after_save).in_sequence(@sequence).returns(true)
38
+ @instance.save
39
+ end
40
+ end
41
+
42
+ context 'update' do
43
+ setup do
44
+ @instance.stubs(:new_record?).returns(false)
45
+ @instance.stubs(:changed_attributes).returns([:blah])
46
+ # must provide an actual new value for an insert to take place
47
+ @simple_representation['blah'] = 'foo'
48
+ end
49
+
50
+ should 'invoke the before_save, before_update, after_update, after_save callbacks' do
51
+ @instance.expects(:do_before_save).in_sequence(@sequence).returns(true)
52
+ @instance.expects(:do_before_update).in_sequence(@sequence).returns(true)
53
+ @connection.expects(:insert).in_sequence(@sequence).returns(@instance)
54
+ @instance.expects(:do_after_update).in_sequence(@sequence).returns(true)
55
+ @instance.expects(:do_after_save).in_sequence(@sequence).returns(true)
56
+ @instance.save
57
+ end
58
+ end
59
+
60
+ context 'after being retrieved via :find' do
61
+ setup do
62
+ @key = 'some_key'
63
+ @class.maps :some_attr
64
+ @source_values = {'some_attr' => 'some_value'}
65
+ @connection = stub('connection', :multi_get => {@key => @source_values})
66
+ @class.stubs(:connection).returns(@connection)
67
+ @class.module_eval do
68
+ after_load :do_after_load
69
+
70
+ def do_after_load
71
+ self.class.after_load_invoked('some_attr' => some_attr)
72
+ end
73
+ end
74
+ end
75
+
76
+ should 'invoke the after_load callback' do
77
+ @class.expects(:after_load_invoked).with(@source_values)
78
+ @class.find(@key)
79
+ end
80
+ end
81
+
82
+ context 'destroy' do
83
+ should 'invoke the before_destroy and after_destroy callbacks on an existing row' do
84
+ @instance.stubs(:new_record?).returns(false)
85
+ @instance.expects(:do_before_destroy).once.in_sequence(@sequence).returns(true)
86
+ @class.expects(:delete).with(@instance.key).once.in_sequence(@sequence)
87
+ @instance.expects(:do_after_destroy).once.in_sequence(@sequence).returns(true)
88
+ @instance.destroy
89
+ end
90
+
91
+ should 'not invoke callbacks on a new row' do
92
+ @instance.stubs(:new_record?).returns(true)
93
+ @instance.expects(:do_before_destroy).never.in_sequence(@sequence)
94
+ @class.expects(:delete).with(@instance.key).never.in_sequence(@sequence)
95
+ @instance.expects(:do_after_destroy).never.in_sequence(@sequence)
96
+ @instance.destroy
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,51 @@
1
+ require 'test_helper'
2
+
3
+ class IdentityTest < Test::Unit::TestCase
4
+ context 'CassandraWrapper::Base' do
5
+ setup do
6
+ @class = Class.new(CassandraMapper::Base)
7
+ @values = {:a => 'Aa', :b => 'Bb', :c => 'Cc'}
8
+ @values.keys.each {|key| @class.maps key}
9
+ @instance = @class.new(@values)
10
+ end
11
+
12
+ context 'key attribute declaration' do
13
+ should 'be possible with the :key class method' do
14
+ @class.key :a
15
+ assert_equal :a, @class.key
16
+ end
17
+
18
+ should 'determine the :key value on an instance' do
19
+ @class.key :a
20
+ assert_equal @values[:a], @instance.key
21
+ @class.key :b
22
+ assert_equal @values[:b], @instance.key
23
+ end
24
+
25
+ should 'be overriddable with an instance method' do
26
+ @class.key :a
27
+ @class.module_eval do
28
+ def key
29
+ "#{a}-#{b}"
30
+ end
31
+ end
32
+ assert_equal "#{@values[:a]}-#{@values[:b]}", @instance.key
33
+ end
34
+
35
+ should 'result in default of :key if left undeclared' do
36
+ assert_equal :key, @class.key
37
+ end
38
+ end
39
+
40
+ should 'default new_record? to true' do
41
+ assert_equal true, @instance.new_record?
42
+ end
43
+
44
+ should 'allow specification of new_record flag via assignment' do
45
+ @instance.new_record = false
46
+ assert_equal false, @instance.new_record?
47
+ @instance.new_record = true
48
+ assert_equal true, @instance.new_record?
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,406 @@
1
+ require 'test_helper'
2
+ class IndexingTest < Test::Unit::TestCase
3
+ context 'A CassandraMapper::Index' do
4
+ setup do
5
+ @class = CassandraMapper::Index
6
+ end
7
+
8
+ context 'with defaults' do
9
+ setup do
10
+ @instance = @class.new
11
+ end
12
+
13
+ should 'have a nil :indexed_class attribute' do
14
+ assert_equal nil, @instance.indexed_class
15
+ end
16
+
17
+ should 'have a nil :source attribute' do
18
+ assert_equal nil, @instance.source
19
+ end
20
+
21
+ should 'have a nil :column_family attribute' do
22
+ assert_equal nil, @instance.column_family
23
+ end
24
+
25
+ should 'have :key for :indexed_identifier' do
26
+ assert_equal :key, @instance.indexed_identifier
27
+ end
28
+
29
+ should 'have nil for :name attribute' do
30
+ assert_equal nil, @instance.name
31
+ end
32
+ end
33
+
34
+ context 'with constructor options' do
35
+ setup do
36
+ @options = {:column_family => :SomeIndex,
37
+ :indexed_class => Class.new,
38
+ :source => :to_be_indexed,
39
+ :indexed_identifier => :index_column,
40
+ :name => :some_index_name}
41
+ @instance = @class.new(@options)
42
+ end
43
+
44
+ should 'get :indexed_class attribute' do
45
+ assert_equal @options[:indexed_class], @instance.indexed_class
46
+ end
47
+
48
+ should 'get :source attribute' do
49
+ assert_equal @options[:source], @instance.source
50
+ end
51
+
52
+ should 'get :column_family attribute' do
53
+ assert_equal @options[:column_family], @instance.column_family
54
+ end
55
+
56
+ should 'get :indexed_identifier attribute' do
57
+ assert_equal @options[:indexed_identifier], @instance.indexed_identifier
58
+ end
59
+
60
+ should 'get :name attribute' do
61
+ assert_equal @options[:name], @instance.name
62
+ end
63
+ end
64
+
65
+ should 'retrieve :source_for(instance) from instance :source' do
66
+ instance = @class.new(:source => :some_source)
67
+ object = mock('object to be indexed')
68
+ object.expects(:some_source).with.returns('my source')
69
+ assert_equal 'my source', instance.source_for(object)
70
+ end
71
+
72
+ should 'retrieve :indexed_identifier_for(instance) from instance :indexed_identifier' do
73
+ instance = @class.new(:indexed_identifier => :some_identifier)
74
+ object = mock('object to be indexed')
75
+ object.expects(:some_identifier).with.returns('my identifier')
76
+ assert_equal 'my identifier', instance.indexed_identifier_for(object)
77
+ end
78
+
79
+ context 'indexing method' do
80
+ setup do
81
+ @name = :this_index_name
82
+ @column_family = stub('column_family')
83
+ @source = :index_source
84
+ @indexed_class = Class.new(CassandraMapper::Base)
85
+ @target_object = @indexed_class.new
86
+ @indexed_identifier = :index_identifier
87
+ @instance = @class.new(:name => @name,
88
+ :indexed_class => @indexed_class,
89
+ :column_family => @column_family,
90
+ :source => @source,
91
+ :indexed_identifier => @indexed_identifier)
92
+ @client = stub('cassandra_client')
93
+ @target_object.stubs(:connection).returns(@client)
94
+ @target_object.stubs(:key).with.returns(@object_key = 'some_object_identifier')
95
+ @target_object.stubs(@indexed_identifier).with.returns(@object_identifier = 'some_object_sortable_identifier')
96
+ @target_object_state = stub('target_object_index_state')
97
+ @target_object.stubs(@name).returns(@target_object_state)
98
+ end
99
+
100
+ context ':create' do
101
+ should 'insert the target source as key to the column family, with key as value' do
102
+ @instance.expects(:source_for).with(@target_object).returns(source_value = 'some_source_value')
103
+ @client.expects(:insert).with(@column_family, source_value, {@object_identifier => @object_key})
104
+ @target_object_state.expects(:source_value=).with(source_value)
105
+ @target_object_state.expects(:identifier_value=).with(@object_identifier)
106
+ @instance.create(@target_object)
107
+ end
108
+
109
+ should 'do nothing if the current index source is nil' do
110
+ @instance.expects(:source_for).with(@target_object).returns(nil)
111
+ @client.expects(:insert).never
112
+ @instance.create(@target_object)
113
+ end
114
+ end
115
+
116
+ context ':remove' do
117
+ should "remove the target object's old identifier value from the row at the old source" do
118
+ # use sequences to work around naming conflicts
119
+ @target_object_state.stubs(:source_value).with.returns(source_value = 'some_old_source_value')
120
+ @target_object_state.stubs(:identifier_value).with.returns(identifier_value = 'some_old_identifier_value')
121
+ @target_object_state.expects(:source_value=).with(nil)
122
+ @target_object_state.expects(:identifier_value=).with(nil)
123
+ @client.expects(:remove).with(@column_family, source_value, identifier_value)
124
+ @instance.remove(@target_object)
125
+ end
126
+
127
+ should 'do nothing if the old identifier value is nil' do
128
+ @target_object_state.stubs(:source_value).with.returns('some_old_source_value')
129
+ @target_object_state.expects(:identifier_value).with.returns(nil)
130
+ @target_object_state.expects(:source_value=).never
131
+ @target_object_state.expects(:identifier_value=).never
132
+ @client.expects(:remove).never
133
+ @instance.remove(@target_object)
134
+ end
135
+
136
+ should 'do nothing if the old source value is nil' do
137
+ @target_object_state.expects(:source_value).with.returns(nil)
138
+ @target_object_state.stubs(:identifier_value).with.returns('some_old_identifier_value')
139
+ @target_object_state.expects(:source_value=).never
140
+ @target_object_state.expects(:identifier_value=).never
141
+ @client.expects(:remove).never
142
+ @instance.remove(@target_object)
143
+ end
144
+ end
145
+
146
+ context ':update' do
147
+ should 'perform a :create and :remove if the source values differ' do
148
+ @target_object_state.stubs(:source_value).with.returns('some_old_source_value')
149
+ @instance.expects(:source_for).with(@target_object).returns('some_source_value').at_least_once
150
+ @target_object_state.stubs(:identifier_value).with.returns(@object_identifier)
151
+ seq = sequence('operations')
152
+ @instance.expects(:remove).with(@target_object).in_sequence(seq)
153
+ @instance.expects(:create).with(@target_object).in_sequence(seq)
154
+ @instance.update(@target_object)
155
+ end
156
+
157
+ should 'perform a :create and :remove if identifier values differ' do
158
+ @target_object_state.stubs(:source_value).with.returns('some_source_value')
159
+ @instance.stubs(:source_for).with(@target_object).returns('some_source_value')
160
+ @target_object_state.stubs(:identifier_value).with.returns('some_old_identifier_value')
161
+ @instance.expects(:indexed_identifier_for).with(@target_object).returns('some new identifier value').at_least_once
162
+ seq = sequence('operations')
163
+ @instance.expects(:remove).with(@target_object).in_sequence(seq)
164
+ @instance.expects(:create).with(@target_object).in_sequence(seq)
165
+ @instance.update(@target_object)
166
+ end
167
+
168
+ should 'do nothing if the values are unchanged' do
169
+ @target_object_state.expects(:source_value).with.returns('some_source_value')
170
+ @instance.expects(:source_for).with(@target_object).returns('some_source_value').at_least_once
171
+ @target_object_state.expects(:identifier_value).with.returns(@object_identifier)
172
+ @instance.expects(:indexed_identifier_for).with(@target_object).returns(@object_identifier).at_least_once
173
+ @instance.expects(:remove).never
174
+ @instance.expects(:create).never
175
+ @instance.update(@target_object)
176
+ end
177
+ end
178
+ end
179
+
180
+ context 'read method' do
181
+ setup do
182
+ @indexed_class = Class.new(CassandraMapper::Base) do
183
+ maps :key
184
+ maps :some_source_field
185
+ end
186
+ @column_family = :MyIndexes
187
+ @source_attrib = :some_source_field
188
+ @instance = @class.new(:indexed_class => @indexed_class,
189
+ :column_family => @column_family,
190
+ :source => @source_attrib)
191
+ @connection = stub(:connection)
192
+ @indexed_class.stubs(:connection).returns(@connection)
193
+ # the order reflects insert order, so we need to be very explicit here
194
+ @expected = Cassandra::OrderedHash.new()
195
+ @expected['0000-id1'] = 'id1'
196
+ @expected['0001-id2'] = 'id2'
197
+ @sorted_ids = ['id1', 'id2']
198
+ end
199
+
200
+ context 'get' do
201
+ context 'for single indexed value' do
202
+ setup do
203
+ @key = 'this indexed value'
204
+ end
205
+
206
+ should 'perform Cassandra get for the given index value and return raw result' do
207
+ @connection.expects(:get).with(@column_family, @key, {}).returns(@expected)
208
+ result = @instance.get(@key)
209
+ assert_equal @expected, result
210
+ end
211
+
212
+ should 'pass any options along to the Cassandra.get invocation' do
213
+ options = {:count => 2, :start => 'foo'}
214
+ @connection.expects(:get).with(@column_family, @key, options).returns(@expected)
215
+ @instance.get(@key, options)
216
+ end
217
+ end
218
+
219
+ context 'for multiple indexed values' do
220
+ setup do
221
+ @keys = ['this indexed value', 'that indexed value', 'mine', 'yours']
222
+ @client_result = Cassandra::OrderedHash.new()
223
+ # the order reflects insert order, so we need to be explicit
224
+ @client_result['this indexed value'] = {'0000-id1' => 'id1'}
225
+ @client_result['that indexed value'] = {'0001-id2' => 'id2'}
226
+ @client_result['mine'] = {'aaaa-id1' => 'id1'}
227
+ @client_result['yours'] = {'bbbb-id2' => 'id2'}
228
+ @expected.merge!(@client_result['mine'])
229
+ @expected.merge!(@client_result['yours'])
230
+ end
231
+
232
+ should 'perform Cassandra.multi_get for multiple index values and return merged result' do
233
+ @connection.expects(:multi_get).with(@column_family, @keys, {}).returns(@client_result)
234
+ result = @instance.get(@keys)
235
+ assert_equal @expected, result
236
+ end
237
+
238
+ should 'pass any options along to underlying Cassandra.multi_get' do
239
+ options = {:count => 4, :finish => 'zzzzzyyyyzzzz'}
240
+ @connection.expects(:multi_get).with(@column_family, @keys, options).returns(@client_result)
241
+ @instance.get(@keys, options)
242
+ end
243
+ end
244
+ end
245
+
246
+ context 'keys' do
247
+ should 'retrieve data from :get and return identifiers based on result indexed id order' do
248
+ value = 'some value'
249
+ @instance.expects(:get).with(value, {}).returns(@expected)
250
+ result = @instance.keys(value)
251
+ assert_equal @sorted_ids, result
252
+ end
253
+
254
+ should 'collapse redundant identifiers down and preserve earliest order' do
255
+ value = 'some value'
256
+ @expected["aaaa-#{@sorted_ids[0]}"] = @sorted_ids[0]
257
+ @expected["bbbb-#{@sorted_ids[1]}"] = @sorted_ids[1]
258
+ @instance.expects(:get).with(value, {}).returns(@expected)
259
+ result = @instance.keys(value)
260
+ assert_equal @sorted_ids, result
261
+ end
262
+
263
+ should 'pass options through to :get' do
264
+ options = {:this => :that, :mine => :not_yours}
265
+ value = 'foo'
266
+ @instance.expects(:get).with(value, options).returns(@expected)
267
+ @instance.keys(value, options)
268
+ end
269
+ end
270
+
271
+ context 'objects' do
272
+ setup do
273
+ @expected_objects = @sorted_ids.collect {|id| @indexed_class.new(:key => id)}
274
+ end
275
+
276
+ should 'return results of a model class :find call with :keys as its arguments' do
277
+ @indexed_class.expects(:find).with(@sorted_ids, {:allow_missing => true}).returns(@expected_objects)
278
+ @instance.expects(:keys).with(value = 'foo', {}).returns(@sorted_ids)
279
+ result = @instance.objects(value)
280
+ assert_equal @expected_objects, result
281
+ end
282
+
283
+ should 'not call :find if :keys is empty, and return an empty array' do
284
+ @indexed_class.expects(:find).never
285
+ @instance.expects(:keys).with(value = 'foo', {}).returns([])
286
+ result = @instance.objects(value)
287
+ assert_equal [], result
288
+ end
289
+
290
+ should 'pass options through to :keys' do
291
+ @instance.expects(:keys).with(value = 'foo', options = {:this => :and, :that => :dude}).returns(@sorted_ids)
292
+ @indexed_class.expects(:find).with(@sorted_ids, {:allow_missing => true}).returns(@expected_objects)
293
+ @instance.objects(value, options)
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ context 'A Cassandra::Base-derived class' do
300
+ setup do
301
+ @class = Class.new(CassandraMapper::Base)
302
+ end
303
+
304
+ context 'has_index method' do
305
+ should 'create a class accessor for the named index' do
306
+ assert_equal true, ! @class.respond_to?(:this_index)
307
+ @class.has_index :this_index
308
+ assert_equal true, @class.respond_to?(:this_index)
309
+ end
310
+
311
+ should 'set the :name attribute on the resulting index object' do
312
+ @class.has_index :this_index
313
+ assert_equal :this_index, @class.this_index.name
314
+ end
315
+
316
+ should 'determine class of index from the :class option' do
317
+ subclass = Class.new(CassandraMapper::Index)
318
+ @class.has_index :this_index, :class => subclass
319
+ assert_equal true, subclass === @class.this_index
320
+ end
321
+
322
+ should 'default class of index to CassandraMapper::Index' do
323
+ @class.has_index :this_index
324
+ assert_equal true, CassandraMapper::Index === @class.this_index
325
+ end
326
+
327
+ should 'pass all options, with :indexed_class mapped to indexed class, to the constructor' do
328
+ options = {:a => 'a', :b => 'b', :c => 'c'}
329
+ subclass = Class.new(CassandraMapper::Index)
330
+ subclass.expects(:new).with(options.merge(:indexed_class => @class,
331
+ :name => :this_index)).returns(CassandraMapper::Index.new(options.merge(:indexed_class => @class)))
332
+ @class.has_index :this_index, options.merge(:class => subclass)
333
+ end
334
+
335
+ context 'given a block' do
336
+ should 'evaluate the block in the context of the new index object' do
337
+ received = []
338
+ @class.has_index :this_index do
339
+ received << self
340
+ end
341
+ assert_equal [@class.this_index], received
342
+ end
343
+ end
344
+ end
345
+
346
+ context 'instance' do
347
+ setup do
348
+ @instance = @class.new
349
+ end
350
+
351
+ context 'with class that has_index :foo' do
352
+ setup do
353
+ @class.has_index :foo
354
+ @class.maps :some_attr
355
+ @class.maps :id
356
+ @class.key :id
357
+ end
358
+
359
+ should 'have an attribute :foo of type CassandraMapper::Index::State' do
360
+ assert_equal CassandraMapper::Index::State, @instance.foo.class
361
+ end
362
+
363
+ should 'have nil index state values for :foo state object' do
364
+ assert_equal nil, @instance.foo.source_value
365
+ assert_equal nil, @instance.foo.identifier_value
366
+ end
367
+
368
+ should 'receive proper state_value and identifier_value values after :loaded' do
369
+ @class.foo.expects(:source_for).with(@instance).returns(source = 'some_source_value')
370
+ @class.foo.expects(:indexed_identifier_for).with(@instance).returns(key = 'some_key')
371
+ @instance.loaded!
372
+ assert_equal source, @instance.foo.source_value
373
+ assert_equal key, @instance.foo.identifier_value
374
+ end
375
+
376
+ should 'create the index upon saving a new instance' do
377
+ @instance.stubs(:new_record?).returns(true)
378
+ @instance.some_attr = 'blah'
379
+ @instance.id = 'foo'
380
+ @class.foo.expects(:create).with(@instance)
381
+ @instance.connection.expects(:insert)
382
+ @instance.save
383
+ end
384
+
385
+ should 'update the index upon saving an existing instance' do
386
+ @instance.stubs(:new_record?).returns(false)
387
+ @instance.some_attr = 'blah'
388
+ @instance.id = 'foo'
389
+ @class.foo.expects(:update).with(@instance)
390
+ @instance.connection.expects(:insert)
391
+ @instance.save
392
+ end
393
+
394
+ should 'remove from the index upon destroying an existing instance' do
395
+ @instance.stubs(:new_record?).returns(false)
396
+ @instance.stubs(:freeze)
397
+ @instance.some_attr = 'blah'
398
+ @instance.id = 'foo'
399
+ @class.foo.expects(:remove).with(@instance)
400
+ @instance.connection.expects(:remove)
401
+ @instance.destroy
402
+ end
403
+ end
404
+ end
405
+ end
406
+ end