gotime-cassandra_object 0.6.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 (54) hide show
  1. data/CHANGELOG +3 -0
  2. data/Gemfile +14 -0
  3. data/Gemfile.lock +42 -0
  4. data/LICENSE +13 -0
  5. data/README.markdown +79 -0
  6. data/Rakefile +74 -0
  7. data/TODO +2 -0
  8. data/VERSION +1 -0
  9. data/gotime-cassandra_object.gemspec +134 -0
  10. data/lib/cassandra_object.rb +13 -0
  11. data/lib/cassandra_object/associations.rb +35 -0
  12. data/lib/cassandra_object/associations/one_to_many.rb +136 -0
  13. data/lib/cassandra_object/associations/one_to_one.rb +77 -0
  14. data/lib/cassandra_object/attributes.rb +93 -0
  15. data/lib/cassandra_object/base.rb +97 -0
  16. data/lib/cassandra_object/callbacks.rb +10 -0
  17. data/lib/cassandra_object/collection.rb +8 -0
  18. data/lib/cassandra_object/cursor.rb +86 -0
  19. data/lib/cassandra_object/dirty.rb +27 -0
  20. data/lib/cassandra_object/identity.rb +61 -0
  21. data/lib/cassandra_object/identity/abstract_key_factory.rb +36 -0
  22. data/lib/cassandra_object/identity/key.rb +20 -0
  23. data/lib/cassandra_object/identity/natural_key_factory.rb +51 -0
  24. data/lib/cassandra_object/identity/uuid_key_factory.rb +37 -0
  25. data/lib/cassandra_object/indexes.rb +129 -0
  26. data/lib/cassandra_object/log_subscriber.rb +17 -0
  27. data/lib/cassandra_object/migrations.rb +72 -0
  28. data/lib/cassandra_object/mocking.rb +15 -0
  29. data/lib/cassandra_object/persistence.rb +195 -0
  30. data/lib/cassandra_object/serialization.rb +6 -0
  31. data/lib/cassandra_object/type_registration.rb +7 -0
  32. data/lib/cassandra_object/types.rb +128 -0
  33. data/lib/cassandra_object/validation.rb +49 -0
  34. data/test/basic_scenarios_test.rb +243 -0
  35. data/test/callbacks_test.rb +19 -0
  36. data/test/config/cassandra.in.sh +53 -0
  37. data/test/config/log4j.properties +38 -0
  38. data/test/config/storage-conf.xml +221 -0
  39. data/test/connection.rb +25 -0
  40. data/test/cursor_test.rb +66 -0
  41. data/test/dirty_test.rb +34 -0
  42. data/test/fixture_models.rb +90 -0
  43. data/test/identity/natural_key_factory_test.rb +94 -0
  44. data/test/index_test.rb +69 -0
  45. data/test/legacy/test_helper.rb +18 -0
  46. data/test/migration_test.rb +21 -0
  47. data/test/one_to_many_associations_test.rb +163 -0
  48. data/test/test_case.rb +28 -0
  49. data/test/test_helper.rb +16 -0
  50. data/test/time_test.rb +32 -0
  51. data/test/types_test.rb +252 -0
  52. data/test/validation_test.rb +25 -0
  53. data/test/z_mock_test.rb +36 -0
  54. metadata +243 -0
