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 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
+