ohm 0.0.7 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -1,5 +1,5 @@
1
- Ohm
2
- ===
1
+ Ohm
2
+ =====
3
3
 
4
4
  Object-hash mapping library for Redis.
5
5
 
@@ -23,6 +23,8 @@ Usage
23
23
  set :participants
24
24
  list :comments
25
25
 
26
+ index :name
27
+
26
28
  def validate
27
29
  assert_present :name
28
30
  end
@@ -31,11 +33,11 @@ Usage
31
33
  event = Event.create(:name => "Ruby Tuesday")
32
34
  event.participants << "Michel Martens"
33
35
  event.participants << "Damian Janowski"
34
- event.participants #=> ["Damian Janowski", "Michel Martens"]
36
+ event.participants.all #=> ["Damian Janowski", "Michel Martens"]
35
37
 
36
38
  event.comments << "Very interesting event!"
37
39
  event.comments << "Agree"
38
- event.comments #=> ["Very interesting event!", "Agree"]
40
+ event.comments.all #=> ["Very interesting event!", "Agree"]
39
41
 
40
42
  another_event = Event.new
41
43
  another_event.valid? #=> false
data/lib/ohm.rb CHANGED
@@ -1,19 +1,33 @@
1
+ require "base64"
1
2
  require File.join(File.dirname(__FILE__), "ohm", "redis")
2
3
  require File.join(File.dirname(__FILE__), "ohm", "validations")
3
4
 
4
5
  module Ohm
6
+
7
+ # Provides access to the redis database. This is shared accross all models and instances.
5
8
  def redis
6
9
  @redis
7
10
  end
8
11
 
9
- def connect(*attrs)
10
- @redis = Ohm::Redis.new(*attrs)
12
+ # Connect to a redis database.
13
+ #
14
+ # @param options [Hash] options to create a message with.
15
+ # @option options [#to_s] :host ('127.0.0.1') Host of the redis database.
16
+ # @option options [#to_s] :port (6379) Port number.
17
+ # @option options [#to_s] :db (0) Database number.
18
+ # @option options [#to_s] :timeout (0) Database timeout in seconds.
19
+ # @example Connect to a database in port 6380.
20
+ # Ohm.connect(:port => 6380)
21
+ def connect(*options)
22
+ @redis = Ohm::Redis.new(*options)
11
23
  end
12
24
 
25
+ # Clear the database.
13
26
  def flush
14
27
  @redis.flushdb
15
28
  end
16
29
 
30
+ # Join the parameters with ":" to create a key.
17
31
  def key(*args)
18
32
  args.join(":")
19
33
  end
@@ -21,42 +35,93 @@ module Ohm
21
35
  module_function :key, :connect, :flush, :redis
22
36
 
23
37
  module Attributes
24
- class Collection < Array
38
+ class Collection
39
+ include Enumerable
40
+
25
41
  attr_accessor :key, :db
26
42
 
27
43
  def initialize(db, key)
28
44
  self.db = db
29
45
  self.key = key
30
- super(retrieve)
46
+ end
47
+
48
+ def each(&block)
49
+ all.each(&block)
50
+ end
51
+
52
+ def all(model = nil)
53
+ model ? raw.collect { |id| model[id] } : raw
31
54
  end
32
55
  end
33
56
 
57
+ # Represents a Redis list.
58
+ #
59
+ # @example Use a list attribute.
60
+ #
61
+ # class Event < Ohm::Model
62
+ # attribute :name
63
+ # list :participants
64
+ # end
65
+ #
66
+ # event = Event.create :name => "Redis Meeting"
67
+ # event.participants << "Albert"
68
+ # event.participants << "Benoit"
69
+ # event.participants.all #=> ["Albert", "Benoit"]
34
70
  class List < Collection
35
- def retrieve
36
- db.list(key)
37
- end
38
71
 
72
+ # @param value [#to_s] Pushes value to the list.
39
73
  def << value
