ohm 0.0.26 → 0.0.27

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,6 +12,12 @@ Ohm is a library for storing objects in
12
12
  database. It includes an extensible list of validations and has very
13
13
  good performance.
14
14
 
15
+ Community
16
+ ---------
17
+
18
+ Join the mailing list: [http://groups.google.com/group/ohm-ruby](http://groups.google.com/group/ohm-ruby)
19
+
20
+ Meet us on IRC: [#ohm](irc://chat.freenode.net/#ohm) on [freenode.net](http://freenode.net/)
15
21
 
16
22
  Getting started
17
23
  ---------------
@@ -157,9 +163,9 @@ An index is a set that's handled automatically by Ohm. For any index declared,
157
163
  Ohm maintains different sets of objects ids for quick lookups.
158
164
 
159
165
  For example, in the example above, the index on the name attribute will
160
- allow for searches like Event.find(:name, "some value").
166
+ allow for searches like Event.find(name: "some value").
161
167
 
162
- Note that the `assert_unique` validation and the methods `find`, `search` and `filter` need a
168
+ Note that the `assert_unique` validation and the methods `find` and `except` need a
163
169
  corresponding index in order to work.
164
170
 
165
171
  ### Finding
@@ -169,27 +175,22 @@ You can find a collection of records with the `find` method:
169
175
  # This returns a collection of users with the username "Albert"
170
176
  User.find(username: "Albert")
171
177
 
172
- ### Searching and filtering
178
+ ### Filtering results
173
179
 
174
- # This returns a collection of users with usernames "Albert" or "Benoit"
175
- User.search(username: ["Albert", "Benoit"]) do |search_results|
176
- @users = search_results.all
177
- end
180
+ # Find all users from Argentina
181
+ User.find(country: "Argentina")
178
182
 
179
- # This returns a collection of users with usernames "Albert" or "Benoit",
180
- # but only those with the account enabled.
181
- User.search(username: ["Albert", "Benoit"]) do |search_results|
182
- search_results.filter(account: "enabled") do |filter_results|
183
- @users = filter_results.all
184
- end
185
- end
183
+ # Find all activated users from Argentina
184
+ User.find(country: "Argentina", status: "activated")
185
+
186
+ # Find all users from Argentina, except those with a suspended account.
187
+ User.find(country: "Argentina").except(status: "suspended")
188
+
189
+ Note that calling these methods results in new sets being created
190
+ on the fly. This is important so that you can perform further operations
191
+ before reading the items to the client.
186
192
 
187
- Important: note that both `search` and `filter` yield the results
188
- to a block. This is important because the set of results is stored
189
- for the duration of the block (to allow for chaining of searches and
190
- filters), but is deleted once the block ends. The `.all` is necessary
191
- for retrieving the actual instances, as keeping a reference to a set
192
- that is going to be deleted would only return empty sets.
193
+ For more information, see (SINTERSTORE)[http://code.google.com/p/redis/wiki/SinterstoreCommand] and (SDIFFSTORE)[http://code.google.com/p/redis/wiki/SdiffstoreCommand].
193
194
 
194
195
  Validations
195
196
  -----------
@@ -0,0 +1,27 @@
1
+ # encoding: UTF-8
2
+
3
+ class Ohm < Thor
4
+ desc "doc", "Generate YARD documentation"
5
+ method_options :open => false
6
+ def doc
7
+ require "yard"
8
+
9
+ opts = ["--protected", "--title", "Ohm – Object-hash mapping library for Redis"]
10
+
11
+ YARD::CLI::Yardoc.run(*opts)
12
+
13
+ system "open doc/index.html" if options[:open]
14
+ end
15
+
16
+ desc "deploy", "Deploy documentation"
17
+ def deploy
18
+ system "rsync -az doc/* ohm.keyvalue.org:deploys/ohm.keyvalue.org/"
19
+ end
20
+
21
+ desc "test", "Run all tests"
22
+ def test
23
+ Dir["test/**/*_test.rb"].each do |file|
24
+ load file
25
+ end
26
+ end
27
+ end
data/lib/ohm.rb CHANGED
@@ -74,11 +74,14 @@ module Ohm
74
74
  # @option options [#to_s] :order (ASC) Sorting order, which can be ASC or DESC.
75
75
  # @option options [Integer] :limit (all) Number of items to return.
76
76
  # @option options [Integer] :start (0) An offset from where the limit will be applied.
77
+ #
77
78
  # @example Get the first ten users sorted alphabetically by name:
78
- # @event.attendees.sort(User, :by => :name, :order => "ALPHA", :limit => 10)
79
+ #
80
+ # @event.attendees.sort(:by => :name, :order => "ALPHA", :limit => 10)
79
81
  #
80
82
  # @example Get five posts sorted by number of votes and starting from the number 5 (zero based):
81
- # @blog.posts.sort(Post, :by => :votes, :start => 5, :limit => 10")
83
+ #
84
+ # @blog.posts.sort(:by => :votes, :start => 5, :limit => 10")
82
85
  def sort(options = {})
83
86
  return [] if empty?
84
87
  options[:start] ||= 0
@@ -94,8 +97,9 @@ module Ohm
94
97
  # User.create :name => "B"
95
98
  # User.create :name => "A"
96
99
  #
97
- # user = User.all.sort_by :name, :order => "ALPHA"
98
- # user.name == "A" #=> true
100
+ # user = User.all.sort_by(:name, :order => "ALPHA").first
101
+ # user.name == "A"
102
+ # # => true
99
103
  def sort_by(att, options = {})
100
104
  sort(options.merge(:by => model.key("*", att)))
101
105
  end
@@ -128,6 +132,29 @@ module Ohm
128
132
  size.zero?
129
133
  end
130
134
 
135
+ # Clears the values in the collection.
136
+ def clear
137
+ db.del(key)
138
+ self
139
+ end
140
+
141
+ # Appends the given values to the collection.
142
+ def concat(values)
143
+ values.each { |value| self << value }
144
+ self
145
+ end
146
+
147
+ # Replaces the collection with the passed values.
148
+ def replace(values)
149
+ clear
150
+ concat(values)
151
+ end
152
+
153
+ # @param value [Ohm::Model#id] Adds the id of the object if it's an Ohm::Model.
154
+ def add(model)
155
+ self << model.id
156
+ end
157
+
131
158
  private
132
159
 
133
160
  def instantiate(raw)
@@ -180,6 +207,10 @@ module Ohm
180
207
  db.llen(key)
181
208
  end
182
209
 
210
+ def include?(value)
211
+ raw.include?(value)
212
+ end
213
+
183
214
  def inspect
184
215
  "#<List: #{raw.inspect}>"
185
216
  end
@@ -206,13 +237,6 @@ module Ohm
206
237
  db.sadd(key, value)
207
238
  end
208
239
 
209
- # @param value [Ohm::Model#id] Adds the id of the object if it's an Ohm::Model.
210
- def add model
211
- raise ArgumentError unless model.kind_of?(Ohm::Model)
212
- raise ArgumentError unless model.id
213
- self << model.id
214
- end
215
-
216
240
  def delete(value)
217
241
  db.srem(key, value)
218
242
  end
@@ -230,70 +254,63 @@ module Ohm
230
254
  db.scard(key)
231
255
  end
232
256
 
257
+ def inspect
258
+ "#<Set: #{raw.inspect}>"
259
+ end
260
+
233
261
  # Returns an intersection with the sets generated from the passed hash.
234
262
  #
235
- # @see Ohm::Model.filter
236
- # @yield [results] Results of the filtering. Beware that the set of results is deleted from Redis when the block ends.
263
+ # @see Ohm::Model.find
237
264
  # @example
238
- # Event.filter(public: true) do |filter_results|
239
- # @events = filter_results.all
240
- # end
265
+ # @events = Event.find(public: true)
241
266
  #
242
- # # You can also combine search and filter
243
- # Event.search(day: "2009-09-11") do |search_results|
244
- # search_results.filter(public: true) do |filter_results|
245
- # @events = filter_results.all
246
- # end
247
- # end
248
- def filter(hash, &block)
249
- raise ArgumentError, "filter expects a block" unless block_given?
250
- apply(:sinterstore, keys(hash).push(key), &block)
251
- end
252
-
253
- # Returns a union with the sets generated from the passed hash.
267
+ # # You can combine the result with sort and other set operations:
268
+ # @events.sort_by(:name)
269
+ def find(hash)
270
+ apply(:sinterstore, hash, "+")
271
+ end
272
+
273
+ # Returns the difference between the receiver and the passed sets.
254
274
  #
255
- # @see Ohm::Model.search
256
- # @yield [results] Results of the search. Beware that the set of results is deleted from Redis when the block ends.
257
275
  # @example
258
- # Event.search(day: "2009-09-11") do |search_results|
259
- # events = search_results.all
260
- # end
261
- def search(hash, &block)
262
- raise ArgumentError, "search expects a block" unless block_given?
263
- apply(:sunionstore, keys(hash), &block)
276
+ # @events = Event.find(public: true).except(status: "sold_out")
277
+ def except(hash)
278
+ apply(:sdiffstore, hash, "-")
264
279
  end
265
280
 
266
- def delete!
267
- db.del(key)
268
- end
281
+ private
269
282
 
270
- # Apply a redis operation on a collection of sets. Note that
271
- # the resulting set is removed inmediatly after use.
272
- def apply(operation, source, &block)
273
- target = source.uniq.join("+")
274
- db.send(operation, target, *source)
275
- set = self.class.new(db, target, model)
276
- block.call(set)
277
- set.delete! if source.size > 1
283
+ # Apply a redis operation on a collection of sets.
284
+ def apply(operation, hash, glue)
285
+ indices = keys(hash).unshift(key).uniq
286
+ target = indices.join(glue)
287
+ db.send(operation, target, *indices)
288
+ self.class.new(db, target, model)
278
289
  end
279
290
 
280
- def inspect
281
- "#<Set: #{raw.inspect}>"
282
- end
283
-
284
- private
285
-
286
291
  # Transform a hash of attribute/values into an array of keys.
287
292
  def keys(hash)
288
293
  hash.inject([]) do |acc, t|
289
294
  acc + Array(t[1]).map do |v|
290
- model.key(t[0], model.encode(v))
295
+ model.index_key_for(t[0], v)
291
296
  end
292
297
  end
293
298
  end
294
299
  end
300
+
301
+ class Index < Set
302
+ def inspect
303
+ "#<Index: #{raw.inspect}>"
304
+ end
305
+
306
+ def clear
307
+ raise Ohm::Model::CannotDeleteIndex
308
+ end
309
+ end
295
310
  end
296
311
 
312
+ Error = Class.new(StandardError)
313
+
297
314
  class Model
298
315
  module Validations
299
316
  include Ohm::Validations
@@ -307,20 +324,44 @@ module Ohm
307
324
  # Validates that the :street and :city pair is unique.
308
325
  def assert_unique(attrs)
309
326
  result = db.sinter(*Array(attrs).map { |att| index_key_for(att, send(att)) })
310
- assert(result.empty? || result.include?(id.to_s), [attrs, :not_unique])
327
+ assert result.empty? || !new? && result.include?(id.to_s), [attrs, :not_unique]
311
328
  end
312
329
  end
313
330
 
314
331
  include Validations
315
332
 
316
- ModelIsNew = Class.new(StandardError)
333
+ class MissingID < Error
334
+ def message
335
+ "You tried to perform an operation that needs the model ID, but it's not present."
336
+ end
337
+ end
338
+
339
+ class CannotDeleteIndex < Error
340
+ def message
341
+ "You tried to delete an internal index used by Ohm."
342
+ end
343
+ end
344
+
345
+ class IndexNotFound < Error
346
+ def initialize(att)
347
+ @att = att
348
+ end
349
+
350
+ def message
351
+ "Index #{@att.inspect} not found."
352
+ end
353
+ end
317
354
 
318
355
  @@attributes = Hash.new { |hash, key| hash[key] = [] }
319
356
  @@collections = Hash.new { |hash, key| hash[key] = [] }
320
357
  @@counters = Hash.new { |hash, key| hash[key] = [] }
321
358
  @@indices = Hash.new { |hash, key| hash[key] = [] }
322
359
 
323
- attr_accessor :id
360
+ attr_writer :id
361
+
362
+ def id
363
+ @id or raise MissingID
364
+ end
324
365
 
325
366
  # Defines a string attribute for the model. This attribute will be persisted by Redis
326
367
  # as a string. Any value stored here will be retrieved in its string representation.
@@ -381,7 +422,7 @@ module Ohm
381
422
  # end
382
423
  #
383
424
  # # Now this is possible:
384
- # User.find :email, "ohm@example.com"
425
+ # User.find email: "ohm@example.com"
385
426
  #
386
427
  # @param name [Symbol] Name of the attribute to be indexed.
387
428
  def self.index(att)
@@ -411,7 +452,7 @@ module Ohm
411
452
  end
412
453
 
413
454
  def self.all
414
- @all ||= Attributes::Set.new(db, key(:all), self)
455
+ @all ||= Attributes::Index.new(db, key(:all), self)
415
456
  end
416
457
 
417
458
  def self.attributes
@@ -436,38 +477,16 @@ module Ohm
436
477
  model
437
478
  end
438
479
 
439
- # Find all the records matching the specified attribute-value pair.
440
- #
441
- # @example
442
- # Event.find(:starts_on, Date.today)
443
- def self.find(attrs, value)
444
- Attributes::Set.new(db, key(attrs, encode(value)), self)
445
- end
446
-
447
480
  # Search across multiple indices and return the intersection of the sets.
448
481
  #
449
482
  # @example Finds all the user events for the supplied days
450
483
  # event1 = Event.create day: "2009-09-09", author: "Albert"
451
484
  # event2 = Event.create day: "2009-09-09", author: "Benoit"
452
485
  # event3 = Event.create day: "2009-09-10", author: "Albert"
453
- # Event.filter(author: "Albert", day: "2009-09-09") do |events|
454
- # assert_equal [event1], events
455
- # end
456
- def self.filter(hash, &block)
457
- self.all.filter(hash, &block)
458
- end
459
-
460
- # Search across multiple indices and return the union of the sets.
461
486
  #
462
- # @example Finds all the events for the supplied days
463
- # event1 = Event.create day: "2009-09-09"
464
- # event2 = Event.create day: "2009-09-10"
465
- # event3 = Event.create day: "2009-09-11"
466
- # Event.search(day: ["2009-09-09", "2009-09-10", "2009-09-011"]) do |events|
467
- # assert_equal [event1, event2, event3], events
468
- # end
469
- def self.search(hash, &block)
470
- self.all.search(hash, &block)
487
+ # assert_equal [event1], Event.find(author: "Albert", day: "2009-09-09")
488
+ def self.find(hash)
489
+ all.find(hash)
471
490
  end
472
491
 
473
492
  def self.encode(value)
@@ -480,7 +499,7 @@ module Ohm
480
499
  end
481
500
 
482
501
  def new?
483
- !id
502
+ !@id
484
503
  end
485
504
 
486
505
  def create
@@ -524,19 +543,19 @@ module Ohm
524
543
  self
525
544
  end
526
545
 
527
- # Increment the attribute denoted by :att.
546
+ # Increment the counter denoted by :att.
528
547
  #
529
548
  # @param att [Symbol] Attribute to increment.
530
549
  def incr(att)
531
- raise ArgumentError unless counters.include?(att)
550
+ raise ArgumentError, "#{att.inspect} is not a counter." unless counters.include?(att)
532
551
  write_local(att, db.incr(key(att)))
533
552
  end
534
553
 
535
- # Decrement the attribute denoted by :att.
554
+ # Decrement the counter denoted by :att.
536
555
  #
537
556
  # @param att [Symbol] Attribute to decrement.
538
557
  def decr(att)
539
- raise ArgumentError unless counters.include?(att)
558
+ raise ArgumentError, "#{att.inspect} is not a counter." unless counters.include?(att)
540
559
  write_local(att, db.decr(key(att)))
541
560
  end
542
561
 
@@ -558,7 +577,7 @@ module Ohm
558
577
 
559
578
  def ==(other)
560
579
  other.kind_of?(self.class) && other.key == key
561
- rescue ModelIsNew
580
+ rescue MissingID
562
581
  false
563
582
  end
564
583
 
@@ -574,20 +593,19 @@ module Ohm
574
593
  everything = (attributes + collections + counters).map do |att|
575
594
  value = begin
576
595
  send(att)
577
- rescue ModelIsNew
596
+ rescue MissingID
578
597
  nil
579
598
  end
580
599
 
581
600
  [att, value.inspect]
582
601
  end
583
602
 
584
- "#<#{self.class}:#{id || "?"} #{everything.map {|e| e.join("=") }.join(" ")}>"
603
+ "#<#{self.class}:#{new? ? "?" : id} #{everything.map {|e| e.join("=") }.join(" ")}>"
585
604
  end
586
605
 
587
606
  protected
588
607
 
589
608
  def key(*args)
590
- raise ModelIsNew if new?
591
609
  self.class.key(id, *args)
592
610
  end
593
611
 
@@ -610,7 +628,7 @@ module Ohm
610
628
  end
611
629
 
612
630
  def initialize_id
613
- self.id = db.incr(self.class.key("id"))
631
+ self.id = db.incr(self.class.key("id")).to_s
614
632
  end
615
633
 
616
634
  def db
@@ -673,7 +691,7 @@ module Ohm
673
691
  end
674
692
 
675
693
  def read_remote(att)
676
- id && db.get(key(att))
694
+ db.get(key(att)) unless new?
677
695
  end
678
696
 
679
697
  def write_remote(att, value)
@@ -694,8 +712,13 @@ module Ohm
694
712
  end
695
713
  end
696
714
 
715
+ def self.index_key_for(att, value)
716
+ raise IndexNotFound, att unless indices.include?(att)
717
+ key(att, encode(value))
718
+ end
719
+
697
720
  def index_key_for(att, value)
698
- self.class.key(att, self.class.encode(value))
721
+ self.class.index_key_for(att, value)
699
722
  end
700
723
 
701
724
  # Lock the object so no other instances can modify it.
@@ -9,6 +9,7 @@ class IndicesTest < Test::Unit::TestCase
9
9
  attribute :email
10
10
  attribute :update
11
11
  attribute :activation_code
12
+ attribute :sandunga
12
13
 
13
14
  index :email
14
15
  index :email_provider
@@ -38,30 +39,46 @@ class IndicesTest < Test::Unit::TestCase
38
39
  end
39
40
 
40
41
  should "be able to find by the given attribute" do
41
- assert_equal @user1, User.find(:email, "foo").first
42
+ assert_equal @user1, User.find(:email => "foo").first
43
+ end
44
+
45
+ should "raise if the field is not indexed" do
46
+ assert_raises(Ohm::Model::IndexNotFound) do
47
+ User.find(:sandunga => "foo")
48
+ end
42
49
  end
43
50
 
44
51
  should "return nil if no results are found" do
45
- assert User.find(:email, "foobar").empty?
46
- assert_equal nil, User.find(:email, "foobar").first
52
+ assert User.find(:email => "foobar").empty?
53
+ assert_equal nil, User.find(:email => "foobar").first
47
54
  end
48
55
 
49
56
  should "update indices when changing attribute values" do
50
57
  @user1.email = "baz"
51
58
  @user1.save
52
59
 
53
- assert_equal [], User.find(:email, "foo")
54
- assert_equal [@user1], User.find(:email, "baz")
60
+ assert_equal [], User.find(:email => "foo")
61
+ assert_equal [@user1], User.find(:email => "baz")
55
62
  end
56
63
 
57
64
  should "remove from the index after deleting" do
58
65
  @user2.delete
59
66
 
60
- assert_equal [], User.find(:email, "bar")
67
+ assert_equal [], User.find(:email => "bar")
61
68
  end
62
69
 
63
70
  should "work with attributes that contain spaces" do
64
- assert_equal [@user3], User.find(:email, "baz qux")
71
+ assert_equal [@user3], User.find(:email => "baz qux")
72
+ end
73
+
74
+ should "not allow to manually clear an index" do
75
+ assert_raise Ohm::Model::CannotDeleteIndex do
76
+ User.find(:email => "bar").clear
77
+ end
78
+
79
+ assert_raise Ohm::Model::CannotDeleteIndex do
80
+ User.find(:email => "bar").find(:email => "baz").clear
81
+ end
65
82
  end
66
83
  end
67
84
 
@@ -73,12 +90,12 @@ class IndicesTest < Test::Unit::TestCase
73
90
  end
74
91
 
75
92
  should "allow indexing by an arbitrary attribute" do
76
- assert_equal [@user1, @user2], User.find(:email_provider, "gmail.com").to_a.sort_by { |u| u.id }
77
- assert_equal [@user3], User.find(:email_provider, "yahoo.com")
93
+ assert_equal [@user1, @user2], User.find(:email_provider => "gmail.com").to_a.sort_by { |u| u.id }
94
+ assert_equal [@user3], User.find(:email_provider => "yahoo.com")
78
95
  end
79
96
 
80
97
  should "allow indexing by an attribute that is lazily set" do
81
- assert_equal [@user1], User.find(:activation_code, "user:1").to_a
98
+ assert_equal [@user1], User.find(:activation_code => "user:1").to_a
82
99
  end
83
100
  end
84
101
 
@@ -97,7 +114,7 @@ class IndicesTest < Test::Unit::TestCase
97
114
  end
98
115
 
99
116
  should "index each item" do
100
- assert_equal [@user1, @user2], User.find(:working_days, "Mon").to_a.sort_by { |u| u.id }
117
+ assert_equal [@user1, @user2], User.find(:working_days => "Mon").to_a.sort_by { |u| u.id }
101
118
  end
102
119
 
103
120
  # TODO If we deal with Ohm collections, the updates are atomic but the reindexing never happens.
@@ -105,11 +122,11 @@ class IndicesTest < Test::Unit::TestCase
105
122
  should "remove the indices when the object changes" do
106
123
  @user2.working_days.delete "Mon"
107
124
  @user2.save
108
- assert_equal [@user1], User.find(:working_days, "Mon")
125
+ assert_equal [@user1], User.find(:working_days => "Mon")
109
126
  end
110
127
  end
111
128
 
112
- context "Intersection and and union" do
129
+ context "Intersection and difference" do
113
130
  class Event < Ohm::Model
114
131
  attr_writer :days
115
132
 
@@ -123,40 +140,29 @@ class IndicesTest < Test::Unit::TestCase
123
140
  end
124
141
 
125
142
  setup do
126
- @event1 = Event.create(:timeline => 1)
127
- @event2 = Event.create(:timeline => 1)
128
- @event3 = Event.create(:timeline => 2)
129
- @event1.days = [1, 2]
130
- @event2.days = [2, 3]
131
- @event3.days = [3, 4]
132
- @event1.save
133
- @event2.save
134
- @event3.save
143
+ @event1 = Event.create(:timeline => 1).update(:days => [1, 2])
144
+ @event2 = Event.create(:timeline => 1).update(:days => [2, 3])
145
+ @event3 = Event.create(:timeline => 2).update(:days => [3, 4])
135
146
  end
136
147
 
137
148
  should "intersect multiple sets of results" do
138
- Event.filter(:timeline => 1, :days => [1, 2]) do |set|
139
- assert_equal [@event1], set
140
- end
149
+ assert_equal [@event1], Event.find(:timeline => 1, :days => [1, 2])
150
+ assert_equal [@event1], Event.find(:timeline => 1).find(:days => [1, 2])
141
151
  end
142
152
 
143
- should "group multiple sets of results" do
144
- Event.search(:days => [1, 2]) do |set|
145
- assert_equal [@event1, @event2], set
146
- end
153
+ should "compute the difference between sets" do
154
+ assert_equal [@event2], Event.find(:timeline => 1).except(:days => 1)
147
155
  end
148
156
 
149
- should "combine intersections and unions" do
150
- Event.search(:days => [1, 2, 3]) do |events|
151
- events.filter(:timeline => 1) do |result|
152
- assert_equal [@event1, @event2], result
153
- end
157
+ should "raise if the argument is not an index" do
158
+ assert_raises(Ohm::Model::IndexNotFound) do
159
+ Event.find(:timeline => 1).except(:not_an_index => 1)
154
160
  end
155
161
  end
156
162
 
157
163
  should "work with strings that generate a new line when encoded" do
158
164
  user = User.create(:email => "foo@bar", :update => "CORRECTED - UPDATE 2-Suspected US missile strike kills 5 in Pakistan")
159
- assert_equal [user], User.find(:update, "CORRECTED - UPDATE 2-Suspected US missile strike kills 5 in Pakistan")
165
+ assert_equal [user], User.find(:update => "CORRECTED - UPDATE 2-Suspected US missile strike kills 5 in Pakistan")
160
166
  end
161
167
  end
162
168
  end
@@ -6,6 +6,7 @@ require "ostruct"
6
6
  class Post < Ohm::Model
7
7
  attribute :body
8
8
  list :comments
9
+ list :related, Post
9
10
  end
10
11
 
11
12
  class User < Ohm::Model
@@ -15,10 +16,15 @@ end
15
16
 
16
17
  class Person < Ohm::Model
17
18
  attribute :name
19
+ index :initial
18
20
 
19
21
  def validate
20
22
  assert_present :name
21
23
  end
24
+
25
+ def initial
26
+ name[0, 1].upcase
27
+ end
22
28
  end
23
29
 
24
30
  class Event < Ohm::Model
@@ -44,9 +50,13 @@ class TestRedis < Test::Unit::TestCase
44
50
 
45
51
  context "An event created from a hash of attributes" do
46
52
  should "assign an id and save the object" do
53
+ Ohm.flush
54
+
47
55
  event1 = Event.create(:name => "Ruby Tuesday")
48
56
  event2 = Event.create(:name => "Ruby Meetup")
49
- assert_equal event1.id + 1, event2.id
57
+
58
+ assert_equal "1", event1.id
59
+ assert_equal "2", event2.id
50
60
  end
51
61
 
52
62
  should "return the unsaved object if validation fails" do
@@ -144,14 +154,19 @@ class TestRedis < Test::Unit::TestCase
144
154
 
145
155
  context "Creating a new model" do
146
156
  should "assign a new id to the event" do
157
+ Ohm.flush
158
+
147
159
  event1 = Event.new
148
160
  event1.create
149
161
 
150
162
  event2 = Event.new
151
163
  event2.create
152
164
 
153
- assert event1.id
154
- assert_equal event1.id + 1, event2.id
165
+ assert !event1.new?
166
+ assert !event2.new?
167
+
168
+ assert_equal "1", event1.id
169
+ assert_equal "2", event2.id
155
170
  end
156
171
  end
157
172
 
@@ -265,6 +280,8 @@ class TestRedis < Test::Unit::TestCase
265
280
 
266
281
  context "Loading attributes" do
267
282
  setup do
283
+ Ohm.flush
284
+
268
285
  event = Event.new
269
286
  event.name = "Ruby Tuesday"
270
287
  @id = event.create.id
@@ -291,7 +308,7 @@ class TestRedis < Test::Unit::TestCase
291
308
  end
292
309
 
293
310
  should "not be available if the model is new" do
294
- assert_raise Ohm::Model::ModelIsNew do
311
+ assert_raise Ohm::Model::MissingID do
295
312
  @event.attendees << 1
296
313
  end
297
314
  end
@@ -338,6 +355,34 @@ class TestRedis < Test::Unit::TestCase
338
355
  @event.attendees << "3"
339
356
  assert_equal 3, @event.attendees.size
340
357
  end
358
+
359
+ should "empty the set" do
360
+ @event.create
361
+ @event.attendees << "1"
362
+
363
+ @event.attendees.clear
364
+
365
+ assert @event.attendees.empty?
366
+ end
367
+
368
+ should "replace the values in the set" do
369
+ @event.create
370
+ @event.attendees << "1"
371
+
372
+ @event.attendees.replace(["2", "3"])
373
+
374
+ assert_equal ["2", "3"], @event.attendees.raw.sort
375
+ end
376
+
377
+ should "filter elements" do
378
+ @event.create
379
+ @event.attendees.add(Person.create(:name => "Albert"))
380
+ @event.attendees.add(Person.create(:name => "Marie"))
381
+
382
+ assert_equal ["1"], @event.attendees.find(:initial => "A").raw
383
+ assert_equal ["2"], @event.attendees.find(:initial => "M").raw
384
+ assert_equal [], @event.attendees.find(:initial => "Z").raw
385
+ end
341
386
  end
342
387
 
343
388
  context "Attributes of type List" do
@@ -408,6 +453,34 @@ class TestRedis < Test::Unit::TestCase
408
453
  assert_equal "2", @post.comments.pop
409
454
  assert @post.comments.empty?
410
455
  end
456
+
457
+ should "empty the list" do
458
+ @post.comments.unshift "1"
459
+ @post.comments.clear
460
+
461
+ assert @post.comments.empty?
462
+ end
463
+
464
+ should "replace the values in the list" do
465
+ @post.comments.replace(["1", "2"])
466
+
467
+ assert_equal ["1", "2"], @post.comments.raw
468
+ end
469
+
470
+ should "add models" do
471
+ @post.related.add(Post.create(:body => "Hello"))
472
+
473
+ assert_equal ["2"], @post.related.raw
474
+ end
475
+
476
+ should "find elements in the list" do
477
+ another_post = Post.create
478
+
479
+ @post.related.add(another_post)
480
+
481
+ assert @post.related.include?(another_post.id)
482
+ assert !@post.related.include?("-1")
483
+ end
411
484
  end
412
485
 
413
486
  context "Applying arbitrary transformations" do
@@ -22,13 +22,13 @@ class ValidationsTest < Test::Unit::TestCase
22
22
  context "That must have a present name" do
23
23
  should "not be created if the name is never assigned" do
24
24
  @event.create
25
- assert_nil @event.id
25
+ assert @event.new?
26
26
  end
27
27
 
28
28
  should "not be created if the name assigned is empty" do
29
29
  @event.name = ""
30
30
  @event.create
31
- assert_nil @event.id
31
+ assert @event.new?
32
32
  end
33
33
 
34
34
  should "be created if the name assigned is not empty" do
@@ -41,7 +41,7 @@ class ValidationsTest < Test::Unit::TestCase
41
41
  should "not be created if the name doesn't match /^\w+$/" do
42
42
  @event.name = "hello-world"
43
43
  @event.create
44
- assert_nil @event.id
44
+ assert @event.new?
45
45
  end
46
46
  end
47
47
  end
@@ -56,7 +56,7 @@ class ValidationsTest < Test::Unit::TestCase
56
56
  @event.place = "bar"
57
57
  @event.create
58
58
 
59
- assert_nil @event.id
59
+ assert @event.new?
60
60
  assert_equal [[:capacity, :not_numeric]], @event.errors
61
61
  end
62
62
 
@@ -70,7 +70,7 @@ class ValidationsTest < Test::Unit::TestCase
70
70
  @event.capacity = "baz"
71
71
  @event.create
72
72
 
73
- assert_nil @event.id
73
+ assert @event.new?
74
74
  assert_equal [[:capacity, :not_numeric]], @event.errors
75
75
  end
76
76
 
@@ -98,7 +98,7 @@ class ValidationsTest < Test::Unit::TestCase
98
98
  @event.name = "foo"
99
99
  @event.create
100
100
 
101
- assert_nil @event.id
101
+ assert @event.new?
102
102
  assert_equal [[:name, :not_unique]], @event.errors
103
103
  end
104
104
  end
@@ -114,7 +114,7 @@ class ValidationsTest < Test::Unit::TestCase
114
114
  @event.place = "bar"
115
115
  @event.create
116
116
 
117
- assert_nil @event.id
117
+ assert @event.new?
118
118
  assert_equal [[[:name, :place], :not_unique]], @event.errors
119
119
 
120
120
  @event.place = "baz"
@@ -123,6 +123,18 @@ class ValidationsTest < Test::Unit::TestCase
123
123
  assert @event.valid?
124
124
  end
125
125
  end
126
+
127
+ context "That defines a unique validation on a non indexed attribute" do
128
+ should "raise ArgumentError" do
129
+ def @event.validate
130
+ assert_unique :capacity
131
+ end
132
+
133
+ assert_raises(Ohm::Model::IndexNotFound) do
134
+ @event.valid?
135
+ end
136
+ end
137
+ end
126
138
  end
127
139
 
128
140
  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.26
4
+ version: 0.0.27
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-10-26 00:00:00 -03:00
13
+ date: 2009-11-19 00:00:00 -03:00
14
14
  default_executable:
15
15
  dependencies: []
16
16
 
@@ -31,6 +31,7 @@ files:
31
31
  - README.markdown
32
32
  - LICENSE
33
33
  - Rakefile
34
+ - Thorfile
34
35
  - test/all_tests.rb
35
36
  - test/benchmarks.rb
36
37
  - test/connection_test.rb
@@ -39,7 +40,6 @@ files:
39
40
  - test/model_test.rb
40
41
  - test/mutex_test.rb
41
42
  - test/redis_test.rb
42
- - test/search_test.rb
43
43
  - test/test_helper.rb
44
44
  - test/validations_test.rb
45
45
  - test/test.conf
@@ -1,42 +0,0 @@
1
- require File.join(File.dirname(__FILE__), "test_helper")
2
-
3
- class SearchTest < Test::Unit::TestCase
4
- setup do
5
- Ohm.flush
6
- end
7
-
8
- class Lane < Ohm::Model
9
- attribute :lane_type
10
-
11
- index :lane_type
12
-
13
- def to_s
14
- lane_type.capitalize
15
- end
16
-
17
- def validate
18
- assert_unique :lane_type
19
- end
20
-
21
- def error_messages
22
- errors.present do |e|
23
- e.on [:lane_type, :not_unique], "The lane type #{lane_type} is already in use"
24
- end
25
- end
26
- end
27
-
28
- context "A model with an indexed attribute" do
29
- setup do
30
- @results = []
31
- @subresults = []
32
- Lane.search(:lane_type => "email") { |sr| @results << sr.size }
33
- Lane.create(:lane_type => "email")
34
- Lane.search(:lane_type => "email") { |sr| @results << sr.size }
35
- Lane.search(:lane_type => "email") { |sr| @results << sr.size }
36
- end
37
-
38
- should "be able to find by the given attribute" do
39
- assert_equal [0, 1, 1], @results
40
- end
41
- end
42
- end