@@ -0,0 +1,6 @@
1
+ module CassandraObject
2
+ module Serialization
3
+ extend ActiveSupport::Concern
4
+ include ActiveModel::Serializers::JSON
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ CassandraObject::Base.register_attribute_type(:integer, Integer, CassandraObject::IntegerType)
2
+ CassandraObject::Base.register_attribute_type(:float, Float, CassandraObject::FloatType)
3
+ CassandraObject::Base.register_attribute_type(:date, Date, CassandraObject::DateType)
4
+ CassandraObject::Base.register_attribute_type(:time, Time, CassandraObject::TimeType)
5
+ CassandraObject::Base.register_attribute_type(:time_with_zone, ActiveSupport::TimeWithZone, CassandraObject::TimeWithZoneType)
6
+ CassandraObject::Base.register_attribute_type(:string, String, CassandraObject::StringType)
7
+ CassandraObject::Base.register_attribute_type(:hash, Hash, CassandraObject::HashType)
@@ -0,0 +1,128 @@
1
+ module CassandraObject
2
+ module IntegerType
3
+ REGEX = /\A[-+]?\d+\Z/
4
+ def encode(int)
5
+ return '' if int.nil?
6
+ raise ArgumentError.new("#{self} requires an Integer. You passed #{int.inspect}") unless int.kind_of?(Integer)
7
+ int.to_s
8
+ end
9
+ module_function :encode
10
+
11
+ def decode(str)
12
+ return nil if str.empty?
13
+ raise ArgumentError.new("#{str} isn't a String that looks like a Integer") unless str.kind_of?(String) && str.match(REGEX)
14
+ str.to_i
15
+ end
16
+ module_function :decode
17
+ end
18
+
19
+ module FloatType
20
+ REGEX = /\A[-+]?\d+(\.\d+)\Z/
21
+ def encode(float)
22
+ return '' if float.nil?
23
+ raise ArgumentError.new("#{self} requires a Float") unless float.kind_of?(Float)
24
+ float.to_s
25
+ end
26
+ module_function :encode
27
+
28
+ def decode(str)
29
+ return nil if str == ''
30
+ raise ArgumentError.new("#{str} isn't a String that looks like a Float") unless str.kind_of?(String) && str.match(REGEX)
31
+ str.to_f
32
+ end
33
+ module_function :decode
34
+ end
35
+
36
+ module DateType
37
+ FORMAT = '%Y-%m-%d'
38
+ REGEX = /\A\d{4}-\d{2}-\d{2}\Z/
39
+ def encode(date)
40
+ raise ArgumentError.new("#{self} requires a Date") unless date.kind_of?(Date)
41
+ date.strftime(FORMAT)
42
+ end
43
+ module_function :encode
44
+
45
+ def decode(str)
46
+ raise ArgumentError.new("#{str} isn't a String that looks like a Date") unless str.kind_of?(String) && str.match(REGEX)
47
+ Date.strptime(str, FORMAT)
48
+ end
49
+ module_function :decode
50
+ end
51
+
52
+ module TimeType
53
+ # lifted from the implementation of Time.xmlschema and simplified
54
+ REGEX = /\A\s*
55
+ (-?\d+)-(\d\d)-(\d\d)
56
+ T
57
+ (\d\d):(\d\d):(\d\d)
58
+ (\.\d*)?
59
+ (Z|[+-]\d\d:\d\d)?
60
+ \s*\z/ix
61
+
62
+ def encode(time)
63
+ raise ArgumentError.new("#{self} requires a Time") unless time.kind_of?(Time)
64
+ time.xmlschema(6)
65
+ end
66
+ module_function :encode
67
+
68
+ def decode(str)
69
+ raise ArgumentError.new("#{str} isn't a String that looks like a Time") unless str.kind_of?(String) && str.match(REGEX)
70
+ Time.xmlschema(str)
71
+ end
72
+ module_function :decode
73
+ end
74
+
75
+ module TimeWithZoneType
76
+ def encode(time)
77
+ TimeType.encode(time.utc)
78
+ end
79
+ module_function :encode
80
+
81
+ def decode(str)
82
+ TimeType.decode(str).in_time_zone
83
+ end
84
+ module_function :decode
85
+ end
86
+
87
+ module StringType
88
+ def encode(str)
89
+ raise ArgumentError.new("#{self} requires a String") unless str.kind_of?(String)
90
+ str
91
+ end
92
+ module_function :encode
93
+
94
+ def decode(str)
95
+ str
96
+ end
97
+ module_function :decode
98
+ end
99
+
100
+ module HashType
101
+ def encode(hash)
102
+ raise ArgumentError.new("#{self} requires a Hash") unless hash.kind_of?(Hash)
103
+ ActiveSupport::JSON.encode(hash)
104
+ end
105
+ module_function :encode
106
+
107
+ def decode(str)
108
+ ActiveSupport::JSON.decode(str)
109
+ end
110
+ module_function :decode
111
+ end
112
+
113
+ module BooleanType
114
+ ALLOWED = [true, false, nil]
115
+ def encode(bool)
116
+ unless ALLOWED.any?{ |a| bool == a }
117
+ raise ArgumentError.new("#{self} requires a Boolean or nil")
118
+ end
119
+ bool ? '1' : '0'
120
+ end
121
+ module_function :encode
122
+
123
+ def decode(bool)
124
+ bool == '1'
125
+ end
126
+ module_function :decode
127
+ end
128
+ end
@@ -0,0 +1,49 @@
1
+ module CassandraObject
2
+ module Validation
3
+ class RecordInvalidError < StandardError
4
+ attr_reader :record
5
+ def initialize(record)
6
+ @record = record
7
+ super("Invalid record: #{@record.errors.full_messages.to_sentence}")
8
+ end
9
+
10
+ def self.raise_error(record)
11
+ raise new(record)
12
+ end
13
+ end
14
+ extend ActiveSupport::Concern
15
+ include ActiveModel::Validations
16
+
17
+ included do
18
+ define_model_callbacks :validation
19
+ define_callbacks :validate, :scope => :name
20
+ end
21
+
22
+ module ClassMethods
23
+ def create!(attributes)
24
+ new(attributes).tap &:save!
25
+ end
26
+ end
27
+
28
+ module InstanceMethods
29
+ def valid?
30
+ run_callbacks :validation do
31
+ super
32
+ end
33
+ end
34
+
35
+ def save
36
+ if valid?
37
+ super
38
+ else
39
+ false
40
+ end
41
+ end
42
+
43
+ def save!
44
+ save || RecordInvalidError.raise_error(self)
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,243 @@
1
+ require 'test_helper'
2
+
3
+ class BasicScenariosTest < CassandraObjectTestCase
4
+ def setup
5
+ super
6
+ @customer = Customer.create :first_name => "Michael",
7
+ :last_name => "Koziarski",
8
+ :date_of_birth => Date.parse("1980/08/15")
9
+ @customer_key = @customer.key.to_s
10
+
11
+ assert @customer.valid?
12
+ end
13
+
14
+ test "get on a non-existent key returns nil" do
15
+ assert_nil Customer.get("THIS IS NOT A KEY")
16
+ end
17
+
18
+ test "a new object can be retrieved by key" do
19
+ other_customer = Customer.get(@customer_key)
20
+ assert_equal @customer, other_customer
21
+
22
+ assert_equal "Michael", other_customer.first_name
23
+ assert_equal "Koziarski", other_customer.last_name
24
+ assert_equal Date.parse("1980-08-15"), other_customer.date_of_birth
25
+ end
26
+
27
+ test "a new object is included in Model.all" do
28
+ assert Customer.all.include?(@customer)
29
+ end
30
+
31
+ test "date_of_birth is a date" do
32
+ assert @customer.date_of_birth.is_a?(Date)
33
+ end
34
+
35
+ test "should not let you assign junk to a date column" do
36
+ assert_raise(ArgumentError) do
37
+ @customer.date_of_birth = 24.5
38
+ end
39
+ end
40
+
41
+ test "should return nil for attributes without a value" do
42
+ assert_nil @customer.preferences
43
+ end
44
+
45
+ test "should let a user set a Hash valued attribute" do
46
+ val = {"a"=>"b"}
47
+ @customer.preferences = val
48
+ assert_equal val, @customer.preferences
49
+ @customer.save
50
+
51
+ other_customer = Customer.get(@customer_key)
52
+ assert_equal val, other_customer.preferences
53
+ end
54
+
55
+ test "should validate strings passed to a typed column" do
56
+ assert_raises(ArgumentError){
57
+ @customer.date_of_birth = "35345908"
58
+ }
59
+ end
60
+
61
+ test "should have a schema version of 0" do
62
+ assert_equal 0, @customer.schema_version
63
+ end
64
+
65
+ test "multiget" do
66
+ custs = Customer.multi_get([@customer_key, "This is not a key either"])
67
+ customer, nothing = custs.values
68
+
69
+ assert_equal @customer, customer
70
+ assert_nil nothing
71
+ end
72
+
73
+ test "creating a new record starts with the right version" do
74
+ @invoice = mock_invoice
75
+
76
+ raw_result = Invoice.connection.get("Invoices", @invoice.key.to_s)
77
+ assert_equal Invoice.current_schema_version, ActiveSupport::JSON.decode(raw_result["schema_version"])
78
+ end
79
+
80
+ test "to_param works" do
81
+ invoice = mock_invoice
82
+ param = invoice.to_param
83
+ assert_match /[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}/, param
84
+ assert_equal invoice.key, Invoice.parse_key(param)
85
+ end
86
+
87
+ test "setting a column_family" do
88
+ class Foo < CassandraObject::Base
89
+ self.column_family = 'Bar'
90
+ end
91
+ assert_equal 'Bar', Foo.column_family
92
+ end
93
+
94
+ context "destroying a customer with invoices" do
95
+ setup do
96
+ @invoice = mock_invoice
97
+ @customer.invoices << @invoice
98
+
99
+ @customer.destroy
100
+ end
101
+
102
+ should "Have removed the customer" do
103
+ assert Customer.connection.get("Customers", @customer.key.to_s).empty?
104
+ end
105
+
106
+ should "Have removed the associations too" do
107
+ assert_equal Hash.new, Customer.connection.get("CustomerRelationships", @customer.key.to_s)
108
+ end
109
+ end
110
+
111
+ context "An object with a natural key" do
112
+ setup do
113
+ @payment = Payment.new :reference_number => "12345",
114
+ :amount => 1001
115
+ @payment.save!
116
+ end
117
+
118
+ should "create a natural key based on that attr" do
119
+ assert_equal "12345", @payment.key.to_s
120
+ end
121
+
122
+ should "have a key equal to another object with that key" do
123
+ p = Payment.new(:reference_number => "12345",
124
+ :amount => 1001)
125
+ p.save
126
+
127
+ assert_equal @payment.key, p.key
128
+ end
129
+ end
130
+
131
+ context "Model with no attributes" do
132
+ setup do
133
+ class Empty < CassandraObject::Base
134
+ end
135
+ end
136
+
137
+ should "work" do
138
+ e = Empty.new
139
+ end
140
+ end
141
+
142
+ context "A model that allows nils" do
143
+ setup do
144
+ class Nilable < CassandraObject::Base
145
+ attribute :user_id, :type => Integer, :allow_nil => true
146
+ end
147
+ end
148
+
149
+ should "should be valid with a nil" do
150
+ n = Nilable.new
151
+ assert n.valid?
152
+ end
153
+ end
154
+
155
+ context "A janky custom key factory" do
156
+ setup do
157
+ class JankyKeys
158
+ def next_key(object)
159
+ nil
160
+ end
161
+ end
162
+ class JankyObject < CassandraObject::Base
163
+ key JankyKeys.new
164
+ end
165
+ @object = JankyObject.new
166
+ end
167
+
168
+ should "raise an error on nil key" do
169
+ assert_raises(RuntimeError) do
170
+ @object.save
171
+ end
172
+ end
173
+ end
174
+
175
+ test "updating columns" do
176
+ appt = Appointment.new(:start_time => Time.now, :title => 'emergency meeting')
177
+ appt.save!
178
+ appt = Appointment.get(appt.key)
179
+ appt.start_time = Time.now + 1.hour
180
+ appt.end_time = Time.now.utc + 5.hours
181
+ appt.save!
182
+ assert appt.reload.end_time.is_a?(ActiveSupport::TimeWithZone)
183
+ end
184
+
185
+ test "Saving a class with custom attributes uses the custom converter" do
186
+ @customer.custom_storage = "hello"
187
+ @customer.save
188
+
189
+ raw_result = Customer.connection.get("Customers", @customer.key.to_s)
190
+
191
+ assert_equal "olleh", raw_result["custom_storage"]
192
+ assert_equal "hello", @customer.reload.custom_storage
193
+
194
+ end
195
+
196
+ context "setting valid consistency levels" do
197
+ setup do
198
+ class Senate < CassandraObject::Base
199
+ consistency_levels :write => :quorum, :read => :quorum
200
+ end
201
+ end
202
+
203
+ should "should have the settings" do
204
+ assert_equal :quorum, Senate.write_consistency
205
+ assert_equal :quorum, Senate.read_consistency
206
+ end
207
+ end
208
+
209
+ context "setting invalid consistency levels" do
210
+ context "invalid write consistency" do
211
+ should "raise an error" do
212
+ assert_raises(ArgumentError) do
213
+ class BadWriter < CassandraObject::Base
214
+ consistency_levels :write => :foo, :read => :quorum
215
+ end
216
+ end
217
+ end
218
+ end
219
+
220
+ context "invalid read consistency" do
221
+ should "raise an error" do
222
+ assert_raises(ArgumentError) do
223
+ class BadReader < CassandraObject::Base
224
+ consistency_levels :write => :quorum, :read => :foo
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ test "ignoring columns we don't know about" do
232
+ # if there's a column in the row that's not configured as an attribute, it should be ignored with no errors
233
+
234
+ payment = Payment.new(:reference_number => 'abc123', :amount => 26)
235
+ payment.save
236
+
237
+ Payment.connection.insert(Payment.column_family, payment.key.to_s, {"bogus" => 'very bogus', "schema_version" => payment.schema_version.to_s}, :consistency => Payment.send(:write_consistency_for_thrift))
238
+
239
+ assert_nothing_raised do
240
+ Payment.get(payment.key)
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,19 @@
1
+ require 'test_helper'
2
+
3
+ class CallbacksTest < CassandraObjectTestCase
4
+ def setup
5
+ super
6
+ end
7
+
8
+ context "a newly created record" do
9
+ setup do
10
+ @customer = Customer.create! :first_name => "Tom",
11
+ :last_name => "Ward",
12
+ :date_of_birth => Date.parse("1977/12/04")
13
+ end
14
+
15
+ should "have had after_create called" do
16
+ assert @customer.after_create_called?
17
+ end
18
+ end
19
+ end