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