ohm 0.0.18 → 0.0.19

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -135,6 +135,21 @@ with an attribute name, which will determine the sorting order. Both
135
135
  methods receive an options hash which is explained in the documentation
136
136
  for {Ohm::Attributes::Collection#sort}.
137
137
 
138
+ Adding instances of `Person` to the attendees hash is done with the
139
+ `add` method:
140
+
141
+ @event.attendees.add(Person.create(name: "Albert"))
142
+
143
+ # And now...
144
+ @event.attendees.each do |person|
145
+ # ...do what you want with this person.
146
+ end
147
+
148
+ Just to clarify: when you add items to a set with `<<`, Ohm inserts
149
+ whatever you send without checking it. When you use `add`, it assumes
150
+ it's an instance of some `Ohm::Model` and stores its id.
151
+
152
+
138
153
  Indexes
139
154
  -------
140
155
 
@@ -144,14 +159,9 @@ Ohm maintains different sets of objects ids for quick lookups.
144
159
  For example, in the example above, the index on the name attribute will
145
160
  allow for searches like Event.find(:name, "some value").
146
161
 
147
- You can also declare an index on multiple colums, like this:
148
-
149
- index [:name, :company]
150
-
151
162
  Note that the `find` method and the `assert_unique` validation need a
152
163
  corresponding index to exist.
153
164
 
154
-
155
165
  Validations
156
166
  -----------
157
167
 
@@ -183,43 +193,28 @@ to false.
183
193
  Checks that the given field is not nil or empty. The error code for this
184
194
  assertion is :not_present.
185
195
 
186
- def assert_present(att, error = [att, :not_present])
187
- assert(!send(att).to_s.empty?, error)
188
- end
196
+ assert_present :name
189
197
 
190
198
  ### assert_format
191
199
 
192
200
  Checks that the given field matches the provided format. The error code
193
201
  for this assertion is :format.
194
202
 
195
- def assert_format(att, format, error = [att, :format])
196
- if assert_present(att, error)
197
- assert(send(att).to_s.match(format), error)
198
- end
199
- end
203
+ assert_format :username, /^\w+$/
200
204
 
201
205
  ### assert_numeric
202
206
 
203
207
  Checks that the given field holds a number as a Fixnum or as a string
204
208
  representation. The error code for this assertion is :not_numeric.
205
209
 
206
- def assert_numeric(att, error = [att, :not_numeric])
207
- if assert_present(att, error)
208
- assert_format(att, /^\d+$/, error)
209
- end
210
- end
210
+ assert_numeric :votes
211
211
 
212
212
  ### assert_unique
213
213
 
214
214
  Validates that the attribute or array of attributes are unique.
215
- For this, an index of the same kind must exist. The error code is
216
- :not_unique.
217
-
218
- def assert_unique(attrs)
219
- index_key = index_key_for(Array(attrs), read_locals(Array(attrs)))
220
- assert(db.scard(index_key).zero? || db.sismember(index_key, id), [Array(attrs), :not_unique])
221
- end
215
+ For this, an index of the same kind must exist. The error code is :not_unique.
222
216
 
217
+ assert_unique :email
223
218
 
224
219
  Errors
225
220
  ------
data/lib/ohm.rb CHANGED
@@ -81,7 +81,8 @@ module Ohm
81
81
  return [] if empty?
82
82
  options[:start] ||= 0
83
83
  options[:limit] = [options[:start], options[:limit]] if options[:limit]
84
- instantiate(db.sort(key, options))
84
+ result = db.sort(key, options)
85
+ options[:get] ? result : instantiate(result)
85
86
  end
86
87
 
87
88
  # Sort the model instances by the given attribute.
@@ -222,6 +223,57 @@ module Ohm
222
223
  def size
223
224
  db.scard(key)
224
225
  end
226
+
227
+ # Returns an intersection with the sets generated from the passed hash.
228
+ #
229
+ # @see Ohm::Model.filter
230
+ # @yield [results] Results of the filtering. Beware that the set of results is deleted from Redis when the block ends.
231
+ # @example
232
+ # Event.search(day: "2009-09-11") do |search_results|
233
+ # events = search_results.all
234
+ # end
235
+ def filter(hash, &block)
236
+ apply(:sinterstore, keys(hash).push(key), &block)
237
+ end
238
+
239
+ # Returns a union with the sets generated from the passed hash.
240
+ #
241
+ # @see Ohm::Model.search
242
+ # @yield [results] Results of the search. Beware that the set of results is deleted from Redis when the block ends.
243
+ # @example
244
+ # Event.search(day: "2009-09-11") do |search_results|
245
+ # search_results.filter(public: true) do |filter_results|
246
+ # events = filter_results.all
247
+ # end
248
+ # end
249
+ def search(hash, &block)
250
+ apply(:sunionstore, keys(hash), &block)
251
+ end
252
+
253
+ def delete!
254
+ db.del(key)
255
+ end
256
+
257
+ # Apply a redis operation on a collection of sets. Note that
258
+ # the resulting set is removed inmediatly after use.
259
+ def apply(operation, source, &block)
260
+ target = source.join(operation)
261
+ db.send(operation, target, *source)
262
+ set = self.class.new(db, target, model)
263
+ block.call(set)
264
+ set.delete!
265
+ end
266
+
267
+ private
268
+
269
+ # Transform a hash of attribute/values into an array of keys.
270
+ def keys(hash)
271
+ hash.inject([]) do |acc, t|
272
+ acc + Array(t[1]).map do |v|
273
+ model.key(t[0], model.encode(v))
274
+ end
275
+ end
276
+ end
225
277
  end
226
278
  end
227
279
 
@@ -237,8 +289,8 @@ module Ohm
237
289
  # @overload assert_unique [:street, :city]
238
290
  # Validates that the :street and :city pair is unique.
239
291
  def assert_unique(attrs)
240
- index_key = index_key_for(Array(attrs), read_locals(Array(attrs)))
241
- assert(db.scard(index_key).zero? || db.sismember(index_key, id), [Array(attrs), :not_unique])
292
+ result = db.sinter(*Array(attrs).map { |att| index_key_for(att, send(att)) })
293
+ assert(result.empty? || result.include?(id.to_s), [attrs, :not_unique])
242
294
  end
243
295
  end
244
296
 
@@ -305,9 +357,6 @@ module Ohm
305
357
  # If you want to find a model instance by some attribute value, then an index for that
306
358
  # attribute must exist.
307
359
  #
308
- # Each index declaration creates an index. It can be either an index on one particular attribute,
309
- # or an index accross many attributes.
310
- #
311
360
  # @example
312
361
  # class User < Ohm::Model
313
362
  # attribute :email
@@ -317,12 +366,9 @@ module Ohm
317
366
  # # Now this is possible:
318
367
  # User.find :email, "ohm@example.com"
319
368
  #
320
- # @overload index :name
321
- # Creates an index for the name attribute.
322
- # @overload index [:street, :city]
323
- # Creates a composite index for street and city.
324
- def self.index(attrs)
325
- indices << Array(attrs)
369
+ # @param name [Symbol] Name of the attribute to be indexed.
370
+ def self.index(att)
371
+ indices << att
326
372
  end
327
373
 
328
374
  def self.attr_list_reader(name, model = nil)
@@ -369,18 +415,42 @@ module Ohm
369
415
  model
370
416
  end
371
417
 
372
- def self.find(attribute, value)
373
- Attributes::Set.new(db, key(attribute, encode(value)), self)
418
+ # Find all the records matching the specified attribute-value pair.
419
+ #
420
+ # @example
421
+ # Event.find(:starts_on, Date.today)
422
+ def self.find(attrs, value)
423
+ Attributes::Set.new(db, key(attrs, encode(value)), self)
424
+ end
425
+
426
+ # Search across multiple indices and return the intersection of the sets.
427
+ #
428
+ # @example Finds all the user events for the supplied days
429
+ # event1 = Event.create day: "2009-09-09", author: "Albert"
430
+ # event2 = Event.create day: "2009-09-09", author: "Benoit"
431
+ # event3 = Event.create day: "2009-09-10", author: "Albert"
432
+ # Event.filter(author: "Albert", day: "2009-09-09") do |events|
433
+ # assert_equal [event1], events
434
+ # end
435
+ def self.filter(hash, &block)
436
+ self.all.filter(hash, &block)
374
437
  end
375
438
 
376
- def self.encode(value)
377
- Base64.encode64(value.to_s).chomp
439
+ # Search across multiple indices and return the union of the sets.
440
+ #
441
+ # @example Finds all the events for the supplied days
442
+ # event1 = Event.create day: "2009-09-09"
443
+ # event2 = Event.create day: "2009-09-10"
444
+ # event3 = Event.create day: "2009-09-11"
445
+ # Event.search(day: ["2009-09-09", "2009-09-10", "2009-09-011"]) do |events|
446
+ # assert_equal [event1, event2, event3], events
447
+ # end
448
+ def self.search(hash, &block)
449
+ self.all.search(hash, &block)
378
450
  end
379
451
 
380
- def self.encode_each(values)
381
- values.collect do |value|
382
- encode(value)
383
- end
452
+ def self.encode(value)
453
+ Base64.encode64(value.to_s).gsub("\n", "")
384
454
  end
385
455
 
386
456
  def initialize(attrs = {})
@@ -533,14 +603,30 @@ module Ohm
533
603
  end
534
604
 
535
605
  def add_to_indices
536
- indices.each do |attrs|
537
- db.sadd(index_key_for(attrs, read_locals(attrs)), id)
606
+ indices.each do |att|
607
+ next add_to_index(att) unless collection?(send(att))
608
+ send(att).each { |value| add_to_index(att, value) }
538
609
  end
539
610
  end
540
611
 
612
+ def collection?(value)
613
+ self.class.collection?(value)
614
+ end
615
+
616
+ def self.collection?(value)
617
+ value.kind_of?(Enumerable) &&
618
+ value.kind_of?(String) == false
619
+ end
620
+
621
+ def add_to_index(att, value = send(att))
622
+ index = index_key_for(att, value)
623
+ db.sadd(index, id)
624
+ db.sadd(key(:_indices), index)
625
+ end
626
+
541
627
  def delete_from_indices
542
- indices.each do |attrs|
543
- db.srem(index_key_for(attrs, read_remotes(attrs)), id)
628
+ db.smembers(key(:_indices)).each do |index|
629
+ db.srem(index, id)
544
630
  end
545
631
  end
546
632
 
@@ -572,8 +658,8 @@ module Ohm
572
658
  end
573
659
  end
574
660
 
575
- def index_key_for(attrs, values)
576
- self.class.key *(attrs + self.class.encode_each(values))
661
+ def index_key_for(att, value)
662
+ self.class.key(att, self.class.encode(value))
577
663
  end
578
664
 
579
665
  # Lock the object so no other instances can modify it.
@@ -25,7 +25,7 @@ class ConnectionTest < Test::Unit::TestCase
25
25
  threads << Thread.new do
26
26
  conn1 = Ohm.redis
27
27
  end
28
-
28
+
29
29
  threads << Thread.new do
30
30
  conn2 = Ohm.redis
31
31
  end
data/test/indices_test.rb CHANGED
@@ -1,21 +1,30 @@
1
1
  require File.join(File.dirname(__FILE__), "test_helper")
2
2
 
3
3
  class IndicesTest < Test::Unit::TestCase
4
+ setup do
5
+ Ohm.flush
6
+ end
7
+
4
8
  class User < Ohm::Model
5
9
  attribute :email
10
+ attribute :update
6
11
 
7
12
  index :email
8
13
  index :email_provider
14
+ index :working_days
15
+ index :update
9
16
 
10
17
  def email_provider
11
18
  email.split("@").last
12
19
  end
20
+
21
+ def working_days
22
+ @working_days ||= []
23
+ end
13
24
  end
14
25
 
15
26
  context "A model with an indexed attribute" do
16
27
  setup do
17
- Ohm.flush
18
-
19
28
  @user1 = User.create(:email => "foo")
20
29
  @user2 = User.create(:email => "bar")
21
30
  @user3 = User.create(:email => "baz qux")
@@ -51,8 +60,6 @@ class IndicesTest < Test::Unit::TestCase
51
60
 
52
61
  context "Indexing arbitrary attributes" do
53
62
  setup do
54
- Ohm.flush
55
-
56
63
  @user1 = User.create(:email => "foo@gmail.com")
57
64
  @user2 = User.create(:email => "bar@gmail.com")
58
65
  @user3 = User.create(:email => "bazqux@yahoo.com")
@@ -63,4 +70,82 @@ class IndicesTest < Test::Unit::TestCase
63
70
  assert_equal [@user3], User.find(:email_provider, "yahoo.com")
64
71
  end
65
72
  end
73
+
74
+ context "Indexing enumerables" do
75
+ setup do
76
+ @user1 = User.create(:email => "foo@gmail.com")
77
+ @user2 = User.create(:email => "bar@gmail.com")
78
+
79
+ @user1.working_days << "Mon"
80
+ @user1.working_days << "Tue"
81
+ @user2.working_days << "Mon"
82
+ @user2.working_days << "Wed"
83
+
84
+ @user1.save
85
+ @user2.save
86
+ end
87
+
88
+ should "index each item" do
89
+ assert_equal [@user1, @user2], User.find(:working_days, "Mon").to_a.sort_by { |u| u.id }
90
+ end
91
+
92
+ # TODO If we deal with Ohm collections, the updates are atomic but the reindexing never happens.
93
+ # One solution may be to reindex after inserts or deletes in collection.
94
+ should "remove the indices when the object changes" do
95
+ @user2.working_days.delete "Mon"
96
+ @user2.save
97
+ assert_equal [@user1], User.find(:working_days, "Mon")
98
+ end
99
+ end
100
+
101
+ context "Intersection and and union" do
102
+ class Event < Ohm::Model
103
+ attr_writer :days
104
+
105
+ attribute :timeline
106
+ index :timeline
107
+ index :days
108
+
109
+ def days
110
+ @days ||= []
111
+ end
112
+ end
113
+
114
+ setup do
115
+ @event1 = Event.create(timeline: 1)
116
+ @event2 = Event.create(timeline: 1)
117
+ @event3 = Event.create(timeline: 2)
118
+ @event1.days = [1, 2]
119
+ @event2.days = [2, 3]
120
+ @event3.days = [3, 4]
121
+ @event1.save
122
+ @event2.save
123
+ @event3.save
124
+ end
125
+
126
+ should "intersect multiple sets of results" do
127
+ Event.filter(timeline: 1, days: [1, 2]) do |set|
128
+ assert_equal [@event1], set
129
+ end
130
+ end
131
+
132
+ should "group multiple sets of results" do
133
+ Event.search(days: [1, 2]) do |set|
134
+ assert_equal [@event1, @event2], set
135
+ end
136
+ end
137
+
138
+ should "combine intersections and unions" do
139
+ Event.search(days: [1, 2, 3]) do |events|
140
+ events.filter(timeline: 1) do |result|
141
+ assert_equal [@event1, @event2], result
142
+ end
143
+ end
144
+ end
145
+
146
+ should "work with strings that generate a new line when encoded" do
147
+ user = User.create(email: "foo@bar", update: "CORRECTED - UPDATE 2-Suspected US missile strike kills 5 in Pakistan")
148
+ assert_equal [user], User.find(:update, "CORRECTED - UPDATE 2-Suspected US missile strike kills 5 in Pakistan")
149
+ end
150
+ end
66
151
  end
data/test/model_test.rb CHANGED
@@ -221,6 +221,14 @@ class TestRedis < Test::Unit::TestCase
221
221
  Person.create :name => "A"
222
222
  assert_equal "A", Person.all.first(:by => :name, :order => "ALPHA").name
223
223
  end
224
+
225
+ should "return attribute values when the get parameter is specified" do
226
+ Ohm.flush
227
+ Person.create :name => "B"
228
+ Person.create :name => "A"
229
+
230
+ assert_equal "A", Person.all.sort_by(:name, get: "Person:*:name", order: "ALPHA").first
231
+ end
224
232
  end
225
233
 
226
234
  context "Loading attributes" do
@@ -7,7 +7,7 @@ class ValidationsTest < Test::Unit::TestCase
7
7
  attribute :capacity
8
8
 
9
9
  index :name
10
- index [:name, :place]
10
+ index :place
11
11
 
12
12
  def validate
13
13
  assert_format(:name, /^\w+$/)
@@ -99,12 +99,12 @@ class ValidationsTest < Test::Unit::TestCase
99
99
  @event.create
100
100
 
101
101
  assert_nil @event.id
102
- assert_equal [[[:name], :not_unique]], @event.errors
102
+ assert_equal [[:name, :not_unique]], @event.errors
103
103
  end
104
104
  end
105
105
 
106
106
  context "That must have a unique name scoped by place" do
107
- should "fail when the value already exists" do
107
+ should "fail when the value already exists for a scoped attribute" do
108
108
  def @event.validate
109
109
  assert_unique [:name, :place]
110
110
  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.18
4
+ version: 0.0.19
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-09-07 00:00:00 -03:00
13
+ date: 2009-09-15 00:00:00 -03:00
14
14
  default_executable:
15
15
  dependencies: []
16
16