redismodel 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/redismodel.rb +319 -0
- data/test/benchmarks.rb +43 -0
- data/test/class_mathod_tests.rb +45 -0
- data/test/models/person.rb +12 -0
- data/test/property_type_tests.rb +65 -0
- data/test/test_helper.rb +5 -0
- metadata +80 -0
data/lib/redismodel.rb
ADDED
@@ -0,0 +1,319 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'md5'
|
3
|
+
require 'date'
|
4
|
+
require 'redis'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
class DateTime
|
8
|
+
def inspect
|
9
|
+
self.to_s
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class RedisModel
|
14
|
+
VERSION = '0.1.3'
|
15
|
+
DEFAULT_CONFIG = {
|
16
|
+
:atomic => true,
|
17
|
+
:default_sort => "created_at"
|
18
|
+
}
|
19
|
+
|
20
|
+
module ArrayProxy
|
21
|
+
def <<(other)
|
22
|
+
tmp = self.dup.push(other)
|
23
|
+
@__model__.send(:set, @__property__, tmp)
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def push(other)
|
28
|
+
self << other
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module HashProxy
|
33
|
+
def []=(a,b)
|
34
|
+
tmp = self.dup
|
35
|
+
tmp[a] = b
|
36
|
+
@__model__.send(:set, @__property__, tmp)
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class InvalidRecord < RuntimeError; end
|
42
|
+
|
43
|
+
def self.connection(klass)
|
44
|
+
hash = {}
|
45
|
+
[:host, :port].each{|v| hash[v] = klass.config[v] unless klass.config[v].nil?}
|
46
|
+
@connections ||= {}
|
47
|
+
@connections[klass.to_s] ||= Redis.new(hash)
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Casts a object type as a compatible type for redis, or reverses the process.
|
52
|
+
#
|
53
|
+
# Examples:
|
54
|
+
# #cast( Integer, "1" ) => 1
|
55
|
+
# #cast( Array, [1,2,3] ) => "[1,2,3]"
|
56
|
+
# #cast( Hash, {:a => 1, 'b' => "2", :c => ['c']} ) => "--- \nb: \"2\"\n:c: \n- c\n:a: 1\n"
|
57
|
+
# #cast( Hash, "--- \nb: \"2\"\n:c: \n- c\n:a: 1\n" ) => {"b"=>"2", :c=>["c"], :a=>1}
|
58
|
+
#
|
59
|
+
def self.cast(klass, value)
|
60
|
+
klass = klass.to_s
|
61
|
+
v_class = value.class.to_s
|
62
|
+
|
63
|
+
if klass == "Integer" && v_class == "String"
|
64
|
+
value.to_i
|
65
|
+
elsif (klass == "Array" || klass == "Hash") && v_class == "String"
|
66
|
+
YAML.load(value)
|
67
|
+
elsif (klass == "Array" || klass == "Hash") && (klass == v_class)
|
68
|
+
value.to_yaml
|
69
|
+
elsif klass == "DateTime" && v_class == "String"
|
70
|
+
DateTime.parse(value)
|
71
|
+
elsif klass == "DateTime" && v_class == "DateTime"
|
72
|
+
value.to_s
|
73
|
+
else
|
74
|
+
value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
class Model
|
80
|
+
# Local cache of the data
|
81
|
+
attr_accessor :_data
|
82
|
+
|
83
|
+
def redis
|
84
|
+
self.class.redis
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def generate_uniq_token
|
89
|
+
"#{MD5.hexdigest("#{self.class.to_s.downcase}-#{self.object_id}-#{rand**rand}-#{Time.now.to_f}")}"
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
# Creates a new object. If `{:id => "..."}` is passed, it'll attempt to recover the record
|
94
|
+
# from the redis DB. If it can't find such a record, it'll raise RedisModel::RecordNotFound
|
95
|
+
def initialize(hash={})
|
96
|
+
@_data = {}
|
97
|
+
if hash[:id].nil?
|
98
|
+
|
99
|
+
self.send("id=", generate_uniq_token)
|
100
|
+
unless self.class.properties[:created_at].nil?
|
101
|
+
set :created_at, DateTime.now
|
102
|
+
end
|
103
|
+
hash.to_a.each do |property|
|
104
|
+
begin
|
105
|
+
self.send("#{property[0]}=", property[1])
|
106
|
+
rescue
|
107
|
+
end
|
108
|
+
end
|
109
|
+
redis.set "#{self.class}:#{id}:id", id
|
110
|
+
redis.incr "#{self.class}:_meta:count"
|
111
|
+
|
112
|
+
else
|
113
|
+
|
114
|
+
self.send("id=", hash[:id])
|
115
|
+
if get(:id).nil?
|
116
|
+
raise RedisModel::InvalidRecord
|
117
|
+
else
|
118
|
+
self.reload
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
# Custom model inspect in the format of:
|
126
|
+
# #<MyModel id: "8a80300a9b3251ec4d930f8ab5e381e2", created_at: "2010-08-01T01:32:11+01:00">
|
127
|
+
def inspect
|
128
|
+
"#<#{self.class} #{self.class.properties.to_a.map{|c| "#{c[0].to_s}: #{get(c[0]).inspect}"}.join(", ")}>"
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
# Updates all keys from redis
|
133
|
+
def reload
|
134
|
+
self.class.properties.each do |property|
|
135
|
+
self.get(property)
|
136
|
+
end
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
# Permamently deletes all traces of the record, and
|
142
|
+
# decrements the model count
|
143
|
+
def destroy
|
144
|
+
redis.keys("#{self.class}:#{id}:*").each do |key|
|
145
|
+
redis.del key
|
146
|
+
end
|
147
|
+
redis.decr "#{self.class}:_meta:count"
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
def id
|
152
|
+
@_data['id']
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
# Setter and getter for the config. Example:
|
157
|
+
#
|
158
|
+
# class MyModel < RedisModel::Model
|
159
|
+
# config :host => "1.2.3.4",
|
160
|
+
# :port => 5678
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
def self.config(hash=nil)
|
164
|
+
@_config ||= DEFAULT_CONFIG
|
165
|
+
if hash.nil?
|
166
|
+
return @_config
|
167
|
+
else
|
168
|
+
@_config = @_config.merge(hash)
|
169
|
+
return true
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
# Defines a new property. Example:
|
175
|
+
#
|
176
|
+
# class MyModel < RedisModel::Model
|
177
|
+
# property :name, String
|
178
|
+
# property :age, Integer
|
179
|
+
# end
|
180
|
+
#
|
181
|
+
def self.property(name, type)
|
182
|
+
properties[:id] = String unless properties[:id]
|
183
|
+
properties[name] = type
|
184
|
+
class_eval do
|
185
|
+
define_method("#{name}=") do |str|
|
186
|
+
set(name, str)
|
187
|
+
end
|
188
|
+
|
189
|
+
if type == Array || type == Hash
|
190
|
+
proxy = (type == Array) ? ArrayProxy : HashProxy
|
191
|
+
define_method(name) do
|
192
|
+
value = get(name)
|
193
|
+
return value if value.is_a?(proxy)
|
194
|
+
value.tap do |value|
|
195
|
+
value.extend(proxy).instance_variable_set(:@__model__, self)
|
196
|
+
value.extend(proxy).instance_variable_set(:@__property__, name)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
else
|
200
|
+
define_method(name) do
|
201
|
+
get(name)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
eval "def self.find_by_#{name.to_s}(str); self.search('#{name.to_s}', str); end"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
# Returns all records
|
211
|
+
def self.all
|
212
|
+
arr = []
|
213
|
+
redis.keys("#{klass}:*:id").each do |key|
|
214
|
+
arr << klass.find( key.split(":")[1] )
|
215
|
+
end
|
216
|
+
arr.sort_by{|m| m.send( DEFAULT_CONFIG[:default_sort] )}
|
217
|
+
end
|
218
|
+
|
219
|
+
|
220
|
+
# Finds a record from it's id
|
221
|
+
def self.find(id)
|
222
|
+
object = klass.new(:id => id)
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
# Returns all records where the value of `property` matches `str`
|
227
|
+
def self.search(property, value)
|
228
|
+
arr = []
|
229
|
+
self.all.each do |record|
|
230
|
+
if record.send(property) == value
|
231
|
+
arr << record
|
232
|
+
end
|
233
|
+
end
|
234
|
+
arr
|
235
|
+
end
|
236
|
+
|
237
|
+
|
238
|
+
# Permamently deletes all records for the model
|
239
|
+
def self.destroy_all
|
240
|
+
redis.keys("#{klass}:*").each do |key|
|
241
|
+
redis.del key
|
242
|
+
end
|
243
|
+
true
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
# Destroys all records with the ids passed
|
248
|
+
def self.destroy(*ids)
|
249
|
+
ids.each do |id|
|
250
|
+
klass.find(id).destroy
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
# Returns the number of records for the model
|
256
|
+
def self.count
|
257
|
+
redis.get("#{klass}:_meta:count").to_i
|
258
|
+
end
|
259
|
+
|
260
|
+
|
261
|
+
# Saves the Redis database to disk
|
262
|
+
def self.save!
|
263
|
+
redis.bgsave == "Background saving started"
|
264
|
+
end
|
265
|
+
|
266
|
+
|
267
|
+
protected
|
268
|
+
|
269
|
+
# Writes a property value to redis, and updates #updated_at if required.
|
270
|
+
def set(property, value)
|
271
|
+
c = self.class.type(property)
|
272
|
+
value = RedisModel.cast c, value
|
273
|
+
redis.set "#{self.class}:#{id}:#{property}", value
|
274
|
+
@_data[property] = value
|
275
|
+
|
276
|
+
unless property == :updated_at && !self.class.properties[:updated_at].nil?
|
277
|
+
set :updated_at, DateTime.now
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# Fetches the value of `property` from redis
|
282
|
+
def get(property)
|
283
|
+
c = self.class.type(property)
|
284
|
+
value = RedisModel.cast c, redis.get("#{self.class}:#{id}:#{property}")
|
285
|
+
@_data[property] = value
|
286
|
+
end
|
287
|
+
|
288
|
+
|
289
|
+
# Returns the class of the extended model
|
290
|
+
def self.klass
|
291
|
+
@klass ||= self.ancestors.first
|
292
|
+
end
|
293
|
+
|
294
|
+
|
295
|
+
# Returns the property class declared in #property above
|
296
|
+
def self.type(property)
|
297
|
+
properties[property]
|
298
|
+
end
|
299
|
+
|
300
|
+
|
301
|
+
# Keeps track of the properties declared via #property
|
302
|
+
def self.properties
|
303
|
+
@_properties ||= {}
|
304
|
+
end
|
305
|
+
|
306
|
+
|
307
|
+
|
308
|
+
# Alias for RedisModel#connection
|
309
|
+
def self.redis
|
310
|
+
RedisModel.connection(klass)
|
311
|
+
end
|
312
|
+
|
313
|
+
|
314
|
+
def id=(str)
|
315
|
+
@_data['id'] = str
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
end
|
data/test/benchmarks.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
Dir.glob("tests/models/*.rb").each{|f| require f}
|
3
|
+
|
4
|
+
Person.destroy_all
|
5
|
+
|
6
|
+
puts "Create 1 record"
|
7
|
+
puts Benchmark.measure{
|
8
|
+
1.times do
|
9
|
+
Person.new(
|
10
|
+
:name => "ashley",
|
11
|
+
:age => 18
|
12
|
+
)
|
13
|
+
end
|
14
|
+
}
|
15
|
+
|
16
|
+
puts "Create 1_000 records"
|
17
|
+
puts Benchmark.measure{
|
18
|
+
1_000.times do
|
19
|
+
Person.new(
|
20
|
+
:name => "ashley",
|
21
|
+
:age => 18
|
22
|
+
)
|
23
|
+
end
|
24
|
+
}
|
25
|
+
|
26
|
+
puts "Search #{Person.count} records"
|
27
|
+
puts Benchmark.measure{
|
28
|
+
Person.search(:age, 18)
|
29
|
+
}
|
30
|
+
|
31
|
+
|
32
|
+
puts "Fetch all #{Person.count} records"
|
33
|
+
puts Benchmark.measure{
|
34
|
+
@all = Person.all
|
35
|
+
}
|
36
|
+
|
37
|
+
|
38
|
+
puts "Destroy #{@all.size} records"
|
39
|
+
puts Benchmark.measure{
|
40
|
+
@all.each do |r|
|
41
|
+
r.destroy
|
42
|
+
end
|
43
|
+
}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class ClassMethodTests < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
Person.destroy_all
|
6
|
+
@person1 = Person.new(:name => "ashley", :age => 18)
|
7
|
+
@person2 = Person.new(:name => "ashley", :age => 18)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_find
|
11
|
+
assert Person.find(@person1.id).name == @person1.name
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_destroy_all
|
15
|
+
assert Person.count == 2
|
16
|
+
Person.destroy_all
|
17
|
+
assert Person.count == 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_destroy
|
21
|
+
assert Person.count == 2
|
22
|
+
Person.destroy(@person1.id, @person2.id)
|
23
|
+
assert Person.count == 0
|
24
|
+
assert @person1.name == nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_all
|
28
|
+
all = Person.all
|
29
|
+
assert all.size == 2
|
30
|
+
assert all[0].name == @person1.name
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_search
|
34
|
+
Person.search(:age, 19)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_properties
|
38
|
+
assert Person.properties.class == Hash
|
39
|
+
assert Person.properties.size > 0
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_save!
|
43
|
+
assert Person.save! == true
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class Person < RedisModel::Model
|
2
|
+
# config :host => "1.2.3.4",
|
3
|
+
# :port => 5678
|
4
|
+
|
5
|
+
property :name, String
|
6
|
+
property :age, Integer
|
7
|
+
property :dob, DateTime
|
8
|
+
property :favorite_games, Array
|
9
|
+
property :test_hash, Hash
|
10
|
+
property :created_at, DateTime
|
11
|
+
property :updated_at, DateTime
|
12
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class ColumnTypeTests < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@person = Person.new(
|
6
|
+
:name => "ashley",
|
7
|
+
:age => 18,
|
8
|
+
:dob => DateTime.parse("05-09-1991"),
|
9
|
+
:favorite_games => ["Portal", "WoW", 101],
|
10
|
+
:test_hash => {:a => 1, 'b' => "2", :c => ["Portal", 1, :c, [1,2,3]]}
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
# def teardown
|
15
|
+
# Person.destroy_all
|
16
|
+
# end
|
17
|
+
|
18
|
+
def test_string
|
19
|
+
assert @person.name == "ashley"
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_integer
|
23
|
+
assert @person.age == 18
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_datetime
|
27
|
+
assert @person.dob.class == DateTime
|
28
|
+
assert @person.dob.to_s == "1991-09-05T00:00:00+00:00"
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_array
|
32
|
+
assert @person.favorite_games == ["Portal", "WoW", 101]
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_add_to_array_left_left
|
36
|
+
@person.favorite_games << "Awesome"
|
37
|
+
assert @person.favorite_games.size == 4
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_add_to_array_push
|
41
|
+
@person.favorite_games.push("Awesome")
|
42
|
+
assert @person.favorite_games.size == 4
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_hash
|
46
|
+
assert @person.test_hash == {:a => 1, 'b' => "2", :c => ["Portal", 1, :c, [1,2,3]]}
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_add_key_to_hash
|
50
|
+
@person.test_hash[:add] = 123
|
51
|
+
assert @person.test_hash[:add] == 123
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_id
|
55
|
+
assert @person.id.size == 32
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_created_at
|
59
|
+
assert @person.created_at.class == DateTime
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_updated_at
|
63
|
+
assert @person.updated_at.class == DateTime
|
64
|
+
end
|
65
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: redismodel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Ashley Williams
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-08-02 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: redis
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 2
|
29
|
+
- 0
|
30
|
+
- 4
|
31
|
+
version: 2.0.4
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
description: Syncs ruby objects with Redis
|
35
|
+
email: a.j.r.williams@gmail.com
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- lib/redismodel.rb
|
44
|
+
- test/benchmarks.rb
|
45
|
+
- test/class_mathod_tests.rb
|
46
|
+
- test/models/person.rb
|
47
|
+
- test/property_type_tests.rb
|
48
|
+
- test/test_helper.rb
|
49
|
+
has_rdoc: true
|
50
|
+
homepage: http://github.com/ashleyw/RedisModel
|
51
|
+
licenses: []
|
52
|
+
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
segments:
|
63
|
+
- 0
|
64
|
+
version: "0"
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
version: "0"
|
72
|
+
requirements: []
|
73
|
+
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 1.3.6
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: Atomic object syncing with Redis
|
79
|
+
test_files: []
|
80
|
+
|