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
data/README.rdoc ADDED
@@ -0,0 +1,98 @@
1
+ = CassandraMapper: Easily build classes for working with Cassandra
2
+
3
+ +CassandraMapper+ uses the features and semantics of +SimpleMapper+ to make
4
+ working with your Cassandra schema productive and expressive.
5
+
6
+ Build your Cassandra-fronting model classes with +CassandraMapper+, using the
7
+ same straightforward semantics of +SimpleMapper+. +CassandraMapper+ adds in
8
+ basic connection management (very basic, and easily customized) and persistence
9
+ logic for inserting/updating via the Thrift client.
10
+
11
+ class Animal < CassandraMapper::Base
12
+ # specify the name of the column family fronted by this class
13
+ column_family 'Animals'
14
+
15
+ # indicate which attribute should serve as the key for identification
16
+ key :species
17
+
18
+ # specify the attributes you expect ala SimpleMapper
19
+ maps :species, :type => :string
20
+ maps :name, :type => :string
21
+
22
+ # define a custom type to define the domain of a given attribute
23
+ module DietaryPreference
24
+ DIETS = [:herbivore, :carnivore, :omnivore].inject({}) {|h, v| h[v] = v.to_s; h}
25
+ # encode should convert a "native" value (as you would work with it in Ruby)
26
+ # to the Cassandra storage format at it should be passed to Thrift.
27
+ def self.encode(value)
28
+ raise Exception unless value = DIETS[value]
29
+ value
30
+ end
31
+ # decode should convert a Cassandra/Thrift value to a "native" value (the inverse
32
+ # of encode)
33
+ def self.decode(value)
34
+ raise Exception unless DIETS.has_key?( key = value.to_sym )
35
+ key
36
+ end
37
+ # let's default to herbivore, for a kinder, gentler world
38
+ def self.default
39
+ :herbivore
40
+ end
41
+ end
42
+
43
+ # Anything that has decode/encode/default can serve as a type,
44
+ # without being registered with the general symbol-lookup
45
+ # type registry.
46
+ # The :from_type default will mean the default value for this
47
+ # attribute (when it is left undefined) will come from the type.
48
+ maps :diet, :type => DietaryPreference, :default => :from_type
49
+ end
50
+
51
+ With a class defined, you can work with these objects as you would intuitively
52
+ expect (though you'll need to see connection management topics to get this to work).
53
+
54
+ # now let's create some animals, in order of ascending stupidity
55
+ deer = Animal.new(:species => 'odocoileus virginianus',
56
+ :name => 'White-tailed Deer)
57
+ deer.save
58
+ gull = Animal.new(:species => 'larus occidentalis',
59
+ :name => 'Seagull',
60
+ :diet => :carnivore)
61
+ gull.save
62
+ human = Animal.new(:species => 'homo sapiens',
63
+ :name => 'Human',
64
+ :diet => :omnivore)
65
+ human.save
66
+ # and let's fetch 'em back. Note that we didn't need to specify the :diet for the deer.
67
+ # This should return [:herbivore, :omnivore, :carnivore], though not necessarily in that order
68
+ Animal.find([human, deer, gull].collect {|animal| animal.species}).collect {|a| a.diet}
69
+
70
+ = Connection Management
71
+
72
+ As stated above, connection management is quite simple. Bare bones, in fact.
73
+
74
+ At present, +CassandraMapper+ expects you to manage your connections to +Cassandra+
75
+ yourself. There are a variety of reasons for this. The most important one is
76
+ that the lack of transactional isolation, or even "session" isolation, and the
77
+ whole eventually-consistent paradigm, effectively deprecate the idea of having all
78
+ steps in a business transaction take place on the same connection. If you're using
79
+ Cassandra, chances are you're interested in serious scaling of writes or reads or
80
+ both. This is best achieved by managing connections yourself in a way that maximizes
81
+ scalability/throughput for your use case.
82
+
83
+ It is likely that connection management will be introduced in more sophisticated form
84
+ in the reasonably near future, but that's not the most pressing priority. So, for
85
+ the time being, connections are at the class level (like in +ActiveRecord+) and need
86
+ to be explicitly assigned.
87
+
88
+ Thus, for the +Animal+ set/get examples above to truly work, you would need to
89
+ get an instance of the +Cassandra+ thrift client (+Cassandra.new(...)+), and
90
+ you would need to assign it to the +Animal+ class.
91
+
92
+ # load up the Cassandra client
93
+ require 'cassandra'
94
+ # get a connection and assign it to the class.
95
+ Animal.connection = Cassandra.new('YourKeyspace', '127.0.0.1:9160')
96
+
97
+ A near-term improvement will allow per-object specification of the connection,
98
+ for more flexible management.
data/Rakefile.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/clean'
4
+
5
+ CLOBBER.include('cassandra_mapper-*.gem')
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.pattern = 'test/**/*_test.rb'
9
+ t.libs = ['test', 'lib']
10
+ end
11
+
@@ -0,0 +1,5 @@
1
+ require 'cassandra'
2
+ module CassandraMapper
3
+ require 'cassandra_mapper/exceptions'
4
+ autoload :Base, 'cassandra_mapper/base'
5
+ end
@@ -0,0 +1,19 @@
1
+ require 'simple_mapper'
2
+ class CassandraMapper::Base
3
+ include SimpleMapper::Attributes
4
+
5
+ require 'cassandra_mapper/identity'
6
+ include CassandraMapper::Identity
7
+
8
+ require 'cassandra_mapper/persistence'
9
+ include CassandraMapper::Persistence
10
+
11
+ require 'cassandra_mapper/connection'
12
+ include CassandraMapper::Connection
13
+
14
+ require 'cassandra_mapper/observable'
15
+ include CassandraMapper::Observable
16
+
17
+ require 'cassandra_mapper/indexing'
18
+ include CassandraMapper::Indexing
19
+ end
@@ -0,0 +1,9 @@
1
+ module CassandraMapper::Connection
2
+ def connection=(conn)
3
+ @connection = conn
4
+ end
5
+
6
+ def connection
7
+ @connection || self.class.connection
8
+ end
9
+ end
@@ -0,0 +1,29 @@
1
+ class Hash
2
+ # By default, only instances of Hash itself are extractable.
3
+ # Subclasses of Hash may implement this method and return
4
+ # true to declare themselves as extractable. If a Hash
5
+ # is extractable, Array#extract_options! pops it from
6
+ # the Array when it is the last element of the Array.
7
+ def extractable_options?
8
+ instance_of?(Hash)
9
+ end
10
+ end
11
+
12
+ class Array
13
+ # Extracts options from a set of arguments. Removes and returns the last
14
+ # element in the array if it's a hash, otherwise returns a blank hash.
15
+ #
16
+ # def options(*args)
17
+ # args.extract_options!
18
+ # end
19
+ #
20
+ # options(1, 2) # => {}
21
+ # options(1, 2, :a => :b) # => {:a=>:b}
22
+ def extract_options!
23
+ if last.is_a?(Hash) && last.extractable_options?
24
+ pop
25
+ else
26
+ {}
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ class Array
2
+ # Wraps the object in an Array unless it's an Array. Converts the
3
+ # object to an Array using #to_ary if it implements that.
4
+ #
5
+ # It differs with Array() in that it does not call +to_a+ on
6
+ # the argument:
7
+ #
8
+ # Array(:foo => :bar) # => [[:foo, :bar]]
9
+ # Array.wrap(:foo => :bar) # => [{:foo => :bar}]
10
+ #
11
+ # Array("foo\nbar") # => ["foo\n", "bar"], in Ruby 1.8
12
+ # Array.wrap("foo\nbar") # => ["foo\nbar"]
13
+ def self.wrap(object)
14
+ if object.nil?
15
+ []
16
+ elsif object.respond_to?(:to_ary)
17
+ object.to_ary
18
+ else
19
+ [object]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,232 @@
1
+ require 'cassandra_mapper/core_ext/object/duplicable'
2
+ require 'cassandra_mapper/core_ext/array/extract_options'
3
+
4
+ # Retain for backward compatibility. Methods are now included in Class.
5
+ module ClassInheritableAttributes # :nodoc:
6
+ end
7
+
8
+ # Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
9
+ # their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
10
+ # to, for example, an array without those additions being shared with either their parent, siblings, or
11
+ # children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
12
+ #
13
+ # The copies of inheritable parent attributes are added to subclasses when they are created, via the
14
+ # +inherited+ hook.
15
+ class Class # :nodoc:
16
+ def class_inheritable_reader(*syms)
17
+ options = syms.extract_options!
18
+ syms.each do |sym|
19
+ next if sym.is_a?(Hash)
20
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
21
+ def self.#{sym} # def self.after_add
22
+ read_inheritable_attribute(:#{sym}) # read_inheritable_attribute(:after_add)
23
+ end # end
24
+ #
25
+ #{" #
26
+ def #{sym} # def after_add
27
+ self.class.#{sym} # self.class.after_add
28
+ end # end
29
+ " unless options[:instance_reader] == false } # # the reader above is generated unless options[:instance_reader] == false
30
+ EOS
31
+ end
32
+ end
33
+
34
+ def class_inheritable_writer(*syms)
35
+ options = syms.extract_options!
36
+ syms.each do |sym|
37
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
38
+ def self.#{sym}=(obj) # def self.color=(obj)
39
+ write_inheritable_attribute(:#{sym}, obj) # write_inheritable_attribute(:color, obj)
40
+ end # end
41
+ #
42
+ #{" #
43
+ def #{sym}=(obj) # def color=(obj)
44
+ self.class.#{sym} = obj # self.class.color = obj
45
+ end # end
46
+ " unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
47
+ EOS
48
+ end
49
+ end
50
+
51
+ def class_inheritable_array_writer(*syms)
52
+ options = syms.extract_options!
53
+ syms.each do |sym|
54
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
55
+ def self.#{sym}=(obj) # def self.levels=(obj)
56
+ write_inheritable_array(:#{sym}, obj) # write_inheritable_array(:levels, obj)
57
+ end # end
58
+ #
59
+ #{" #
60
+ def #{sym}=(obj) # def levels=(obj)
61
+ self.class.#{sym} = obj # self.class.levels = obj
62
+ end # end
63
+ " unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
64
+ EOS
65
+ end
66
+ end
67
+
68
+ def class_inheritable_hash_writer(*syms)
69
+ options = syms.extract_options!
70
+ syms.each do |sym|
71
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
72
+ def self.#{sym}=(obj) # def self.nicknames=(obj)
73
+ write_inheritable_hash(:#{sym}, obj) # write_inheritable_hash(:nicknames, obj)
74
+ end # end
75
+ #
76
+ #{" #
77
+ def #{sym}=(obj) # def nicknames=(obj)
78
+ self.class.#{sym} = obj # self.class.nicknames = obj
79
+ end # end
80
+ " unless options[:instance_writer] == false } # # the writer above is generated unless options[:instance_writer] == false
81
+ EOS
82
+ end
83
+ end
84
+
85
+ def class_inheritable_accessor(*syms)
86
+ class_inheritable_reader(*syms)
87
+ class_inheritable_writer(*syms)
88
+ end
89
+
90
+ def class_inheritable_array(*syms)
91
+ class_inheritable_reader(*syms)
92
+ class_inheritable_array_writer(*syms)
93
+ end
94
+
95
+ def class_inheritable_hash(*syms)
96
+ class_inheritable_reader(*syms)
97
+ class_inheritable_hash_writer(*syms)
98
+ end
99
+
100
+ def inheritable_attributes
101
+ @inheritable_attributes ||= EMPTY_INHERITABLE_ATTRIBUTES
102
+ end
103
+
104
+ def write_inheritable_attribute(key, value)
105
+ if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
106
+ @inheritable_attributes = {}
107
+ end
108
+ inheritable_attributes[key] = value
109
+ end
110
+
111
+ def write_inheritable_array(key, elements)
112
+ write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
113
+ write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
114
+ end
115
+
116
+ def write_inheritable_hash(key, hash)
117
+ write_inheritable_attribute(key, {}) if read_inheritable_attribute(key).nil?
118
+ write_inheritable_attribute(key, read_inheritable_attribute(key).merge(hash))
119
+ end
120
+
121
+ def read_inheritable_attribute(key)
122
+ inheritable_attributes[key]
123
+ end
124
+
125
+ def reset_inheritable_attributes
126
+ @inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
127
+ end
128
+
129
+ private
130
+ # Prevent this constant from being created multiple times
131
+ EMPTY_INHERITABLE_ATTRIBUTES = {}.freeze unless const_defined?(:EMPTY_INHERITABLE_ATTRIBUTES)
132
+
133
+ def inherited_with_inheritable_attributes(child)
134
+ inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes)
135
+
136
+ if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
137
+ new_inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
138
+ else
139
+ new_inheritable_attributes = inheritable_attributes.inject({}) do |memo, (key, value)|
140
+ memo.update(key => value.duplicable? ? value.dup : value)
141
+ end
142
+ end
143
+
144
+ child.instance_variable_set('@inheritable_attributes', new_inheritable_attributes)
145
+ end
146
+
147
+ alias inherited_without_inheritable_attributes inherited
148
+ alias inherited inherited_with_inheritable_attributes
149
+ end
150
+
151
+ class Class
152
+ # Defines class-level inheritable attribute reader. Attributes are available to subclasses,
153
+ # each subclass has a copy of parent's attribute.
154
+ #
155
+ # @param *syms<Array[#to_s]> Array of attributes to define inheritable reader for.
156
+ # @return <Array[#to_s]> Array of attributes converted into inheritable_readers.
157
+ #
158
+ # @api public
159
+ #
160
+ # @todo Do we want to block instance_reader via :instance_reader => false
161
+ # @todo It would be preferable that we do something with a Hash passed in
162
+ # (error out or do the same as other methods above) instead of silently
163
+ # moving on). In particular, this makes the return value of this function
164
+ # less useful.
165
+ def extlib_inheritable_reader(*ivars, &block)
166
+ options = ivars.extract_options!
167
+
168
+ ivars.each do |ivar|
169
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
170
+ def self.#{ivar}
171
+ return @#{ivar} if self.object_id == #{self.object_id} || defined?(@#{ivar})
172
+ ivar = superclass.#{ivar}
173
+ return nil if ivar.nil? && !#{self}.instance_variable_defined?("@#{ivar}")
174
+ @#{ivar} = ivar.duplicable? ? ivar.dup : ivar
175
+ end
176
+ RUBY
177
+ unless options[:instance_reader] == false
178
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
179
+ def #{ivar}
180
+ self.class.#{ivar}
181
+ end
182
+ RUBY
183
+ end
184
+ instance_variable_set(:"@#{ivar}", yield) if block_given?
185
+ end
186
+ end
187
+
188
+ # Defines class-level inheritable attribute writer. Attributes are available to subclasses,
189
+ # each subclass has a copy of parent's attribute.
190
+ #
191
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
192
+ # define inheritable writer for.
193
+ # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
194
+ # @return <Array[#to_s]> An Array of the attributes that were made into inheritable writers.
195
+ #
196
+ # @api public
197
+ #
198
+ # @todo We need a style for class_eval <<-HEREDOC. I'd like to make it
199
+ # class_eval(<<-RUBY, __FILE__, __LINE__), but we should codify it somewhere.
200
+ def extlib_inheritable_writer(*ivars)
201
+ options = ivars.extract_options!
202
+
203
+ ivars.each do |ivar|
204
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
205
+ def self.#{ivar}=(obj)
206
+ @#{ivar} = obj
207
+ end
208
+ RUBY
209
+ unless options[:instance_writer] == false
210
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
211
+ def #{ivar}=(obj) self.class.#{ivar} = obj end
212
+ RUBY
213
+ end
214
+
215
+ self.send("#{ivar}=", yield) if block_given?
216
+ end
217
+ end
218
+
219
+ # Defines class-level inheritable attribute accessor. Attributes are available to subclasses,
220
+ # each subclass has a copy of parent's attribute.
221
+ #
222
+ # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
223
+ # define inheritable accessor for.
224
+ # @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
225
+ # @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
226
+ #
227
+ # @api public
228
+ def extlib_inheritable_accessor(*syms, &block)
229
+ extlib_inheritable_reader(*syms)
230
+ extlib_inheritable_writer(*syms, &block)
231
+ end
232
+ end
@@ -0,0 +1,62 @@
1
+ require 'rbconfig'
2
+ module Kernel
3
+ # Sets $VERBOSE to nil for the duration of the block and back to its original value afterwards.
4
+ #
5
+ # silence_warnings do
6
+ # value = noisy_call # no warning voiced
7
+ # end
8
+ #
9
+ # noisy_call # warning voiced
10
+ def silence_warnings
11
+ with_warnings(nil) { yield }
12
+ end
13
+
14
+ # Sets $VERBOSE to true for the duration of the block and back to its original value afterwards.
15
+ def enable_warnings
16
+ with_warnings(true) { yield }
17
+ end
18
+
19
+ # Sets $VERBOSE for the duration of the block and back to its original value afterwards.
20
+ def with_warnings(flag)
21
+ old_verbose, $VERBOSE = $VERBOSE, flag
22
+ yield
23
+ ensure
24
+ $VERBOSE = old_verbose
25
+ end
26
+
27
+ # For compatibility
28
+ def silence_stderr #:nodoc:
29
+ silence_stream(STDERR) { yield }
30
+ end
31
+
32
+ # Silences any stream for the duration of the block.
33
+ #
34
+ # silence_stream(STDOUT) do
35
+ # puts 'This will never be seen'
36
+ # end
37
+ #
38
+ # puts 'But this will'
39
+ def silence_stream(stream)
40
+ old_stream = stream.dup
41
+ stream.reopen(Config::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')
42
+ stream.sync = true
43
+ yield
44
+ ensure
45
+ stream.reopen(old_stream)
46
+ end
47
+
48
+ # Blocks and ignores any exception passed as argument if raised within the block.
49
+ #
50
+ # suppress(ZeroDivisionError) do
51
+ # 1/0
52
+ # puts "This code is NOT reached"
53
+ # end
54
+ #
55
+ # puts "This code gets executed and nothing related to ZeroDivisionError was seen"
56
+ def suppress(*exception_classes)
57
+ begin yield
58
+ rescue Exception => e
59
+ raise unless exception_classes.any? { |cls| e.kind_of?(cls) }
60
+ end
61
+ end
62
+ end