40
- super(value) if db.rpush(key, value)
74
+ db.rpush(key, value)
75
+ end
76
+
77
+ private
78
+
79
+ def raw
80
+ db.list(key)
41
81
  end
42
82
  end
43
83
 
84
+ # Represents a Redis set.
85
+ #
86
+ # @example Use a set attribute.
87
+ #
88
+ # class Company < Ohm::Model
89
+ # attribute :name
90
+ # set :employees
91
+ # end
92
+ #
93
+ # company = Company.create :name => "Redis Co."
94
+ # company.employees << "Albert"
95
+ # company.employees << "Benoit"
96
+ # company.employees.all #=> ["Albert", "Benoit"]
97
+ # company.include?("Albert") #=> true
44
98
  class Set < Collection
45
- def retrieve
46
- db.smembers(key).sort
47
- end
48
99
 
100
+ # @param value [#to_s] Adds value to the list.
49
101
  def << value
50
- super(value) if db.sadd(key, value)
102
+ db.sadd(key, value)
103
+ end
104
+
105
+ # @param value [Ohm::Model#id] Adds the id of the object if it's an Ohm::Model.
106
+ def add model
107
+ raise ArgumentError unless model.kind_of?(Ohm::Model)
108
+ raise ArgumentError unless model.id
109
+ self << model.id
51
110
  end
52
111
 
53
112
  def delete(value)
54
- super(value) if db.srem(key, value)
113
+ db.srem(key, value)
55
114
  end
56
115
 
57
116
  def include?(value)
58
117
  db.sismember(key, value)
59
118
  end
119
+
120
+ private
121
+
122
+ def raw
123
+ db.smembers(key).sort
124
+ end
60
125
  end
61
126
  end
62
127
 
@@ -64,6 +129,13 @@ module Ohm
64
129
  module Validations
65
130
  include Ohm::Validations
66
131
 
132
+ # Validates that the attribute or array of attributes are unique. For this,
133
+ # an index of the same kind must exist.
134
+ #
135
+ # @overload assert_unique :name
136
+ # Validates that the name attribute is unique.
137
+ # @overload assert_unique [:street, :city]
138
+ # Validates that the :street and :city pair is unique.
67
139
  def assert_unique(attrs)
68
140
  index_key = index_key_for(Array(attrs), read_locals(Array(attrs)))
69
141
  assert(db.scard(index_key).zero? || db.sismember(index_key, id), [Array(attrs), :not_unique])
@@ -76,10 +148,15 @@ module Ohm
76
148
 
77
149
  @@attributes = Hash.new { |hash, key| hash[key] = [] }
78
150
  @@collections = Hash.new { |hash, key| hash[key] = [] }
151
+ @@counters = Hash.new { |hash, key| hash[key] = [] }
79
152
  @@indices = Hash.new { |hash, key| hash[key] = [] }
80
153
 
81
154
  attr_accessor :id
82
155
 
156
+ # Defines a string attribute for the model. This attribute will be persisted by Redis
157
+ # as a string. Any value stored here will be retrieved in its string representation.
158
+ #
159
+ # @param name [Symbol] Name of the attribute.
83
160
  def self.attribute(name)
84
161
  define_method(name) do
85
162
  read_local(name)
@@ -92,16 +169,58 @@ module Ohm
92
169
  attributes << name
93
170
  end
94
171
 
172
+ # Defines a counter attribute for the model. This attribute can't be assigned, only incremented
173
+ # or decremented. It will be zero by default.
174
+ #
175
+ # @param name [Symbol] Name of the counter.
176
+ def self.counter(name)
177
+ define_method(name) do
178
+ read_local(name).to_i
179
+ end
180
+
181
+ counters << name
182
+ end
183
+
184
+ # Defines a list attribute for the model. It can be accessed only after the model instance
185
+ # is created.
186
+ #
187
+ # @param name [Symbol] Name of the list.
95
188
  def self.list(name)
96
189
  attr_list_reader(name)
97
190
  collections << name
98
191
  end
99
192
 
