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.
- data/CHANGELOG +3 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +13 -0
- data/README.markdown +79 -0
- data/Rakefile +74 -0
- data/TODO +2 -0
- data/VERSION +1 -0
- data/gotime-cassandra_object.gemspec +134 -0
- data/lib/cassandra_object.rb +13 -0
- data/lib/cassandra_object/associations.rb +35 -0
- data/lib/cassandra_object/associations/one_to_many.rb +136 -0
- data/lib/cassandra_object/associations/one_to_one.rb +77 -0
- data/lib/cassandra_object/attributes.rb +93 -0
- data/lib/cassandra_object/base.rb +97 -0
- data/lib/cassandra_object/callbacks.rb +10 -0
- data/lib/cassandra_object/collection.rb +8 -0
- data/lib/cassandra_object/cursor.rb +86 -0
- data/lib/cassandra_object/dirty.rb +27 -0
- data/lib/cassandra_object/identity.rb +61 -0
- data/lib/cassandra_object/identity/abstract_key_factory.rb +36 -0
- data/lib/cassandra_object/identity/key.rb +20 -0
- data/lib/cassandra_object/identity/natural_key_factory.rb +51 -0
- data/lib/cassandra_object/identity/uuid_key_factory.rb +37 -0
- data/lib/cassandra_object/indexes.rb +129 -0
- data/lib/cassandra_object/log_subscriber.rb +17 -0
- data/lib/cassandra_object/migrations.rb +72 -0
- data/lib/cassandra_object/mocking.rb +15 -0
- data/lib/cassandra_object/persistence.rb +195 -0
- data/lib/cassandra_object/serialization.rb +6 -0
- data/lib/cassandra_object/type_registration.rb +7 -0
- data/lib/cassandra_object/types.rb +128 -0
- data/lib/cassandra_object/validation.rb +49 -0
- data/test/basic_scenarios_test.rb +243 -0
- data/test/callbacks_test.rb +19 -0
- data/test/config/cassandra.in.sh +53 -0
- data/test/config/log4j.properties +38 -0
- data/test/config/storage-conf.xml +221 -0
- data/test/connection.rb +25 -0
- data/test/cursor_test.rb +66 -0
- data/test/dirty_test.rb +34 -0
- data/test/fixture_models.rb +90 -0
- data/test/identity/natural_key_factory_test.rb +94 -0
- data/test/index_test.rb +69 -0
- data/test/legacy/test_helper.rb +18 -0
- data/test/migration_test.rb +21 -0
- data/test/one_to_many_associations_test.rb +163 -0
- data/test/test_case.rb +28 -0
- data/test/test_helper.rb +16 -0
- data/test/time_test.rb +32 -0
- data/test/types_test.rb +252 -0
- data/test/validation_test.rb +25 -0
- data/test/z_mock_test.rb +36 -0
- metadata +243 -0
@@ -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
|