activeredis 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/README.markdown +68 -0
- data/Rakefile +1 -0
- data/activeredis-console.rb +4 -0
- data/activeredis.gemspec +23 -0
- data/benchmark.rb +61 -0
- data/benchmark_results.txt +19 -0
- data/lib/activeredis/version.rb +3 -0
- data/lib/activeredis.rb +325 -0
- data/sample/user.rb +38 -0
- data/spec/active_redis_spec.rb +264 -0
- data/spec/spec_helper.rb +10 -0
- metadata +97 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# ActiveRedis
|
2
|
+
|
3
|
+
ActiveModel based object persisting library for [Redis](http://code.google.com/p/redis) key-value database.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
* ActiveModel compatibility
|
8
|
+
* Race condition free operations
|
9
|
+
|
10
|
+
## Missing features
|
11
|
+
|
12
|
+
A lot. ActiveRedis is currently designed to handle concurrency issues. Use [OHM](http://ohm.keyvalue.org/) or [remodel](http://github.com/tlossen/remodel) if you need advanced features and can accept some potential concurrency issues.
|
13
|
+
|
14
|
+
* Indexes
|
15
|
+
* Relations
|
16
|
+
* Other cool stuff
|
17
|
+
|
18
|
+
## How to start
|
19
|
+
|
20
|
+
1. Clone the repository
|
21
|
+
3. Install gems
|
22
|
+
|
23
|
+
bundle install
|
24
|
+
|
25
|
+
4. Run spec
|
26
|
+
|
27
|
+
rspec spec/core_spec.rb
|
28
|
+
|
29
|
+
## Sample Code
|
30
|
+
|
31
|
+
require 'active_redis'
|
32
|
+
require 'pp'
|
33
|
+
|
34
|
+
class User < ActiveRedis::Base
|
35
|
+
fields :name, :age, :country
|
36
|
+
end
|
37
|
+
|
38
|
+
create user(1)
|
39
|
+
user = User.new :age => 22, :name => "Joe"
|
40
|
+
user.save
|
41
|
+
|
42
|
+
# create user(2)
|
43
|
+
user = User.new
|
44
|
+
user.age = 12
|
45
|
+
user.name = "Tom"
|
46
|
+
user.country = "japan"
|
47
|
+
user.save
|
48
|
+
|
49
|
+
puts "count: #{User.count}" #=> 2
|
50
|
+
pp joe = User.find_by_name("Joe") #=>#<User:0x0000010193e930 @attributes={"age"=>"22", "name"=>"Joe"}, @id="1">
|
51
|
+
pp tom = User.find_by_age(12) #=>#<User:0x00000101910c60 @attributes={"age"=>"12", "name"=>"Tom", "country"=>"japan"}, @id="2">
|
52
|
+
|
53
|
+
joe.destroy
|
54
|
+
puts "count: #{User.count}" #=> 1
|
55
|
+
|
56
|
+
tom.destroy
|
57
|
+
puts "count: #{User.count}" #=> 0
|
58
|
+
|
59
|
+
pp User.find_by_name("Tom") #=> nil
|
60
|
+
|
61
|
+
User.delete_all
|
62
|
+
|
63
|
+
|
64
|
+
## Contributing
|
65
|
+
|
66
|
+
Pull requests are welcome!
|
67
|
+
|
68
|
+
For any questions contact [yamadakazu45@gmail.com](mailto:yamadakazu45@gmail.com)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/activeredis.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'activeredis/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "activeredis"
|
8
|
+
gem.version = ActiveRedis::VERSION
|
9
|
+
gem.authors = ["Kazuhiro Yamada", "Yuta Hirakawa"]
|
10
|
+
gem.email = ["sonixlabs@sonix.asia", "kyamada@sonix.asia"]
|
11
|
+
gem.description = %q{ActiveModel based object persistance library for Redis}
|
12
|
+
gem.summary = %q{ActiveModel based object persisting library for Redis key-value database.}
|
13
|
+
gem.homepage = "https://github.com/sonixlabs/activeredis"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'redis'
|
21
|
+
gem.add_dependency 'activemodel'
|
22
|
+
gem.add_development_dependency 'rspec'
|
23
|
+
end
|
data/benchmark.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
$LOAD_PATH << File.dirname(__FILE__)
|
2
|
+
require 'benchmark'
|
3
|
+
require 'lib/active_redis'
|
4
|
+
|
5
|
+
class Post < ActiveRedis::Base
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
LOREM = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
11
|
+
|
12
|
+
# Returns 0 if first round, otherwise return offset which is the sum
|
13
|
+
# of previous rounds.
|
14
|
+
def calculate_offset(index, rounds)
|
15
|
+
return (index == 0) ? 0 : rounds[0..index-1].inject{|sum,x| sum +x}
|
16
|
+
end
|
17
|
+
|
18
|
+
Benchmark.bm(26) do |x|
|
19
|
+
|
20
|
+
rounds = [1000,5000,10000]
|
21
|
+
|
22
|
+
rounds.each_with_index do |howmany, index|
|
23
|
+
|
24
|
+
x.report("Create #{howmany} posts: ") do
|
25
|
+
howmany.times do |t|
|
26
|
+
p = Post.new({ :topic => "a"*(8+rand(55)),
|
27
|
+
:author => "a"*(8+rand(55)),
|
28
|
+
:content => LOREM*(8+rand(55))})
|
29
|
+
p.save
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
x.report("Count posts: ") do
|
34
|
+
Post.count
|
35
|
+
end
|
36
|
+
|
37
|
+
x.report("Find posts: ") do
|
38
|
+
howmany.times do |t|
|
39
|
+
p = Post.find(calculate_offset(index, rounds)+t+1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
x.report("Update (find+save) posts: ") do
|
44
|
+
howmany.times do |t|
|
45
|
+
p = Post.find(calculate_offset(index, rounds)+t+1)
|
46
|
+
p.save
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
x.report("Delete posts: ") do
|
51
|
+
howmany.times do |t|
|
52
|
+
p = Post.find(calculate_offset(index, rounds)+t+1)
|
53
|
+
p.destroy
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
puts
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
user system total real
|
2
|
+
Create 1000 posts: 0.350000 0.110000 0.460000 ( 0.708575)
|
3
|
+
Count posts: 0.000000 0.000000 0.000000 ( 0.000095)
|
4
|
+
Find posts: 0.180000 0.040000 0.220000 ( 0.301364)
|
5
|
+
Update (find+save) posts: 0.420000 0.140000 0.560000 ( 0.845421)
|
6
|
+
Delete posts: 0.310000 0.080000 0.390000 ( 0.479831)
|
7
|
+
|
8
|
+
Create 5000 posts: 1.760000 0.560000 2.320000 ( 3.553861)
|
9
|
+
Count posts: 0.000000 0.000000 0.000000 ( 0.000091)
|
10
|
+
Find posts: 0.900000 0.210000 1.110000 ( 1.509994)
|
11
|
+
Update (find+save) posts: 2.140000 0.680000 2.820000 ( 4.263500)
|
12
|
+
Delete posts: 1.490000 0.390000 1.880000 ( 2.368327)
|
13
|
+
|
14
|
+
Create 10000 posts: 3.500000 1.100000 4.600000 ( 7.094810)
|
15
|
+
Count posts: 0.000000 0.000000 0.000000 ( 0.000092)
|
16
|
+
Find posts: 1.840000 0.420000 2.260000 ( 3.062314)
|
17
|
+
Update (find+save) posts: 4.250000 1.350000 5.600000 ( 8.498068)
|
18
|
+
Delete posts: 3.040000 0.790000 3.830000 ( 4.838310)
|
19
|
+
|
data/lib/activeredis.rb
ADDED
@@ -0,0 +1,325 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'time'
|
3
|
+
require 'active_model'
|
4
|
+
|
5
|
+
require 'active_model/version'
|
6
|
+
|
7
|
+
class Module
|
8
|
+
def module_attr_reader(*syms)
|
9
|
+
syms.each do |sym|
|
10
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
11
|
+
@@#{sym} = nil unless defined?(@@#{sym})
|
12
|
+
def self.#{sym}()
|
13
|
+
return @@#{sym}
|
14
|
+
end
|
15
|
+
EOS
|
16
|
+
end
|
17
|
+
end
|
18
|
+
def module_attr_writer(*syms)
|
19
|
+
syms.each do |sym|
|
20
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
21
|
+
def self.#{sym}=(obj)
|
22
|
+
@@#{sym} = obj
|
23
|
+
end
|
24
|
+
EOS
|
25
|
+
end
|
26
|
+
end
|
27
|
+
def module_attr_accessor(*syms)
|
28
|
+
module_attr_reader(*syms)
|
29
|
+
module_attr_writer(*syms)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module ActiveRedis
|
34
|
+
module_attr_accessor :host, :port
|
35
|
+
@@host = "localhost"
|
36
|
+
@@port = "6379"
|
37
|
+
|
38
|
+
class ActiveRedisError < StandardError
|
39
|
+
end
|
40
|
+
|
41
|
+
# Raised when Active Redis cannot find record by given id or set of ids.
|
42
|
+
class RecordNotFound < ActiveRedisError
|
43
|
+
end
|
44
|
+
|
45
|
+
class Base
|
46
|
+
include ActiveModel::Validations
|
47
|
+
include ActiveModel::Dirty
|
48
|
+
include ActiveModel::Serialization
|
49
|
+
include ActiveModel::Serializers::JSON
|
50
|
+
include ActiveModel::Naming
|
51
|
+
|
52
|
+
QUEUED = "QUEUED"
|
53
|
+
|
54
|
+
# RAILSISM
|
55
|
+
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values
|
56
|
+
# --> means: Strings as keys!
|
57
|
+
# --> initialize stringifies
|
58
|
+
#
|
59
|
+
# called by to_json for_example
|
60
|
+
attr_reader :attributes
|
61
|
+
attr_reader :id
|
62
|
+
attr :frozen
|
63
|
+
|
64
|
+
class << self; attr_accessor :_fields; end
|
65
|
+
|
66
|
+
# INSTANCE METHODS
|
67
|
+
|
68
|
+
def initialize(attributes = {}, id = nil)
|
69
|
+
@id = id if id
|
70
|
+
@attributes = {}
|
71
|
+
initialize_attributes(attributes)
|
72
|
+
frozen = false
|
73
|
+
end
|
74
|
+
|
75
|
+
# Object's attributes' keys are converted to strings because of railsisms.
|
76
|
+
# Because of activeredisism, also values are converted to strings for consistency.
|
77
|
+
def initialize_attributes(attributes)
|
78
|
+
fields = Hash[[self.class._fields, Array.new(self.class._fields.size)].transpose]
|
79
|
+
fields.stringify_keys! # NEEDS to be strings for railsisms
|
80
|
+
attributes.stringify_keys! # NEEDS to be strings for railsisms
|
81
|
+
attributes.each_pair { |key, value| attributes[key] = value.to_s }
|
82
|
+
fields.merge!(attributes)
|
83
|
+
@attributes.merge!(fields)
|
84
|
+
@attributes.each_pair do |key, value|
|
85
|
+
self.class.define_field key
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def save
|
90
|
+
creation = new_record?
|
91
|
+
@id = self.class.fetch_new_identifier if creation
|
92
|
+
while true
|
93
|
+
begin
|
94
|
+
connection.multi
|
95
|
+
rescue
|
96
|
+
sleep(0.1)
|
97
|
+
redo
|
98
|
+
end
|
99
|
+
break
|
100
|
+
end
|
101
|
+
if @attributes.size > 0
|
102
|
+
@attributes.each_pair { |key, value|
|
103
|
+
if key.to_sym == :updated_at
|
104
|
+
value = Time.now.to_s
|
105
|
+
end
|
106
|
+
connection.hset("#{key_namespace}:attributes", key, value)
|
107
|
+
}
|
108
|
+
end
|
109
|
+
connection.zadd("#{class_namespace}:all", @id, @id)
|
110
|
+
connection.exec
|
111
|
+
return true
|
112
|
+
end
|
113
|
+
|
114
|
+
def update_attributes(attributes)
|
115
|
+
attributes.stringify_keys! # NEEDS to be strings for railsisms
|
116
|
+
attributes.each_pair { |key, value| attributes[key] = value.to_s }
|
117
|
+
@attributes.merge!(attributes)
|
118
|
+
attributes.each_pair do |key, value|
|
119
|
+
self.class.define_field key
|
120
|
+
end
|
121
|
+
save
|
122
|
+
end
|
123
|
+
|
124
|
+
def reload
|
125
|
+
@attributes = connection.hgetall "#{key_namespace}:attributes"
|
126
|
+
end
|
127
|
+
|
128
|
+
def new_record?
|
129
|
+
@id == nil
|
130
|
+
end
|
131
|
+
|
132
|
+
def key_namespace
|
133
|
+
"#{self.class.key_namespace}:#{self.id}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def class_namespace
|
137
|
+
"#{self.class.key_namespace}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def connection
|
141
|
+
self.class.connection
|
142
|
+
end
|
143
|
+
|
144
|
+
def destroy
|
145
|
+
connection.multi do
|
146
|
+
connection.del "#{key_namespace}:attributes"
|
147
|
+
connection.zrem "#{class_namespace}:all", @id
|
148
|
+
@frozen = true
|
149
|
+
end
|
150
|
+
return true
|
151
|
+
end
|
152
|
+
|
153
|
+
def frozen?
|
154
|
+
@frozen
|
155
|
+
end
|
156
|
+
|
157
|
+
def add_attribute(name, value=nil)
|
158
|
+
initialize_attributes({name => value})
|
159
|
+
end
|
160
|
+
|
161
|
+
def [](field)
|
162
|
+
send(field)
|
163
|
+
end
|
164
|
+
|
165
|
+
def []=(field, value)
|
166
|
+
send(field+"=", value)
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# CLASS METHODS
|
171
|
+
#
|
172
|
+
|
173
|
+
def self.create(attributes)
|
174
|
+
self.new(attributes).save
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.define_field(field)
|
178
|
+
define_method field.to_sym do
|
179
|
+
if field.to_sym == :updated_at
|
180
|
+
Time.parse(@attributes["#{field}"])
|
181
|
+
else
|
182
|
+
@attributes["#{field}"]
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
define_method "#{field}=".to_sym do |new_value|
|
187
|
+
@attributes["#{field}"] = new_value.to_s
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Run this method to declare the fields of your model.
|
192
|
+
def self.fields(*fields)
|
193
|
+
self._fields ||= []
|
194
|
+
self._fields = fields
|
195
|
+
end
|
196
|
+
|
197
|
+
def self.get_fields
|
198
|
+
self._fields
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.key_namespace
|
202
|
+
"#{self}"
|
203
|
+
end
|
204
|
+
|
205
|
+
def self.fetch_new_identifier
|
206
|
+
self.connection.incr self.identifier_sequencer
|
207
|
+
end
|
208
|
+
|
209
|
+
def self.identifier_sequencer
|
210
|
+
"#{key_namespace}:sequence"
|
211
|
+
end
|
212
|
+
|
213
|
+
def self.inherited(child)
|
214
|
+
#puts "Redis.new(:host => #{ActiveRedis.host}, :port => #{ActiveRedis.port})"
|
215
|
+
@@redis = Redis.new(:host => ActiveRedis.host, :port => ActiveRedis.port)
|
216
|
+
@@class = child
|
217
|
+
end
|
218
|
+
|
219
|
+
def self.redis_information
|
220
|
+
connection.info # call_command [:info]
|
221
|
+
end
|
222
|
+
|
223
|
+
def self.connection
|
224
|
+
@@redis
|
225
|
+
end
|
226
|
+
|
227
|
+
def self.count
|
228
|
+
begin
|
229
|
+
size = connection.zcard "#{key_namespace}:all"
|
230
|
+
while size == QUEUED
|
231
|
+
sleep(0.1)
|
232
|
+
size = connection.zcard "#{key_namespace}:all"
|
233
|
+
end
|
234
|
+
return size
|
235
|
+
rescue RuntimeError => e
|
236
|
+
return 0
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def self.find_all()
|
241
|
+
record = []
|
242
|
+
# TODO Interim fix, "QUEUED" is find(id) rescue
|
243
|
+
while true
|
244
|
+
ids = connection.zrange "#{key_namespace}:all", 0, count
|
245
|
+
while ids == QUEUED
|
246
|
+
sleep(0.1)
|
247
|
+
ids = connection.zrange "#{key_namespace}:all", 0, count
|
248
|
+
end
|
249
|
+
begin
|
250
|
+
ids.each do |id|
|
251
|
+
record << find(id)
|
252
|
+
end
|
253
|
+
rescue
|
254
|
+
redo
|
255
|
+
end
|
256
|
+
break
|
257
|
+
end
|
258
|
+
record
|
259
|
+
end
|
260
|
+
|
261
|
+
def self.all
|
262
|
+
find_all
|
263
|
+
end
|
264
|
+
|
265
|
+
def self.delete_all
|
266
|
+
records = find_all
|
267
|
+
records.each do |record|
|
268
|
+
record.destroy
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def self.find(id)
|
273
|
+
return find_all if id == :all
|
274
|
+
exists = connection.zscore "#{key_namespace}:all", id
|
275
|
+
raise RecordNotFound.new("Couldn't find #{self.name} with ID=#{id}") unless exists
|
276
|
+
attributes = connection.hgetall "#{key_namespace}:#{id}:attributes"
|
277
|
+
obj = self.new attributes, id
|
278
|
+
return obj
|
279
|
+
end
|
280
|
+
|
281
|
+
def self.find_all_by_param(field, value)
|
282
|
+
finded = []
|
283
|
+
records = find_all
|
284
|
+
records.each do |record|
|
285
|
+
if record.attributes[field.to_s] == value.to_s
|
286
|
+
finded << record
|
287
|
+
end
|
288
|
+
end
|
289
|
+
return finded
|
290
|
+
end
|
291
|
+
|
292
|
+
def self.find_by_param(field, value)
|
293
|
+
records = find_all_by_param(field, value)
|
294
|
+
if records.size > 0
|
295
|
+
return records[0]
|
296
|
+
else
|
297
|
+
nil
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def self.method_missing(name, *args)
|
302
|
+
return find_by_param($1.to_sym, args[0]) if name.to_s =~ /^find_by_(.*)/
|
303
|
+
return find_all_by_param($1.to_sym, args[0]) if name.to_s =~ /^find_all_by_(.*)/
|
304
|
+
super
|
305
|
+
end
|
306
|
+
|
307
|
+
def self.delete_unused_field
|
308
|
+
ids = connection.zrange "#{key_namespace}:all", 0, count
|
309
|
+
if ids.size > 0
|
310
|
+
attributes = connection.hgetall "#{key_namespace}:#{ids[0]}:attributes"
|
311
|
+
now_keys = self.get_fields
|
312
|
+
array = []
|
313
|
+
now_keys.each do |key|
|
314
|
+
array << key.to_s
|
315
|
+
end
|
316
|
+
attributes.reject! {|key| array.include? key }
|
317
|
+
attributes.keys.each do |delete_key|
|
318
|
+
ids.each do |id|
|
319
|
+
connection.hdel "#{key_namespace}:#{id}:attributes", delete_key
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
data/sample/user.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'active_redis'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
# ActiveRedis.host = "localhost"
|
5
|
+
# ActiveRedis.port = "6379"
|
6
|
+
class User < ActiveRedis::Base
|
7
|
+
fields "name", "age", "country", "updated_at"
|
8
|
+
end
|
9
|
+
|
10
|
+
# create user(1)
|
11
|
+
user = User.new :age => 22, :name => "Joe"
|
12
|
+
user.save
|
13
|
+
|
14
|
+
# create user(2)
|
15
|
+
user = User.new
|
16
|
+
user.age = 12
|
17
|
+
user.name = "Tom"
|
18
|
+
user.country = "japan"
|
19
|
+
user.save
|
20
|
+
|
21
|
+
puts "count: #{User.count}" #=> 2
|
22
|
+
pp joe = User.find_by_name("Joe") #=>#<User:0x0000010193e930 @attributes={"age"=>"22", "name"=>"Joe"}, @id="1">
|
23
|
+
pp tom = User.find_by_name("Tom") #=>#<User:0x00000101910c60 @attributes={"age"=>"12", "name"=>"Tom", "country"=>"japan"}, @id="2">
|
24
|
+
|
25
|
+
tom.update_attributes({:age => 20})
|
26
|
+
#tom = User.find_by_age(20)
|
27
|
+
tom.reload
|
28
|
+
pp tom
|
29
|
+
|
30
|
+
joe.destroy
|
31
|
+
puts "count: #{User.count}" #=> 1
|
32
|
+
|
33
|
+
User.delete_all
|
34
|
+
puts "count: #{User.count}" #=> 0
|
35
|
+
|
36
|
+
pp User.find_by_name("Tom") #=> nil
|
37
|
+
|
38
|
+
|
@@ -0,0 +1,264 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Cat < ActiveRedis::Base
|
4
|
+
fields :name, :age
|
5
|
+
end
|
6
|
+
|
7
|
+
class Dog < ActiveRedis::Base
|
8
|
+
fields :category, :name, :color
|
9
|
+
end
|
10
|
+
|
11
|
+
describe Cat do
|
12
|
+
|
13
|
+
before(:each) do
|
14
|
+
ActiveRedis::Base.connection.flushall
|
15
|
+
@cat = Cat.new(:name => "lol")
|
16
|
+
@dog = Dog.new(:category => "Dachshund")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should be a rektive model" do
|
20
|
+
Cat.ancestors.should include(ActiveRedis::Base)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should give out info on the redis server" do
|
24
|
+
Cat.redis_information.should be_an_instance_of(Hash)
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "counting" do
|
28
|
+
it "should know count of its objects when empty" do
|
29
|
+
Cat.count.should == 0
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should know count of its objects" do
|
33
|
+
@cat.save
|
34
|
+
Cat.count.should == 1
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "attributes" do
|
39
|
+
|
40
|
+
it "should have attributes" do
|
41
|
+
@cat.attributes.should include("name")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should have accessors for all attributes" do
|
45
|
+
@cat.name.should == "lol"
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should get fields" do
|
49
|
+
Cat.get_fields.should == [:name, :age]
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should stringify all values" do
|
53
|
+
numeric_cat = Cat.new(:age => 25, :length => 5.4)
|
54
|
+
numeric_cat.age.should == "25"
|
55
|
+
numeric_cat.length.should == "5.4"
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should add a new attribute to a persisted object" do
|
59
|
+
@cat.save
|
60
|
+
@cat.add_attribute(:color)
|
61
|
+
@cat.color.should == ""
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should add a new attribute with empty value and persist changes" do
|
65
|
+
@cat.save
|
66
|
+
@cat.add_attribute(:color)
|
67
|
+
@cat.color.should == ""
|
68
|
+
@cat.save
|
69
|
+
Cat.find(@cat.id).color.should == ""
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should add a new attribute with non-empty value and persist changes" do
|
73
|
+
@cat.save
|
74
|
+
@cat.add_attribute(:color, "black")
|
75
|
+
@cat.color.should == "black"
|
76
|
+
@cat.save
|
77
|
+
Cat.find(@cat.id).color.should == "black"
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should add a new attribute with a value" do
|
81
|
+
@cat.add_attribute(:color, "black")
|
82
|
+
@cat.color.should == "black"
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should add a new attribute to a non-persisted object" do
|
86
|
+
cat = Cat.new(:name => "long")
|
87
|
+
cat.add_attribute(:length, 2.5)
|
88
|
+
cat.length.should == "2.5"
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should update a attribute" do
|
92
|
+
cat = Cat.new(:name => "mike", :color => "black")
|
93
|
+
cat.update_attributes({:name => "tama"})
|
94
|
+
cat.reload
|
95
|
+
cat.name.should == "tama"
|
96
|
+
cat.color.should == "black"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should convert attribute values to string when creating new" do
|
100
|
+
cat = Cat.new(:age => 2.5, :long => true, :length => 100)
|
101
|
+
cat.attributes["age"].should == "2.5"
|
102
|
+
cat.attributes["long"].should == "true"
|
103
|
+
cat.attributes["length"].should == "100"
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should create persisted object" do
|
107
|
+
Cat.create(:age => 100, :long => true, :length => 120)
|
108
|
+
Cat.find_by_age(100).age.should == "100"
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "persisting simple objects" do
|
114
|
+
before(:each) do
|
115
|
+
@cat = Cat.new(:name => "lol",
|
116
|
+
:color => "white")
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should persist a simple object successfully" do
|
120
|
+
@cat.save.should be_true
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should have a key to fetch the next free identifier from" do
|
124
|
+
Cat.identifier_sequencer.should == "Cat:sequence"
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should reserve a unique identifier for the object" do
|
128
|
+
Cat.connection.should_receive(:incr).with(Cat.identifier_sequencer).and_return(1)
|
129
|
+
|
130
|
+
Cat.fetch_new_identifier
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should save the identifier to the object" do
|
134
|
+
@cat.save
|
135
|
+
|
136
|
+
@cat.id.should_not be_nil
|
137
|
+
end
|
138
|
+
|
139
|
+
# it "should persist the attributes of the object" do
|
140
|
+
# Cat.stub!(:fetch_new_identifier).and_return(1)
|
141
|
+
# Cat.connection.stub!(:multi).and_yield
|
142
|
+
#
|
143
|
+
# Cat.connection.should_receive(:call_command).with([ "hmset",
|
144
|
+
# "#{Cat.key_namespace}:1:attributes",
|
145
|
+
# "name", "lol",
|
146
|
+
# "color", "white"]).and_return(true)
|
147
|
+
#
|
148
|
+
# Cat.connection.should_receive(:zadd).with("#{Cat.key_namespace}:all", 1, 1).and_return(true)
|
149
|
+
#
|
150
|
+
# @cat.save
|
151
|
+
# end
|
152
|
+
|
153
|
+
# it "should have atomic save operations" do
|
154
|
+
# Cat.stub!(:fetch_new_identifier)
|
155
|
+
# Cat.connection.should_receive(:multi).and_yield
|
156
|
+
# Cat.connection.should_receive(:call_command)
|
157
|
+
# Cat.connection.should_receive(:zadd)
|
158
|
+
# @cat.save
|
159
|
+
# end
|
160
|
+
|
161
|
+
it "should persist attributeless cat" do
|
162
|
+
attributeless = Cat.new
|
163
|
+
attributeless.save
|
164
|
+
|
165
|
+
attributeless.id.should_not be_nil
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "destroy" do
|
173
|
+
it "object should remove itself" do
|
174
|
+
@cat.save
|
175
|
+
|
176
|
+
@cat.destroy.should == true
|
177
|
+
lambda {
|
178
|
+
Cat.find(@cat.id)
|
179
|
+
}.should raise_error(ActiveRedis::RecordNotFound)
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
it "should destroy as an atomic operation" do
|
184
|
+
dead_cat = Cat.new({}, 1)
|
185
|
+
Cat.connection.should_receive(:multi).and_yield
|
186
|
+
Cat.connection.should_receive(:del).with("#{Cat.key_namespace}:1:attributes").and_return(10)
|
187
|
+
Cat.connection.should_receive(:zrem).with("#{Cat.key_namespace}:all", 1).and_return(1)
|
188
|
+
dead_cat.destroy.should == true
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
describe "find" do
|
194
|
+
|
195
|
+
it "should raise error if record was not found" do
|
196
|
+
no_such_cat_id = 99
|
197
|
+
lambda {
|
198
|
+
Cat.find(no_such_cat_id)
|
199
|
+
}.should raise_error(ActiveRedis::RecordNotFound)
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
it "should check that object exists in all" do
|
204
|
+
@cat.save
|
205
|
+
Cat.find(1)
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should find existing cat" do
|
209
|
+
attributes_to_persist = {"name" => "", "age" => "", "fur" => "long", "eyes" => "blue"}
|
210
|
+
c = Cat.new attributes_to_persist
|
211
|
+
c.save
|
212
|
+
|
213
|
+
same_cat = Cat.find(c.id)
|
214
|
+
same_cat.attributes.should == attributes_to_persist
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should have id set" do
|
218
|
+
@cat.save
|
219
|
+
|
220
|
+
existing_cat = Cat.find(1)
|
221
|
+
existing_cat.id.should == 1
|
222
|
+
end
|
223
|
+
|
224
|
+
it "should find attributeless cat" do
|
225
|
+
attributeless = Cat.new
|
226
|
+
attributeless.save
|
227
|
+
Cat.find(attributeless.id).should_not be_nil
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
|
232
|
+
describe "update" do
|
233
|
+
|
234
|
+
it "should not create new objects when updating" do
|
235
|
+
same_cat = Cat.new
|
236
|
+
same_cat.save
|
237
|
+
id_before_save = same_cat.id
|
238
|
+
|
239
|
+
same_cat.save
|
240
|
+
same_cat.id.should == id_before_save
|
241
|
+
end
|
242
|
+
|
243
|
+
it "should update attribute with new value" do
|
244
|
+
|
245
|
+
@cat.save
|
246
|
+
|
247
|
+
new_name = "updated lolcat"
|
248
|
+
@cat.name = new_name
|
249
|
+
@cat.save
|
250
|
+
Cat.find(@cat.id).name.should == new_name
|
251
|
+
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
describe "ActiveModel compatibility" do
|
258
|
+
|
259
|
+
it "should respond to new_record?" do
|
260
|
+
Cat.new.new_record?.should == true
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activeredis
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Kazuhiro Yamada
|
9
|
+
- Yuta Hirakawa
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-10-09 00:00:00.000000000Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: redis
|
17
|
+
requirement: &70240177908780 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70240177908780
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: activemodel
|
28
|
+
requirement: &70240177908360 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *70240177908360
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rspec
|
39
|
+
requirement: &70240177907940 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :development
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *70240177907940
|
48
|
+
description: ActiveModel based object persistance library for Redis
|
49
|
+
email:
|
50
|
+
- sonixlabs@sonix.asia
|
51
|
+
- kyamada@sonix.asia
|
52
|
+
executables: []
|
53
|
+
extensions: []
|
54
|
+
extra_rdoc_files: []
|
55
|
+
files:
|
56
|
+
- .gitignore
|
57
|
+
- .rspec
|
58
|
+
- Gemfile
|
59
|
+
- README.markdown
|
60
|
+
- Rakefile
|
61
|
+
- activeredis-console.rb
|
62
|
+
- activeredis.gemspec
|
63
|
+
- benchmark.rb
|
64
|
+
- benchmark_results.txt
|
65
|
+
- lib/activeredis.rb
|
66
|
+
- lib/activeredis/version.rb
|
67
|
+
- sample/user.rb
|
68
|
+
- spec/active_redis_spec.rb
|
69
|
+
- spec/spec_helper.rb
|
70
|
+
homepage: https://github.com/sonixlabs/activeredis
|
71
|
+
licenses: []
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.8.6
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: ActiveModel based object persisting library for Redis key-value database.
|
94
|
+
test_files:
|
95
|
+
- spec/active_redis_spec.rb
|
96
|
+
- spec/spec_helper.rb
|
97
|
+
has_rdoc:
|