193
+ # Defines a set attribute for the model. It can be accessed only after the model instance
194
+ # is created. Sets are recommended when insertion and retrival order is irrelevant, and
195
+ # operations like union, join, and membership checks are important.
196
+ #
197
+ # @param name [Symbol] Name of the set.
100
198
  def self.set(name)
101
199
  attr_set_reader(name)
102
200
  collections << name
103
201
  end
104
202
 
203
+ # Creates an index (a set) that will be used for finding instances.
204
+ #
205
+ # If you want to find a model instance by some attribute value, then an index for that
206
+ # attribute must exist.
207
+ #
208
+ # Each index declaration creates an index. It can be either an index on one particular attribute,
209
+ # or an index accross many attributes.
210
+ #
211
+ # @example
212
+ # class User < Ohm::Model
213
+ # attribute :email
214
+ # index :email
215
+ # end
216
+ #
217
+ # # Now this is possible:
218
+ # User.find :email, "ohm@example.com"
219
+ #
220
+ # @overload index :name
221
+ # Creates an index for the name attribute.
222
+ # @overload index [:street, :city]
223
+ # Creates a composite index for street and city.
105
224
  def self.index(attrs)
106
225
  indices << Array(attrs)
107
226
  end
@@ -134,6 +253,10 @@ module Ohm
134
253
  @@attributes[self]
135
254
  end
136
255
 
256
+ def self.counters
257
+ @@counters[self]
258
+ end
259
+
137
260
  def self.collections
138
261
  @@collections[self]
139
262
  end
@@ -143,11 +266,23 @@ module Ohm
143
266
  end
144
267
 
145
268
  def self.create(*args)
146
- new(*args).create
269
+ model = new(*args)
270
+ model.create
271
+ model
147
272
  end
148
273
 
149
274
  def self.find(attribute, value)
150
- filter(Ohm.key(attribute, value))
275
+ filter(Ohm.key(attribute, encode(value)))
276
+ end
277
+
278
+ def self.encode(value)
279
+ Base64.encode64(value.to_s).chomp
280
+ end
281
+
282
+ def self.encode_each(values)
283
+ values.collect do |value|
284
+ encode(value)
285
+ end
151
286
  end
152
287
 
153
288
  def initialize(attrs = {})
@@ -179,16 +314,37 @@ module Ohm
179
314
 
180
315
  def delete
181
316
  delete_from_indices
182
- delete_attributes(collections)
183
317
  delete_attributes(attributes)
318
+ delete_attributes(counters)
319
+ delete_attributes(collections)
184
320
  delete_model_membership
185
321
  self
186
322
  end
187
323
 
324
+ # Increment the attribute denoted by :att.
325
+ #
326
+ # @param att [Symbol] Attribute to increment.
327
+ def incr(att)
328
+ raise ArgumentError unless counters.include?(att)
329
+ write_local(att, db.incr(key(att)))
330
+ end
331
+
332
+ # Decrement the attribute denoted by :att.
333
+ #
334
+ # @param att [Symbol] Attribute to decrement.
335
+ def decr(att)
336
+ raise ArgumentError unless counters.include?(att)
337
+ write_local(att, db.decr(key(att)))
338
+ end
339
+
188
340
  def attributes
189
341
  self.class.attributes
190
342
  end
191
343
 
344
+ def counters
345
+ self.class.counters
346
+ end
347
+
192
348
  def collections
193
349
  self.class.collections
194
350
  end
@@ -303,7 +459,7 @@ module Ohm
303
459
  end
304
460
 
305
461
  def index_key_for(attrs, values)
306
- self.class.key *(attrs + values)
462
+ self.class.key *(attrs + self.class.encode_each(values))
307
463
  end
308
464
  end
309
465
  end
data/lib/ohm/redis.rb CHANGED
@@ -157,9 +157,12 @@ module Ohm
157
157
  def call_command(argv)
158
158
  connect unless connected?
159
159
  raw_call_command(argv.dup)
