redismodel 0.1.0
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/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
|
+
|