ohm 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ohm.rb CHANGED
@@ -1,48 +1,14 @@
1
1
  require "rubygems"
2
2
  require "redis"
3
+ require File.join(File.dirname(__FILE__), "ohm", "validations")
3
4
 
4
5
  module Ohm
5
- module Validations
6
- def valid?
7
- errors.clear
8
- validate
9
- errors.empty?
10
- end
11
-
12
- def validate
13
- end
14
-
15
- def errors
16
- @errors ||= []
17
- end
18
-
19
- private
20
-
21
- def assert_format(att, format)
22
- if assert_present(att)
23
- assert attribute(att).match(format), [att, :format]
24
- end
25
- end
26
-
27
- def assert_present(att)
28
- if assert_not_nil(att)
29
- assert attribute(att).any?, [att, :empty]
30
- end
31
- end
32
-
33
- def assert_not_nil(att)
34
- assert attribute(att), [att, :nil]
35
- end
36
-
37
- def assert(value, error)
38
- value or errors.push(error) && false
39
- end
40
-
41
- def attribute(att)
42
- instance_variable_get("@#{att}")
43
- end
6
+ def key(*args)
7
+ args.join(":")
44
8
  end
45
9
 
10
+ module_function :key
11
+
46
12
  module Attributes
47
13
  class Collection < Array
48
14
  attr_accessor :key, :db
@@ -84,18 +50,34 @@ module Ohm
84
50
  end
85
51
 
86
52
  class Model
53
+ module Validations
54
+ include Ohm::Validations
55
+
56
+ def assert_unique(attrs)
57
+ index_key = index_key_for(attrs, read_locals(attrs))
58
+ assert(db.set_count(index_key).zero? || db.set_member?(index_key, id), [attrs, :not_unique])
59
+ end
60
+ end
61
+
87
62
  include Validations
88
63
 
89
64
  ModelIsNew = Class.new(StandardError)
90
65
 
91
66
  @@attributes = Hash.new { |hash, key| hash[key] = [] }
92
67
  @@collections = Hash.new { |hash, key| hash[key] = [] }
68
+ @@indices = Hash.new { |hash, key| hash[key] = [] }
93
69
 
94
70
  attr_accessor :id
95
71
 
96
72
  def self.attribute(name)
97
- attr_writer(name)
98
- attr_value_reader(name)
73
+ define_method(name) do
74
+ read_local(name)
75
+ end
76
+
77
+ define_method(:"#{name}=") do |value|
78
+ write_local(name, value)
79
+ end
80
+
99
81
  attributes << name
100
82
  end
101
83
 
@@ -109,13 +91,8 @@ module Ohm
109
91
  collections << name
110
92
  end
111
93
 
112
- # TODO Encapsulate access to db?
113
- def self.attr_value_reader(name)
114
- class_eval <<-EOS
115
- def #{name}
116
- @#{name} ||= db[key("#{name}")]
117
- end
118
- EOS
94
+ def self.index(attrs)
95
+ indices << attrs
119
96
  end
120
97
 
121
98
  def self.attr_list_reader(name)
@@ -139,9 +116,7 @@ module Ohm
139
116
  end
140
117
 
141
118
  def self.all
142
- filter(:all).map do |id|
143
- new(:id => id)
144
- end
119
+ filter(:all)
145
120
  end
146
121
 
147
122
  def self.attributes
@@ -152,11 +127,24 @@ module Ohm
152
127
  @@collections[self]
153
128
  end
154
129
 
130
+ def self.indices
131
+ @@indices[self]
132
+ end
133
+
155
134
  def self.create(*args)
156
135
  new(*args).create
157
136
  end
158
137
 
138
+ # TODO Add a method that receives several arguments and returns a
139
+ # string with the values separated by colons.
140
+ def self.find(attribute, value)
141
+ # filter("#{attribute}:#{value}")
142
+ filter(Ohm.key(attribute, value))
143
+ end
144
+
159
145
  def initialize(attrs = {})
146
+ @_attributes = Hash.new {|hash,key| hash[key] = read_remote(key) }
147
+
160
148
  attrs.each do |key, value|
161
149
  send(:"#{key}=", value)
162
150
  end
@@ -166,15 +154,18 @@ module Ohm
166
154
  return unless valid?
167
155
  initialize_id
168
156
  create_model_membership
157
+ add_to_indices
169
158
  save!
170
159
  end
171
160
 
172
161
  def save
173
162
  return unless valid?
163
+ update_indices
174
164
  save!
175
165
  end
176
166
 
177
167
  def delete
168
+ delete_from_indices
178
169
  delete_attributes(collections)
179
170
  delete_attributes(attributes)
180
171
  delete_model_membership
@@ -189,6 +180,23 @@ module Ohm
189
180
  self.class.collections
190
181
  end
191
182
 