160
- rescue Errno::ECONNRESET
161
- reconnect
162
- raw_call_command(argv.dup)
160
+ rescue Errno::ECONNRESET, Errno::EPIPE
161
+ if reconnect
162
+ raw_call_command(argv.dup)
163
+ else
164
+ raise Errno::ECONNRESET
165
+ end
163
166
  end
164
167
 
165
168
  def raw_call_command(argv)
@@ -72,20 +72,20 @@ module Ohm
72
72
 
73
73
  protected
74
74
 
75
- def assert_format(att, format)
76
- if assert_present(att)
77
- assert send(att).match(format), [att, :format]
75
+ def assert_format(att, format, error = [att, :format])
76
+ if assert_present(att, error)
77
+ assert(send(att).to_s.match(format), error)
78
78
  end
79
79
  end
80
80
 
81
- def assert_present(att)
82
- if assert_not_nil(att)
83
- assert !send(att).empty?, [att, :empty]
84
- end
81
+ def assert_present(att, error = [att, :not_present])
82
+ assert(!send(att).to_s.empty?, error)
85
83
  end
86
84
 
87
- def assert_not_nil(att)
88
- assert send(att), [att, :nil]
85
+ def assert_numeric(att, error = [att, :not_numeric])
86
+ if assert_present(att, error)
87
+ assert_format(att, /^\d+$/, error)
88
+ end
89
89
  end
90
90
 
91
91
  def assert(value, error)
data/test/db/redis.pid CHANGED
@@ -1 +1 @@
1
- 36980
1
+ 65913
data/test/errors_test.rb CHANGED
@@ -31,11 +31,11 @@ class ErrorsTest < Test::Unit::TestCase
31
31
  values = []
32
32
 
33
33
  @model.errors.present do |e|
34
- e.on [:name, :nil] do
34
+ e.on [:name, :not_present] do
35
35
  values << 1
36
36
  end
37
37
 
38
- e.on [:account, :empty] do
38
+ e.on [:account, :not_present] do
39
39
  values << 2
40
40
  end
41
41
 
@@ -65,8 +65,8 @@ class ErrorsTest < Test::Unit::TestCase
65
65
 
66
66
  should "accept multiple matches for an error" do
67
67
  values = @model.errors.present do |e|
68
- e.on [:name, :nil], "A"
69
- e.on [:account, :empty] do
68
+ e.on [:name, :not_present], "A"
69
+ e.on [:account, :not_present] do
70
70
  "B"
71
71
  end
72
72
  e.on :terrible_error, "C"
@@ -85,8 +85,8 @@ class ErrorsTest < Test::Unit::TestCase
85
85
 
86
86
  should "take a custom presenter" do
87
87
  values = @model.errors.present(MyPresenter) do |e|
88
- e.on([:name, :nil]) { "A" }
89
- e.on([:account, :empty]) { "B" }
88
+ e.on([:name, :not_present]) { "A" }
89
+ e.on([:account, :not_present]) { "B" }
90
90
  e.on(:terrible_error) { "C" }
91
91
  end
92
92
 
data/test/indices_test.rb CHANGED
@@ -9,10 +9,11 @@ class IndicesTest < Test::Unit::TestCase
9
9
 
10
10
  context "A model with an indexed attribute" do
11
11
  setup do
12
- Ohm.redis.flushdb
12
+ Ohm.flush
13
13
 
14
14
  @user1 = User.create(:email => "foo")
15
15
  @user2 = User.create(:email => "bar")
16
+ @user3 = User.create(:email => "baz qux")
16
17
  end
17
18
 
18
19
  should "be able to find by the given attribute" do
@@ -32,5 +33,9 @@ class IndicesTest < Test::Unit::TestCase
32
33
 
33
34
  assert_equal [], User.find(:email, "bar")
34
35
  end
36
+
37
+ should "work with attributes that contain spaces" do
38
+ assert_equal [@user3], User.find(:email, "baz qux")
39
+ end
35
40
  end
36
41
  end
data/test/model_test.rb CHANGED
@@ -2,6 +2,7 @@ require File.join(File.dirname(__FILE__), "test_helper")
2
2
 
