cassandra_mapper 0.0.1

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