183
+ def indices
184
+ self.class.indices
185
+ end
186
+
187
+ def ==(other)
188
+ other.key == key
189
+ rescue ModelIsNew
190
+ false
191
+ end
192
+
193
+ protected
194
+
195
+ def key(*args)
196
+ raise ModelIsNew unless id
197
+ self.class.key(id, *args)
198
+ end
199
+
192
200
  private
193
201
 
194
202
  def self.db
@@ -196,11 +204,13 @@ module Ohm
196
204
  end
197
205
 
198
206
  def self.key(*args)
199
- args.unshift(self).join(":")
207
+ Ohm.key(*args.unshift(self))
200
208
  end
201
209
 
202
210
  def self.filter(name)
203
- db.set_members(key(name))
211
+ db.set_members(key(name)).map do |id|
212
+ new(:id => id)
213
+ end
204
214
  end
205
215
 
206
216
  def self.exists?(id)
@@ -215,11 +225,6 @@ module Ohm
215
225
  self.class.db
216
226
  end
217
227
 
218
- def key(*args)
219
- raise ModelIsNew unless id
220
- self.class.key(id, *args)
221
- end
222
-
223
228
  def delete_attributes(atts)
224
229
  atts.each do |att|
225
230
  db.delete(key(att))
@@ -235,8 +240,57 @@ module Ohm
235
240
  end
236
241
 
237
242
  def save!
238
- attributes.each { |att| db[key(att)] = send(att) }
243
+ attributes.each { |att| write_remote(att, send(att)) }
239
244
  self
240
245
  end
246
+
247
+ def update_indices
248
+ delete_from_indices
249
+ add_to_indices
250
+ end
251
+
252
+ def add_to_indices
253
+ indices.each do |attrs|
254
+ db.set_add(index_key_for(attrs, read_locals(attrs)), id)
255
+ end
256
+ end
257
+
258
+ def delete_from_indices
259
+ indices.each do |attrs|
260
+ db.set_delete(index_key_for(attrs, read_remotes(attrs)), id)
261
+ end
262
+ end
263
+
264
+ def read_local(att)
265
+ @_attributes[att]
266
+ end
267
+
268
+ def write_local(att, value)
269
+ @_attributes[att] = value
270
+ end
271
+
272
+ def read_remote(att)
273
+ id && db[key(att)]
274
+ end
275
+
276
+ def write_remote(att, value)
277
+ db[key(att)] = value
278
+ end
279
+
280
+ def read_locals(attrs)
281
+ attrs.map do |att|
282
+ read_local(att)
283
+ end
284
+ end
285
+
286
+ def read_remotes(attrs)
287
+ attrs.map do |att|
288
+ read_remote(att)
289
+ end
290
+ end
291
+
292
+ def index_key_for(attrs, values)
293
+ self.class.key *(attrs + values)
294
+ end
241
295
  end
242
296
  end
@@ -0,0 +1,38 @@
1
+ module Ohm
2
+ module Validations
3
+ def valid?
4
+ errors.clear
5
+ validate
6
+ errors.empty?
7
+ end
8
+
9
+ def validate
10
+ end
11
+
12
+ def errors
13
+ @errors ||= []
14
+ end
15
+
16
+ protected
17
+
18
+ def assert_format(att, format)
19
+ if assert_present(att)
20
+ assert send(att).match(format), [att, :format]
21
+ end
22
+ end
23
+
24
+ def assert_present(att)
25
+ if assert_not_nil(att)
26
+ assert send(att).any?, [att, :empty]
27
+ end
28
+ end
29
+
30
+ def assert_not_nil(att)
31
+ assert send(att), [att, :nil]
32
+ end
33
+
34
+ def assert(value, error)
35
+ value or errors.push(error) && false
36
+ end
37
+ end
38
+ end
@@ -29,4 +29,24 @@ benchmark "ruby array push" do
29
29
  array.push(1)
30
30
  end
31
31
 
32
- run 10000
32
+ $redis.set_add("bar", 1)
33
+ $redis.set_add("bar", 2)
34
+
35
+ benchmark "retrieve a set of two members" do
36
+ $redis.set_members("bar")
37
+ end
38
+
39
+ benchmark "retrieve membership status and set count" do
40
+ $redis.set_count("bar")
41
+ $redis.set_member?("bar", "1")
42
+ end
43
+
44
+ benchmark "retrieve set count" do
45
+ $redis.set_count("bar").zero?
46
+ end
47
+
48
+ benchmark "retrieve membership status" do
49
+ $redis.set_member?("bar", "1")
50
+ end
51
+
52
+ run 20_000
Binary file
@@ -1 +1 @@
1
- 6416
1
+ 26947
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class IndicesTest < Test::Unit::TestCase
4
+ class User < Ohm::Model
5
+ attribute :email
6
+
7
+ index [:email]
8
+ end
9
+
10
+ context "A model with an indexed attribute" do
11
+ setup do
12
+ $redis.flush_db
13
+
14
+ @user1 = User.create(:email => "foo")
15
+ @user2 = User.create(:email => "bar")
16
+ end
17
+
18
+ should "be able to find by the given attribute" do
19
+ assert_equal [@user1], User.find(:email, "foo").to_a
20
+ end
21
+
22
+ should "update indices when changing attribute values" do
23
+ @user1.email = "baz"
24
+ @user1.save
25
+
26
+ assert_equal [], User.find(:email, "foo").to_a
27
+ assert_equal [@user1], User.find(:email, "baz").to_a
28
+ end
29
+
30
+ should "remove from the index after deleting" do
31
+ @user2.delete
32
+
33
+ assert_equal [], User.find(:email, "bar").to_a
34
+ end
35
+ end
36
+ end
@@ -241,4 +241,21 @@ class TestRedis < Test::Unit::TestCase
241
241
  assert_equal ["1", "2", "3"], Post[@post.id].comments