3
3
  class Event < Ohm::Model
4
4
  attribute :name
5
+ counter :votes
5
6
  set :attendees
6
7
  end
7
8
 
@@ -14,6 +15,14 @@ class Post < Ohm::Model
14
15
  list :comments
15
16
  end
16
17
 
18
+ class Person < Ohm::Model
19
+ attribute :name
20
+
21
+ def validate
22
+ assert_present :name
23
+ end
24
+ end
25
+
17
26
  class TestRedis < Test::Unit::TestCase
18
27
  context "An event initialized with a hash of attributes" do
19
28
  should "assign the passed attributes" do
@@ -28,6 +37,10 @@ class TestRedis < Test::Unit::TestCase
28
37
  event2 = Event.create(:name => "Ruby Meetup")
29
38
  assert_equal event1.id + 1, event2.id
30
39
  end
40
+
41
+ should "return the unsaved object if validation fails" do
42
+ assert Person.create(:name => nil).kind_of?(Person)
43
+ end
31
44
  end
32
45
 
33
46
  context "Finding an event" do
@@ -189,19 +202,14 @@ class TestRedis < Test::Unit::TestCase
189
202
  end
190
203
  end
191
204
 
192
- should "return an array if the model exists" do
193
- @event.create
194
- assert @event.attendees.kind_of?(Array)
195
- end
196
-
197
205
  should "remove an element if sent :delete" do
198
206
  @event.create
199
207
  @event.attendees << "1"
200
208
  @event.attendees << "2"
201
209
  @event.attendees << "3"
202
- assert_equal ["1", "2", "3"], @event.attendees
210
+ assert_equal ["1", "2", "3"], @event.attendees.all
203
211
  @event.attendees.delete("2")
204
- assert_equal ["1", "3"], Event[@event.id].attendees
212
+ assert_equal ["1", "3"], Event[@event.id].attendees.all
205
213
  end
206
214
 
207
215
  should "return true if the set includes some member" do
@@ -212,6 +220,22 @@ class TestRedis < Test::Unit::TestCase
212
220
  assert @event.attendees.include?("2")
213
221
  assert_equal false, @event.attendees.include?("4")
214
222
  end
223
+
224
+ should "return instances of the passed model if the call to all includes a class" do
225
+ @event.create
226
+ @person = Person.create :name => "albert"
227
+ @event.attendees << @person.id
228
+
229
+ assert_equal [@person], @event.attendees.all(Person)
230
+ end
231
+
232
+ should "insert the model instance id instead of the object if using add" do
233
+ @event.create
234
+ @person = Person.create :name => "albert"
235
+ @event.attendees.add(@person)
236
+
237
+ assert_equal [@person.id.to_s], @event.attendees.all
238
+ end
215
239
  end
216
240
 
217
241
  context "Attributes of type List" do
@@ -222,14 +246,14 @@ class TestRedis < Test::Unit::TestCase
222
246
  end
223
247
 
224
248
  should "return an array" do
225
- assert @post.comments.kind_of?(Array)
249
+ assert @post.comments.all.kind_of?(Array)
226
250
  end
227
251
 
228
252
  should "keep the inserting order" do
229
253
  @post.comments << "1"
230
254
  @post.comments << "2"
231
255
  @post.comments << "3"
232
- assert_equal ["1", "2", "3"], @post.comments
256
+ assert_equal ["1", "2", "3"], @post.comments.all
233
257
  end
234
258
 
235
259
  should "keep the inserting order after saving" do
@@ -237,7 +261,45 @@ class TestRedis < Test::Unit::TestCase
237
261
  @post.comments << "2"
238
262
  @post.comments << "3"
239
263
  @post.save
