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 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
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'stringio'
2
+ require 'test/unit'
3
+ require File.dirname(__FILE__) + '/../lib/redismodel' unless defined? RedisModel
4
+ Dir.glob("test/models/*.rb").each{|f| require f}
5
+ Dir.glob("test/*_tests.rb").each{|f| require f}
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
+