cassandra_mapper 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +98 -0
- data/Rakefile.rb +11 -0
- data/lib/cassandra_mapper.rb +5 -0
- data/lib/cassandra_mapper/base.rb +19 -0
- data/lib/cassandra_mapper/connection.rb +9 -0
- data/lib/cassandra_mapper/core_ext/array/extract_options.rb +29 -0
- data/lib/cassandra_mapper/core_ext/array/wrap.rb +22 -0
- data/lib/cassandra_mapper/core_ext/class/inheritable_attributes.rb +232 -0
- data/lib/cassandra_mapper/core_ext/kernel/reporting.rb +62 -0
- data/lib/cassandra_mapper/core_ext/kernel/singleton_class.rb +13 -0
- data/lib/cassandra_mapper/core_ext/module/aliasing.rb +70 -0
- data/lib/cassandra_mapper/core_ext/module/attribute_accessors.rb +66 -0
- data/lib/cassandra_mapper/core_ext/object/duplicable.rb +65 -0
- data/lib/cassandra_mapper/core_ext/string/inflections.rb +160 -0
- data/lib/cassandra_mapper/core_ext/string/multibyte.rb +72 -0
- data/lib/cassandra_mapper/exceptions.rb +10 -0
- data/lib/cassandra_mapper/identity.rb +29 -0
- data/lib/cassandra_mapper/indexing.rb +465 -0
- data/lib/cassandra_mapper/observable.rb +36 -0
- data/lib/cassandra_mapper/persistence.rb +309 -0
- data/lib/cassandra_mapper/support/callbacks.rb +136 -0
- data/lib/cassandra_mapper/support/concern.rb +31 -0
- data/lib/cassandra_mapper/support/dependencies.rb +60 -0
- data/lib/cassandra_mapper/support/descendants_tracker.rb +41 -0
- data/lib/cassandra_mapper/support/inflections.rb +58 -0
- data/lib/cassandra_mapper/support/inflector.rb +7 -0
- data/lib/cassandra_mapper/support/inflector/inflections.rb +213 -0
- data/lib/cassandra_mapper/support/inflector/methods.rb +143 -0
- data/lib/cassandra_mapper/support/inflector/transliterate.rb +99 -0
- data/lib/cassandra_mapper/support/multibyte.rb +46 -0
- data/lib/cassandra_mapper/support/multibyte/utils.rb +62 -0
- data/lib/cassandra_mapper/support/observing.rb +218 -0
- data/lib/cassandra_mapper/support/support_callbacks.rb +593 -0
- data/test/test_helper.rb +11 -0
- data/test/unit/callbacks_test.rb +100 -0
- data/test/unit/identity_test.rb +51 -0
- data/test/unit/indexing_test.rb +406 -0
- data/test/unit/observer_test.rb +56 -0
- data/test/unit/persistence_test.rb +561 -0
- metadata +192 -0
data/test/test_helper.rb
ADDED
@@ -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
|