242
242
  end
243
243
  end
244
+
245
+ context "Comparison" do
246
+ setup do
247
+ @user = User.create(:email => "foo")
248
+ end
249
+
250
+ should "be comparable to other instances" do
251
+ assert_equal @user, User[@user.id]
252
+
253
+ assert_not_equal @user, User.create
254
+ assert_not_equal User.new, User.new
255
+ end
256
+
257
+ should "not be comparable to instances of other models" do
258
+ assert_not_equal @user, Event.create(:name => "Ruby Tuesday")
259
+ end
260
+ end
244
261
  end
@@ -3,6 +3,10 @@ require File.dirname(__FILE__) + '/test_helper'
3
3
  class ValidationsTest < Test::Unit::TestCase
4
4
  class Event < Ohm::Model
5
5
  attribute :name
6
+ attribute :place
7
+
8
+ index [:name]
9
+ index [:name, :place]
6
10
 
7
11
  def validate
8
12
  assert_format(:name, /^\w+$/)
@@ -40,6 +44,42 @@ class ValidationsTest < Test::Unit::TestCase
40
44
  end
41
45
  end
42
46
  end
47
+
48
+ context "That must have a unique name" do
49
+ should "fail when the value already exists" do
50
+ def @event.validate
51
+ assert_unique [:name]
52
+ end
53
+
54
+ Event.create(:name => "foo")
55
+ @event.name = "foo"
56
+ @event.create
57
+
58
+ assert_nil @event.id
59
+ assert_equal [[[:name], :not_unique]], @event.errors
60
+ end
61
+ end
62
+
63
+ context "That must have a unique name scoped by place" do
64
+ should "fail when the value already exists" do
65
+ def @event.validate
66
+ assert_unique [:name, :place]
67
+ end
68
+
69
+ Event.create(:name => "foo", :place => "bar")
70
+ @event.name = "foo"
71
+ @event.place = "bar"
72
+ @event.create
73
+
74
+ assert_nil @event.id
75
+ assert_equal [[[:name, :place], :not_unique]], @event.errors
76
+
77
+ @event.place = "baz"
78
+ @event.create
79
+
80
+ assert @event.valid?
81
+ end
82
+ end
43
83
  end
44
84
 
45
85
  context "An existing model with a valid name" do
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.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michel Martens, Damian Janowski
@@ -31,6 +31,7 @@ extensions: []
31
31
  extra_rdoc_files: []
32
32
 
33
33
  files:
34
+ - lib/ohm/validations.rb
34
35
  - lib/ohm.rb
35
36
  - README.markdown
36
37
  - LICENSE
@@ -39,15 +40,13 @@ files:
39
40
  - test/benchmarks.rb
40
41
  - test/db/dump.rdb
41
42
  - test/db/redis.pid
43
+ - test/indices_test.rb
42
44
  - test/model_test.rb
43
- - test/references_test.rb
44
45
  - test/test.conf
45
46
  - test/test_helper.rb
46
47
  - test/validations_test.rb
47
48
  has_rdoc: false
48
49
  homepage: http://github.com/soveran/ohm
49
- licenses: []
50
-
51
50
  post_install_message:
52
51
  rdoc_options: []
53
52
 
@@ -68,7 +67,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
67
  requirements: []
69
68
 
70
69
  rubyforge_project:
71
- rubygems_version: 1.3.2
70
+ rubygems_version: 1.3.1
72
71
  signing_key:
73
72
  specification_version: 2
74
73
  summary: Object-hash mapping library for Redis.
@@ -1,29 +0,0 @@
1
- require File.dirname(__FILE__) + '/test_helper'
2
-
3
- module Ohm
4
- class Reference
5
- attr :model, :id
6
-
7
- def initialize(model, id)
8
- end
9
-
10
- def self.marshal_load(*args)
11
- 1
12
- end
13
- end
14
- end
15
-
16
- class ReferencesTest < Test::Unit::TestCase
17
- class Referee < Ohm::Model
18
- end
19
-
20
- test "dump" do
21
- model = Referee.new.create
22
-
23
- ref = Ohm::Reference.new(Referee, 1)
24
-
25
- $redis["foo"] = ref
26
-
27
- #assert_equal Referee[1], $redis["foo"]
28
- end
29
- end