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,36 @@
1
+ require 'cassandra_mapper/support/observing'
2
+ module CassandraMapper
3
+ module Observable
4
+ CALLBACKS = [
5
+ :after_load,
6
+ :before_create,
7
+ :after_create,
8
+ :before_update,
9
+ :after_update,
10
+ :before_save,
11
+ :after_save,
12
+ :before_destroy,
13
+ :after_destroy,
14
+ ]
15
+
16
+ CALLBACKS.each do |cb|
17
+ name = cb.to_s
18
+ module_eval <<-cbnotify
19
+ def _notify_observer_#{name}; notify_observers(:#{name}); true; end
20
+ cbnotify
21
+ end
22
+
23
+ def self.included(klass)
24
+ klass.module_eval do
25
+ include CassandraMapper::Support::Observing
26
+ CALLBACKS.each do |callback|
27
+ name = callback.to_s
28
+ send(callback, :"_notify_observer_#{name}")
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ class Observer < CassandraMapper::Support::Observer
35
+ end
36
+ end
@@ -0,0 +1,309 @@
1
+ require 'cassandra_mapper/support/callbacks'
2
+ module CassandraMapper::Persistence
3
+ def _determine_transform_options
4
+ options = {:string_keys => true}
5
+ is_update = false
6
+ if new_record?
7
+ options[:defined] = true
8
+ else
9
+ return false unless changed_attributes.length > 0
10
+ options[:changed] = true
11
+ is_update = true
12
+ end
13
+ [options, is_update]
14
+ end
15
+
16
+ def save(with_validation = true)
17
+ _run_save_callbacks do
18
+ uniq_key = _check_key
19
+ options, is_update = _determine_transform_options
20
+ return false unless options
21
+ if is_update
22
+ update(uniq_key, options)
23
+ else
24
+ create(uniq_key, options)
25
+ end
26
+ end
27
+ end
28
+
29
+ def loaded!
30
+ self.new_record = false
31
+ _run_load_callbacks
32
+ self
33
+ end
34
+
35
+ def create(uniq_key, options)
36
+ _run_create_callbacks do
37
+ write!(uniq_key, options)
38
+ self.new_record = false
39
+ self
40
+ end
41
+ end
42
+
43
+ def update(uniq_key, options)
44
+ _run_update_callbacks do
45
+ write!(uniq_key, options.merge({ :with_delete => true }))
46
+ self
47
+ end
48
+ end
49
+
50
+ def destroy
51
+ unless new_record?
52
+ _run_destroy_callbacks do
53
+ self.class.delete(_check_key)
54
+ end
55
+ end
56
+ @destroyed = true
57
+ freeze
58
+ self
59
+ end
60
+
61
+ def write!(uniq_key, options)
62
+ with_delete = options.delete(:with_delete)
63
+ structure = to_simple(options)
64
+ if with_delete
65
+ deletes = self.class.prune_deletes_from_simple_structure(structure)
66
+ cassandra_args = []
67
+ if ! (structure.empty? or deletes.empty?) or deletes.size >= 2
68
+ cassandra_args << {:timestamp => Time.stamp}
69
+ end
70
+ base_args = [self.class.column_family, uniq_key]
71
+ connection.insert(*(base_args + [structure] + cassandra_args)) unless structure.empty?
72
+ deletes.each {|del_args| connection.remove(*(base_args + del_args + cassandra_args))}
73
+ else
74
+ connection.insert(self.class.column_family, uniq_key, to_simple(options))
75
+ end
76
+ end
77
+
78
+ def to_mutation(with_validation = true, options = {})
79
+ uniq_key = _check_key.to_s
80
+ timestamp = options.delete(:timestamp) || Time.stamp
81
+ general_opts, is_update = _determine_transform_options
82
+ return false unless general_opts
83
+ options.merge!(general_opts)
84
+ {
85
+ uniq_key => {
86
+ self.class.column_family.to_s => self.class.to_mutation(to_simple(options), timestamp)
87
+ }
88
+ }
89
+ end
90
+
91
+ def _check_key
92
+ uniq_key = self.key
93
+ raise CassandraMapper::UndefinedKeyException if uniq_key.nil?
94
+ uniq_key
95
+ end
96
+
97
+ def delete
98
+ self.class.delete(_check_key) unless new_record?
99
+ @destroyed = true
100
+ freeze
101
+ self
102
+ end
103
+
104
+ def destroyed?
105
+ (@destroyed && true) || false
106
+ end
107
+
108
+ module ClassMethods
109
+ # Given a single key or list of keys, returns all mapped objects found
110
+ # for thoese keys.
111
+ #
112
+ # If the row for a specified key is missing, a CassndraMapper::RecordNotFoundException
113
+ # exception is raised. This can be overridden by specifying the +:allow_missing+
114
+ # option (:allow_missing => true)
115
+ #
116
+ # Keys and options may be specified in a variety of ways:
117
+ # * Flat list
118
+ # SomeClass.find(key1, key2, key3, options)
119
+ # * Separate lists
120
+ # SomeClass.find([key1, key2, key3], options)
121
+ #
122
+ # And of course, _options_ can always be left out.
123
+ def find(*args)
124
+ single = false
125
+ case args.first
126
+ when Array
127
+ keys = args.first
128
+ when nil
129
+ raise CassandraMapper::InvalidArgumentException
130
+ else
131
+ keys = args
132
+ single = true if keys.length == 1
133
+ end
134
+ case args.last
135
+ when Hash
136
+ options = args.pop
137
+ else
138
+ options = {}
139
+ end
140
+
141
+ result = connection.multi_get(column_family, keys).values.inject([]) do |arr, hash|
142
+ if not hash.empty?
143
+ obj = new(hash)
144
+ obj.new_record = false
145
+ arr << obj
146
+ obj.loaded!
147
+ end
148
+ arr
149
+ end
150
+ raise CassandraMapper::RecordNotFoundException unless result.size == keys.size or options[:allow_missing]
151
+ single ? result.first : result
152
+ end
153
+
154
+ # Given a _key_ of a single row key or array of row keys,
155
+ # removes the rows from the Cassandra column family associated with
156
+ # the receiving class.
157
+ #
158
+ # As the underlying Cassandra client returns no information about the
159
+ # result of the operation, the result is always +nil+.
160
+ #
161
+ # The delete operation is purely database-side; no objects are instantiated
162
+ # and therefore no object callbacks take place for the deleted rows.
163
+ # This makes delete risky for classes that manage associations, indexes, etc.
164
+ def delete(key)
165
+ keys = Array === key ? key : [key]
166
+ keys.each do |key|
167
+ connection.remove(column_family, key)
168
+ end
169
+ nil
170
+ end
171
+
172
+ # Similar to +delete+; given a _key_ of a single row key or array of row keys,
173
+ # removes the rows from the Cassandra column family associated with the receiving
174
+ # class.
175
+ #
176
+ # However, unlike +delete+, +destroy+ loads up the object per row key in turn
177
+ # and invokes +destroy+ on each object. This allows for callbacks and observers
178
+ # to be executed, which is essential for index maintenance, association maintenance,
179
+ # etc.
180
+ def destroy(key)
181
+ objs = find(key)
182
+ case objs
183
+ when Array
184
+ objs.each {|obj| obj.destroy}
185
+ else
186
+ objs.destroy
187
+ end
188
+ nil
189
+ end
190
+
191
+ def connection
192
+ @cassandra_mapper_connection
193
+ end
194
+
195
+ def connection=(connection)
196
+ @cassandra_mapper_connection = connection
197
+ end
198
+
199
+ def column_family(family = nil)
200
+ @cassandra_mapper_column_family = family if ! family.nil?
201
+ @cassandra_mapper_column_family
202
+ end
203
+
204
+ def self.extended(klass)
205
+ klass.module_eval do
206
+ extend CassandraMapper::Support::Callbacks
207
+ define_model_callbacks :save, :create, :update, :destroy
208
+ define_model_callbacks :load, :only => :after
209
+ end
210
+ end
211
+
212
+ def to_mutation(simple_structure, timestamp)
213
+ mutator.from_simple(simple_structure, timestamp)
214
+ end
215
+
216
+ def mutator
217
+ unless @mutator
218
+ mutator_class = simple_mapper.attributes.first[1].mapper ? SuperMutator : SimpleMutator
219
+ @mutator = mutator_class.new
220
+ end
221
+ @mutator
222
+ end
223
+
224
+ class SimpleMutator
225
+ def from_simple(structure, timestamp)
226
+ deletion = nil
227
+ deletion_columns = nil
228
+ structure.inject([]) do |list, pair|
229
+ key,val = pair
230
+ if val.nil?
231
+ unless deletion
232
+ deletion = CassandraThrift::Mutation.new(
233
+ :deletion => CassandraThrift::Deletion.new(
234
+ :super_column => nil,
235
+ :timestamp => timestamp,
236
+ :predicate => CassandraThrift::SlicePredicate.new(
237
+ :column_names => (deletion_columns = [])
238
+ )
239
+ )
240
+ )
241
+ list << deletion
242
+ end
243
+ deletion_columns << key
244
+ else
245
+ list << CassandraThrift::Mutation.new(
246
+ :column_or_supercolumn => CassandraThrift::ColumnOrSuperColumn.new(
247
+ :column => CassandraThrift::Column.new(
248
+ :name => key,
249
+ :value => val,
250
+ :timestamp => timestamp
251
+ )
252
+ )
253
+ )
254
+ end
255
+ list
256
+ end
257
+ end
258
+ end
259
+
260
+ class SuperMutator
261
+ def from_simple(structure, timestamp)
262
+ structure.inject([]) do |list, pair|
263
+ supercol_key, val = pair
264
+ if val and ! val.empty?
265
+ list << CassandraThrift::Mutation.new(
266
+ :column_or_supercolumn => CassandraThrift::ColumnOrSuperColumn.new(
267
+ :super_column => CassandraThrift::SuperColumn.new(
268
+ :name => supercol_key,
269
+ :columns => val.collect {|column, value|
270
+ CassandraThrift::Column.new(
271
+ :name => column,
272
+ :value => value,
273
+ :timestamp => timestamp
274
+ )
275
+ }
276
+ )
277
+ )
278
+ )
279
+ end
280
+ list
281
+ end
282
+ end
283
+ end
284
+
285
+ def prune_deletes(source, deletes, context)
286
+ source.each do |k, v|
287
+ if v.nil?
288
+ deletes << context + [k]
289
+ source.delete(k)
290
+ # pre 1.9 String responds to :each; but :values_at is a
291
+ # safe bet for 1.8 and 1.9 for array, hash, but not string.
292
+ elsif v.respond_to?(:values_at)
293
+ prune_deletes(v, deletes, context + [k])
294
+ source.delete(k) if v.empty?
295
+ end
296
+ end
297
+ end
298
+
299
+ def prune_deletes_from_simple_structure(structure)
300
+ deletes = []
301
+ prune_deletes(structure, deletes, [])
302
+ deletes
303
+ end
304
+ end
305
+
306
+ def self.included(klass)
307
+ klass.extend ClassMethods
308
+ end
309
+ end
@@ -0,0 +1,136 @@
1
+ require 'cassandra_mapper/core_ext/array/wrap'
2
+ require 'cassandra_mapper/support/support_callbacks'
3
+
4
+ module CassandraMapper
5
+ module Support
6
+ # == Active Model Callbacks
7
+ #
8
+ # Provides an interface for any class to have Active Record like callbacks.
9
+ #
10
+ # Like the Active Record methods, the callback chain is aborted as soon as
11
+ # one of the methods in the chain returns false.
12
+ #
13
+ # First, extend ActiveModel::Callbacks from the class you are creating:
14
+ #
15
+ # class MyModel
16
+ # extend ActiveModel::Callbacks
17
+ # end
18
+ #
19
+ # Then define a list of methods that you want callbacks attached to:
20
+ #
21
+ # define_model_callbacks :create, :update
22
+ #
23
+ # This will provide all three standard callbacks (before, around and after) around
24
+ # both the :create and :update methods. To implement, you need to wrap the methods
25
+ # you want callbacks on in a block so that the callbacks get a chance to fire:
26
+ #
27
+ # def create
28
+ # _run_create_callbacks do
29
+ # # Your create action methods here
30
+ # end
31
+ # end
32
+ #
33
+ # The _run_<method_name>_callbacks methods are dynamically created when you extend
34
+ # the <tt>ActiveModel::Callbacks</tt> module.
35
+ #
36
+ # Then in your class, you can use the +before_create+, +after_create+ and +around_create+
37
+ # methods, just as you would in an Active Record module.
38
+ #
39
+ # before_create :action_before_create
40
+ #
41
+ # def action_before_create
42
+ # # Your code here
43
+ # end
44
+ #
45
+ # You can choose not to have all three callbacks by passing a hash to the
46
+ # define_model_callbacks method.
47
+ #
48
+ # define_model_callbacks :create, :only => :after, :before
49
+ #
50
+ # Would only create the after_create and before_create callback methods in your
51
+ # class.
52
+ module Callbacks
53
+ def self.extended(base)
54
+ base.class_eval do
55
+ include CassandraMapper::Support::SupportCallbacks
56
+ end
57
+ end
58
+
59
+ # define_model_callbacks accepts the same options define_callbacks does, in case
60
+ # you want to overwrite a default. Besides that, it also accepts an :only option,
61
+ # where you can choose if you want all types (before, around or after) or just some.
62
+ #
63
+ # define_model_callbacks :initializer, :only => :after
64
+ #
65
+ # Note, the <tt>:only => <type></tt> hash will apply to all callbacks defined on
66
+ # that method call. To get around this you can call the define_model_callbacks
67
+ # method as many times as you need.
68
+ #
69
+ # define_model_callbacks :create, :only => :after
70
+ # define_model_callbacks :update, :only => :before
71
+ # define_model_callbacks :destroy, :only => :around
72
+ #
73
+ # Would create +after_create+, +before_update+ and +around_destroy+ methods only.
74
+ #
75
+ # You can pass in a class to before_<type>, after_<type> and around_<type>, in which
76
+ # case the callback will call that class's <action>_<type> method passing the object
77
+ # that the callback is being called on.
78
+ #
79
+ # class MyModel
80
+ # extend ActiveModel::Callbacks
81
+ # define_model_callbacks :create
82
+ #
83
+ # before_create AnotherClass
84
+ # end
85
+ #
86
+ # class AnotherClass
87
+ # def self.before_create( obj )
88
+ # # obj is the MyModel instance that the callback is being called on
89
+ # end
90
+ # end
91
+ #
92
+ def define_model_callbacks(*callbacks)
93
+ options = callbacks.extract_options!
94
+ options = { :terminator => "result == false", :scope => [:kind, :name] }.merge(options)
95
+
96
+ types = Array.wrap(options.delete(:only))
97
+ types = [:before, :around, :after] if types.empty?
98
+
99
+ callbacks.each do |callback|
100
+ define_callbacks(callback, options)
101
+
102
+ types.each do |type|
103
+ send(:"_define_#{type}_model_callback", self, callback)
104
+ end
105
+ end
106
+ end
107
+
108
+ def _define_before_model_callback(klass, callback) #:nodoc:
109
+ klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
110
+ def self.before_#{callback}(*args, &block)
111
+ set_callback(:#{callback}, :before, *args, &block)
112
+ end
113
+ CALLBACK
114
+ end
115
+
116
+ def _define_around_model_callback(klass, callback) #:nodoc:
117
+ klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
118
+ def self.around_#{callback}(*args, &block)
119
+ set_callback(:#{callback}, :around, *args, &block)
120
+ end
121
+ CALLBACK
122
+ end
123
+
124
+ def _define_after_model_callback(klass, callback) #:nodoc:
125
+ klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
126
+ def self.after_#{callback}(*args, &block)
127
+ options = args.extract_options!
128
+ options[:prepend] = true
129
+ options[:if] = Array.wrap(options[:if]) << "!halted && value != false"
130
+ set_callback(:#{callback}, :after, *(args << options), &block)
131
+ end
132
+ CALLBACK
133
+ end
134
+ end
135
+ end
136
+ end