ohm 0.0.7 → 0.0.9

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