240
- assert_equal ["1", "2", "3"], Post[@post.id].comments
264
+ assert_equal ["1", "2", "3"], Post[@post.id].comments.all
265
+ end
266
+
267
+ should "respond to each" do
268
+ @post.comments << "1"
269
+ @post.comments << "2"
270
+ @post.comments << "3"
271
+
272
+ i = 1
273
+ @post.comments.each do |c|
274
+ assert_equal i, c.to_i
275
+ i += 1
276
+ end
277
+ end
278
+ end
279
+
280
+ context "Counters" do
281
+ setup do
282
+ @event = Event.create(:name => "Ruby Tuesday")
283
+ end
284
+
285
+ should "raise ArgumentError if the attribute is not a counter" do
286
+ assert_raise ArgumentError do
287
+ @event.incr(:name)
288
+ end
289
+ end
290
+
291
+ should "be zero if not initialized" do
292
+ assert_equal 0, @event.votes
293
+ end
294
+
295
+ should "be able to increment a counter" do
296
+ @event.incr(:votes)
297
+ assert_equal 1, @event.votes
298
+ end
299
+
300
+ should "be able to decrement a counter" do
301
+ @event.decr(:votes)
302
+ assert_equal -1, @event.votes
241
303
  end
242
304
  end
243
305
 
@@ -4,6 +4,7 @@ class ValidationsTest < Test::Unit::TestCase
4
4
  class Event < Ohm::Model
5
5
  attribute :name
6
6
  attribute :place
7
+ attribute :capacity
7
8
 
8
9
  index :name
9
10
  index [:name, :place]
@@ -45,6 +46,48 @@ class ValidationsTest < Test::Unit::TestCase
45
46
  end
46
47
  end
47
48
 
49
+ context "That must have a numeric attribute :capacity" do
50
+ should "fail when the value is nil" do
51
+ def @event.validate
52
+ assert_numeric :capacity
53
+ end
54
+
55
+ @event.name = "foo"
56
+ @event.place = "bar"
57
+ @event.create
58
+
59
+ assert_nil @event.id
60
+ assert_equal [[:capacity, :not_numeric]], @event.errors
61
+ end
62
+
63
+ should "fail when the value is not numeric" do
64
+ def @event.validate
65
+ assert_numeric :capacity
66
+ end
67
+
68
+ @event.name = "foo"
69
+ @event.place = "bar"
70
+ @event.capacity = "baz"
71
+ @event.create
72
+
73
+ assert_nil @event.id
74
+ assert_equal [[:capacity, :not_numeric]], @event.errors
75
+ end
76
+
77
+ should "succeed when the value is numeric" do
78
+ def @event.validate
79
+ assert_numeric :capacity
80
+ end
81
+
82
+ @event.name = "foo"
83
+ @event.place = "bar"
84
+ @event.capacity = 42
85
+ @event.create
86
+
87
+ assert_not_nil @event.id
88
+ end
89
+ end
90
+
48
91
  context "That must have a unique name" do
49
92
  should "fail when the value already exists" do
50
93
  def @event.validate
@@ -160,32 +203,14 @@ class ValidationsTest < Test::Unit::TestCase
160
203
  should "fail when the attribute is nil" do
161
204
  @target.validate
162
205
 
163
- assert_equal [[:name, :nil]], @target.errors
206
+ assert_equal [[:name, :not_present]], @target.errors
164
207
  end
165
208
 
166
209
  should "fail when the attribute is empty" do
167
210
  @target.name = ""
168
211
  @target.validate
169
212
 
170
- assert_equal [[:name, :empty]], @target.errors
171
- end
172
- end
173
-
174
- context "assert_not_nil" do
175
- should "fail when the attribute is nil" do
176
- def @target.validate
177
- assert_not_nil(:name)
178
- end
179
-
180
- @target.validate
181
-
182
- assert_equal [[:name, :nil]], @target.errors
183
-
184
- @target.errors.clear
185
- @target.name = ""
186
- @target.validate
187
-
188
- assert_equal [], @target.errors
213
+ assert_equal [[:name, :not_present]], @target.errors
189
214
  end
190
215
  end
191
216
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ohm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michel Martens
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2009-06-27 00:00:00 -03:00
13
+ date: 2009-07-22 00:00:00 -03:00
14
14
  default_executable:
15
15
  dependencies: []
16
16