ohm 1.0.0.alpha2 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -123,47 +123,51 @@ the example below:
123
123
 
124
124
  ### Example
125
125
 
126
- class Event < Ohm::Model
127
- attribute :name
128
- reference :venue, Venue
129
- set :participants, Person
130
- counter :votes
126
+ ```ruby
127
+ class Event < Ohm::Model
128
+ attribute :name
129
+ reference :venue, Venue
130
+ set :participants, Person
131
+ counter :votes
131
132
 
132
- index :name
133
+ index :name
133
134
 
134
- def validate
135
- assert_present :name
136
- end
137
- end
135
+ def validate
136
+ assert_present :name
137
+ end
138
+ end
138
139
 
139
- class Venue < Ohm::Model
140
- attribute :name
141
- collection :events, Event
142
- end
140
+ class Venue < Ohm::Model
141
+ attribute :name
142
+ collection :events, Event
143
+ end
143
144
 
144
- class Person < Ohm::Model
145
- attribute :name
146
- end
145
+ class Person < Ohm::Model
146
+ attribute :name
147
+ end
148
+ ```
147
149
 
148
150
  All models have the `id` attribute built in, you don't need to declare it.
149
151
 
150
152
  This is how you interact with IDs:
151
153
 
152
- event = Event.create :name => "Ohm Worldwide Conference 2031"
153
- event.id
154
- # => 1
154
+ ```ruby
155
+ event = Event.create :name => "Ohm Worldwide Conference 2031"
156
+ event.id
157
+ # => 1
155
158
 
156
- # Find an event by id
157
- event == Event[1]
158
- # => true
159
+ # Find an event by id
160
+ event == Event[1]
161
+ # => true
159
162
 
160
- # Trying to find a non existent event
161
- Event[2]
162
- # => nil
163
+ # Trying to find a non existent event
164
+ Event[2]
165
+ # => nil
163
166
 
164
- # Finding all the events
165
- Event.all
166
- # => [#<Event @values={:id=>1, :name=>"Ohm Worldwide Conference 2031"}>]
167
+ # Finding all the events
168
+ Event.all
169
+ # => [#<Event @values={:id=>1, :name=>"Ohm Worldwide Conference 2031"}>]
170
+ ```
167
171
 
168
172
  This example shows some basic features, like attribute declarations and
169
173
  validations. Keep reading to find out what you can do with models.
@@ -233,35 +237,43 @@ For most use cases, this pattern doesn't represent a problem.
233
237
  If you need to check for validity before operating on lists, sets or
234
238
  counters, you can use this pattern:
235
239
 
236
- if event.valid?
237
- event.comments << Comment.create(:body => "Great event!")
238
- end
240
+ ```ruby
241
+ if event.valid?
242
+ event.comments.add(Comment.create(:body => "Great event!"))
243
+ end
244
+ ```
239
245
 
240
246
  If you are saving the object, this will suffice:
241
247
 
242
- if event.save
243
- event.comments << Comment.create(:body => "Wonderful event!")
244
- end
248
+ ```ruby
249
+ if event.save
250
+ event.comments.add(Comment.create(:body => "Wonderful event!"))
251
+ end
252
+ ```
245
253
 
246
254
  Working with Sets
247
255
  -----------------
248
256
 
249
257
  Given the following model declaration:
250
258
 
251
- class Event < Ohm::Model
252
- attribute :name
253
- set :attendees, Person
254
- end
259
+ ```ruby
260
+ class Event < Ohm::Model
261
+ attribute :name
262
+ set :attendees, Person
263
+ end
264
+ ```
255
265
 
256
266
  You can add instances of `Person` to the set of attendees with the
257
- `<<` method:
267
+ `add` method:
258
268
 
259
- event.attendees << Person.create(:name => "Albert")
269
+ ```ruby
270
+ event.attendees.add(Person.create(:name => "Albert"))
260
271
 
261
- # And now...
262
- event.attendees.each do |person|
263
- # ...do what you want with this person.
264
- end
272
+ # And now...
273
+ event.attendees.each do |person|
274
+ # ...do what you want with this person.
275
+ end
276
+ ```
265
277
 
266
278
  ## Sorting
267
279
 
@@ -275,22 +287,25 @@ order. Both methods receive an options hash which is explained below:
275
287
 
276
288
  Order direction and strategy. You can pass in any of the following:
277
289
 
278
- 1. ASC
279
- 2. ASC ALPHA (or ALPHA ASC)
280
- 3. DESC
281
- 4. DESC ALPHA (or ALPHA DESC)
290
+ 1. ASC
291
+ 2. ASC ALPHA (or ALPHA ASC)
292
+ 3. DESC
293
+ 4. DESC ALPHA (or ALPHA DESC)
282
294
 
283
295
  It defaults to `ASC`.
284
296
 
285
- ### :start
297
+ __Important Note:__ Starting with Redis 2.6, `ASC` and `DESC` only
298
+ work with integers or floating point data types. If you need to sort
299
+ by an alphanumeric field, add the `ALPHA` keyword.
300
+
301
+ ### :limit
286
302
 
287
- The offset from which we should start with. Note that
303
+ The offset and limit from which we should start with. Note that
288
304
  this is 0-indexed. It defaults to `0`.
289
305
 
290
- ### :limit
306
+ Example:
291
307
 
292
- The number of entries to get. If you don't pass in anything, it will
293
- get all the results from the LIST or SET that you are sorting.
308
+ `limit: [0, 10]` will get the first 10 entries starting from offset 0.
294
309
 
295
310
  ### :by
296
311
 
@@ -300,8 +315,14 @@ using {Ohm::Model::Collection#sort sort} and
300
315
  converts the passed argument with the assumption that it is a hash key
301
316
  and it's within the current model you are sorting.
302
317
 
303
- Post.all.sort_by(:title) # SORT Post:all BY Post:*->title
304
- Post.all.sort(:by => :title) # SORT Post:all BY title
318
+ ```ruby
319
+ Post.all.sort_by(:title) # SORT Post:all BY Post:*->title
320
+ Post.all.sort(:by => :title) # SORT Post:all BY title
321
+ ```
322
+
323
+ __Tip:__ Unless you absolutely know what you're doing, use `sort`
324
+ when you want to sort your models by their `id`, and use `sort_by`
325
+ otherwise.
305
326
 
306
327
  ### :get
307
328
 
@@ -310,34 +331,13 @@ the `:by` option, using {Ohm::Model::Collection#sort sort} and
310
331
  {Ohm::Model::Collection#sort_by sort_by} has distinct differences in
311
332
  that `sort_by` does much of the hand-coding for you.
312
333
 
313
- Post.all.sort_by(:title, :get => :title)
314
- # SORT Post:all BY Post:*->title GET Post:*->title
315
-
316
- Post.all.sort(:by => :title, :get => :title)
317
- # SORT Post:all BY title GET title
318
-
319
-
320
- ### :store
321
-
322
- An optional key which you may use to cache the sorted result. The key
323
- may or may not exist.
324
-
325
- This option can only be used together with `:get`.
334
+ ```ruby
335
+ Post.all.sort_by(:title, :get => :title)
336
+ # SORT Post:all BY Post:*->title GET Post:*->title
326
337
 
327
- The type that is used for the STORE key is a LIST.
328
-
329
- Post.all.sort_by(:title, :store => "FOO")
330
-
331
- # Get all the results stored in FOO.
332
- Post.db.lrange("FOO", 0, -1)
333
-
334
- When using temporary values, it might be a good idea to use a `volatile`
335
- key. In Ohm, a volatile key means it just starts with a `~` character.
336
-
337
- Post.all.sort_by(:title, :get => :title,
338
- :store => Post.key.volatile["FOO"])
339
-
340
- Post.key.volatile["FOO"].lrange 0, -1
338
+ Post.all.sort(:by => :title, :get => :title)
339
+ # SORT Post:all BY title GET title
340
+ ```
341
341
 
342
342
 
343
343
  Associations
@@ -345,16 +345,18 @@ Associations
345
345
 
346
346
  Ohm lets you declare `references` and `collections` to represent associations.
347
347
 
348
- class Post < Ohm::Model
349
- attribute :title
350
- attribute :body
351
- collection :comments, Comment
352
- end
348
+ ```ruby
349
+ class Post < Ohm::Model
350
+ attribute :title
351
+ attribute :body
352
+ collection :comments, Comment
353
+ end
353
354
 
354
- class Comment < Ohm::Model
355
- attribute :body
356
- reference :post, Post
357
- end
355
+ class Comment < Ohm::Model
356
+ attribute :body
357
+ reference :post, Post
358
+ end
359
+ ```
358
360
 
359
361
  After this, every time you refer to `post.comments` you will be talking
360
362
  about instances of the model `Comment`. If you want to get a list of IDs
@@ -365,20 +367,22 @@ you can use `post.comments.key.smembers`.
365
367
  Doing a {Ohm::Model.reference reference} is actually just a shortcut for
366
368
  the following:
367
369
 
368
- # Redefining our model above
369
- class Comment < Ohm::Model
370
- attribute :body
371
- attribute :post_id
372
- index :post_id
370
+ ```ruby
371
+ # Redefining our model above
372
+ class Comment < Ohm::Model
373
+ attribute :body
374
+ attribute :post_id
375
+ index :post_id
373
376
 
374
- def post=(post)
375
- self.post_id = post.id
376
- end
377
+ def post=(post)
378
+ self.post_id = post.id
379
+ end
377
380
 
378
- def post
379
- Post[post_id]
380
- end
381
- end
381
+ def post
382
+ Post[post_id]
383
+ end
384
+ end
385
+ ```
382
386
 
383
387
  The only difference with the actual implementation is that the model
384
388
  is memoized.
@@ -386,8 +390,9 @@ is memoized.
386
390
  The net effect here is we can conveniently set and retrieve `Post` objects,
387
391
  and also search comments using the `post_id` index.
388
392
 
389
- Comment.find(:post_id => 1)
390
-
393
+ ```ruby
394
+ Comment.find(:post_id => 1)
395
+ ```
391
396
 
392
397
  ### Collections explained
393
398
 
@@ -397,34 +402,38 @@ just a macro that defines a finder for you, and we know that to find a model
397
402
  by a field requires an {Ohm::Model.index index} to be defined for the field
398
403
  you want to search.
399
404
 
400
- # Redefining our post above
401
- class Post < Ohm::Model
402
- attribute :title
403
- attribute :body
405
+ ```ruby
406
+ # Redefining our post above
407
+ class Post < Ohm::Model
408
+ attribute :title
409
+ attribute :body
404
410
 
405
- def comments
406
- Comment.find(:post_id => self.id)
407
- end
408
- end
411
+ def comments
412
+ Comment.find(:post_id => self.id)
413
+ end
414
+ end
415
+ ```
409
416
 
410
417
  The only "magic" happening is with the inference of the `index` that was used
411
418
  in the other model. The following all produce the same effect:
412
419
 
413
- # easiest, with the basic assumption that the index is `:post_id`
414
- collection :comments, Comment
420
+ ```ruby
421
+ # easiest, with the basic assumption that the index is `:post_id`
422
+ collection :comments, Comment
415
423
 
416
- # we can explicitly declare this as follows too:
417
- collection :comments, Comment, :post
424
+ # we can explicitly declare this as follows too:
425
+ collection :comments, Comment, :post
418
426
 
419
- # finally, we can use the default argument for the third parameter which
420
- # is `to_reference`.
421
- collection :comments, Comment, to_reference
427
+ # finally, we can use the default argument for the third parameter which
428
+ # is `to_reference`.
429
+ collection :comments, Comment, to_reference
422
430
 
423
- # exploring `to_reference` reveals a very interesting and simple concept:
424
- Post.to_reference == :post
425
- # => true
431
+ # exploring `to_reference` reveals a very interesting and simple concept:
432
+ Post.to_reference == :post
433
+ # => true
434
+ ```
426
435
 
427
- Indexes
436
+ Indices
428
437
  -------
429
438
 
430
439
  An {Ohm::Model.index index} is a set that's handled automatically by Ohm. For
@@ -442,28 +451,54 @@ validation and the methods {Ohm::Model::Set#find find} and
442
451
 
443
452
  You can find a collection of records with the `find` method:
444
453
 
445
- # This returns a collection of users with the username "Albert"
446
- User.find(:username => "Albert")
454
+ ```ruby
455
+ # This returns a collection of users with the username "Albert"
456
+ User.find(:username => "Albert")
457
+ ```
447
458
 
448
459
  ### Filtering results
449
460
 
450
- # Find all users from Argentina
451
- User.find(:country => "Argentina")
461
+ ```ruby
462
+ # Find all users from Argentina
463
+ User.find(:country => "Argentina")
452
464
 
453
- # Find all activated users from Argentina
454
- User.find(:country => "Argentina", :status => "activated")
465
+ # Find all activated users from Argentina
466
+ User.find(:country => "Argentina", :status => "activated")
455
467
 
456
- # Find all users from Argentina, except those with a suspended account.
457
- User.find(:country => "Argentina").except(:status => "suspended")
468
+ # Find all users from Argentina, except those with a suspended account.
469
+ User.find(:country => "Argentina").except(:status => "suspended")
458
470
 
459
- # Find all users both from Argentina and Uruguay
460
- User.find(:country => "Argentina").union(:country => "Uruguay")
471
+ # Find all users both from Argentina and Uruguay
472
+ User.find(:country => "Argentina").union(:country => "Uruguay")
473
+ ```
461
474
 
462
475
  Note that calling these methods results in new sets being created
463
476
  on the fly. This is important so that you can perform further operations
464
477
  before reading the items to the client.
465
478
 
466
- For more information, see [SINTERSTORE](http://redis.io/commands/sinterstore) and [SDIFFSTORE](http://redis.io/commands/sdiffstore).
479
+ For more information, see [SINTERSTORE](http://redis.io/commands/sinterstore),
480
+ [SDIFFSTORE](http://redis.io/commands/sdiffstore) and
481
+ [SUNIONSTORE](http://redis.io/commands/sunionstore)
482
+
483
+ Uniques
484
+ -------
485
+
486
+ Uniques are similar to indices except that there can only be one record per
487
+ entry. The canonical example of course would be the email of your user, e.g.
488
+
489
+ ```ruby
490
+ class User < Ohm::Model
491
+ attribute :email
492
+ unique :email
493
+ end
494
+
495
+ u = User.create(email: "foo@bar.com")
496
+ u == User.with(:email, "foo@bar.com")
497
+ # => true
498
+
499
+ User.create(email: "foo@bar.com")
500
+ # => raises Ohm::UniqueIndexViolation
501
+ ```
467
502
 
468
503
  Validations
469
504
  -----------
@@ -486,37 +521,38 @@ The `assert` method is used by all the other assertions. It pushes the
486
521
  second parameter to the list of errors if the first parameter evaluates
487
522
  to false.
488
523
 
489
- def assert(value, error)
490
- value or errors.push(error) && false
491
- end
524
+ ```ruby
525
+ def assert(value, error)
526
+ value or errors.push(error) && false
527
+ end
528
+ ```
492
529
 
493
530
  ### assert_present
494
531
 
495
532
  Checks that the given field is not nil or empty. The error code for this
496
- assertion is :not_present.
533
+ assertion is `:not_present`.
497
534
 
498
- assert_present :name
535
+ ```ruby
536
+ assert_present :name
537
+ ```
499
538
 
500
539
  ### assert_format
501
540
 
502
541
  Checks that the given field matches the provided format. The error code
503
542
  for this assertion is :format.
504
543
 
505
- assert_format :username, /^\w+$/
544
+ ```ruby
545
+ assert_format :username, /^\w+$/
546
+ ```
506
547
 
507
548
  ### assert_numeric
508
549
 
509
550
  Checks that the given field holds a number as a Fixnum or as a string
510
551
  representation. The error code for this assertion is :not_numeric.
511
552
 
512
- assert_numeric :votes
513
-
514
- ### assert_unique
515
-
516
- Validates that the attribute or array of attributes are unique.
517
- For this, an index of the same kind must exist. The error code is :not_unique.
518
-
519
- assert_unique :email
553
+ ```ruby
554
+ assert_numeric :votes
555
+ ```
520
556
 
521
557
  Errors
522
558
  ------
@@ -529,60 +565,37 @@ was issued and the error code.
529
565
 
530
566
  Given the following example:
531
567
 
532
- def validate
533
- assert_present :foo
534
- assert_numeric :bar
535
- assert_format :baz, /^\d{2}$/
536
- assert_unique :qux
537
- end
568
+ ```ruby
569
+ def validate
570
+ assert_present :foo
571
+ assert_numeric :bar
572
+ assert_format :baz, /^\d{2}$/
573
+ end
574
+ ```
538
575
 
539
576
  If all the assertions fail, the following errors will be present:
540
577
 
541
- obj.errors
542
- # => [[:foo, :not_present], [:bar, :not_numeric], [:baz, :format], [:qux, :not_unique]]
543
-
544
- Presenting errors
545
- -----------------
546
-
547
- Unlike other ORMs, that define the full error messages in the model
548
- itself, Ohm encourages you to define the error messages outside. If
549
- you are using Ohm in the context of a web framework, the views are the
550
- proper place to write the error messages.
551
-
552
- Ohm provides a presenter that helps you in this quest. The basic usage
553
- is as follows:
554
-
555
- error_messages = @model.errors.present do |e|
556
- e.on [:name, :not_present], "Name must be present"
557
- e.on [:account, :not_present], "You must supply an account"
558
- end
559
-
560
- error_messages
561
- # => ["Name must be present", "You must supply an account"]
562
-
563
- Having the error message definitions in the views means you can use any
564
- sort of helpers. You can also use blocks instead of strings for the
565
- values. The result of the block is used as the error message:
566
-
567
- error_messages = @model.errors.present do |e|
568
- e.on [:email, :not_unique] do
569
- "The email #{@model.email} is already registered."
570
- end
571
- end
572
-
573
- error_messages
574
- # => ["The email foo@example.com is already registered."]
578
+ ```ruby
579
+ obj.errors
580
+ # => { foo: [:not_present], bar: [:not_numeric], baz: [:format] }
581
+ ```
575
582
 
576
583
  Ohm Extensions
577
584
  ==============
578
585
 
579
586
  Ohm is rather small and can be extended in many ways.
580
587
 
581
- A lot of amazing contributions are available at [Ohm Contrib](http://cyx.github.com/ohm-contrib/doc/), make sure to check them if you need to extend Ohm's functionality.
588
+ A lot of amazing contributions are available at [Ohm Contrib][contrib]
589
+ make sure to check them if you need to extend Ohm's functionality.
590
+
591
+ [contrib]: http://cyx.github.com/ohm-contrib/doc/,
582
592
 
583
593
  Tutorials
584
594
  =========
585
595
 
596
+ NOTE: These tutorials were written against Ohm 0.1.x. Please give us
597
+ a while to fully update all of them.
598
+
586
599
  Check the examples to get a feeling of the design patterns for Redis.
587
600
 
588
601
  1. [Activity Feed](http://ohm.keyvalue.org/examples/activity-feed.html)
@@ -594,11 +607,12 @@ Check the examples to get a feeling of the design patterns for Redis.
594
607
  7. [Slugs and permalinks](http://ohm.keyvalue.org/examples/slug.html)
595
608
  8. [Tagging](http://ohm.keyvalue.org/examples/tagging.html)
596
609
 
610
+
597
611
  Versions
598
612
  ========
599
613
 
600
- Ohm uses features from Redis > 1.3.10. If you are stuck in previous
601
- versions, please use Ohm 0.0.35 instead.
614
+ Ohm uses features from Redis > 2.6.x. If you are stuck in previous
615
+ versions, please use Ohm 0.1.x instead.
602
616
 
603
617
  Upgrading from 0.0.x to 0.1
604
618
  ---------------------------
@@ -607,11 +621,13 @@ Since Ohm 0.1 changes the persistence strategy (from 1-key-per-attribute
607
621
  to Hashes), you'll need to run a script to upgrade your old data set.
608
622
  Fortunately, it is built in:
609
623
 
610
- require "ohm/utils/upgrade"
624
+ ```ruby
625
+ require "ohm/utils/upgrade"
611
626
 
612
- Ohm.connect :port => 6380
627
+ Ohm.connect :port => 6380
613
628
 
614
- Ohm::Utils::Upgrade.new([:User, :Post, :Comment]).run
629
+ Ohm::Utils::Upgrade.new([:User, :Post, :Comment]).run
630
+ ```
615
631
 
616
632
  Yes, you need to provide the model names. The good part is that you
617
633
  don't have to load your application environment. Since we assume it's
data/lib/ohm.rb CHANGED
@@ -267,45 +267,145 @@ module Ohm
267
267
 
268
268
  def fetch(ids)
269
269
  arr = model.db.pipelined do
270
- ids.each { |id| namespace[id].hgetall }
270
+ ids.each { |id| model.db.hgetall(namespace[id]) }
271
271
  end
272
272
 
273
273
  return [] if arr.nil?
274
274
 
275
275
  arr.map.with_index do |atts, idx|
276
- model.new(atts.update(id: ids[idx]))
276
+ model.new(Hash[*atts].update(id: ids[idx]))
277
277
  end
278
278
  end
279
279
  end
280
280
 
281
- class Set < Struct.new(:key, :namespace, :model)
282
- include Collection
281
+ class List < Struct.new(:key, :namespace, :model)
282
+ include Enumerable
283
283
 
284
- # Add a model directly to the set.
284
+ # Returns the total size of the list using LLEN.
285
+ def size
286
+ key.llen
287
+ end
288
+
289
+ # Returns the first element of the list using LINDEX.
290
+ def first
291
+ model[key.lindex(0)]
292
+ end
293
+
294
+ # Returns the last element of the list using LINDEX.
295
+ def last
296
+ model[key.lindex(-1)]
297
+ end
298
+
299
+ # Checks if the model is part of this List.
300
+ #
301
+ # An important thing to note is that this method loads all of the
302
+ # elements of the List since there is no command in Redis that
303
+ # allows you to actually check the list contents efficiently.
304
+ #
305
+ # You may want to avoid doing this if your list has say, 10K entries.
306
+ def include?(model)
307
+ ids.include?(model.id.to_s)
308
+ end
309
+
310
+ # Replace all the existing elements of a list with a different
311
+ # collection of models. This happens atomically in a MULTI-EXEC
312
+ # block.
285
313
  #
286
314
  # Example:
287
315
  #
288
316
  # user = User.create
289
- # post = Post.create
317
+ # p1 = Post.create
318
+ # user.posts.push(p1)
290
319
  #
291
- # user.posts.add(post)
320
+ # p2, p3 = Post.create, Post.create
321
+ # user.posts.replace([p2, p3])
292
322
  #
293
- def add(model)
294
- key.sadd(model.id)
323
+ # user.posts.include?(p1)
324
+ # # => false
325
+ #
326
+ def replace(models)
327
+ ids = models.map { |model| model.id }
328
+
329
+ model.db.multi do
330
+ key.del
331
+ ids.each { |id| key.rpush(id) }
332
+ end
295
333
  end
296
334
 
297
- # Remove a model directly from the set.
335
+ # Fetch the data from Redis in one go.
336
+ def to_a
337
+ fetch(ids)
338
+ end
339
+
340
+ def each
341
+ to_a.each { |element| yield element }
342
+ end
343
+
344
+ def empty?
345
+ size == 0
346
+ end
347
+
348
+ # Pushes the model to the _end_ of the list using RPUSH.
349
+ def push(model)
350
+ key.rpush(model.id)
351
+ end
352
+
353
+ # Pushes the model to the _beginning_ of the list using LPUSH.
354
+ def unshift(model)
355
+ key.lpush(model.id)
356
+ end
357
+
358
+ # Delete a model from the list.
359
+ #
360
+ # Note: If your list contains the model multiple times, this method
361
+ # will delete all instances of that model in one go.
298
362
  #
299
363
  # Example:
300
364
  #
301
- # user = User.create
302
- # post = Post.create
365
+ # class Comment < Ohm::Model
366
+ # end
303
367
  #
304
- # user.posts.delete(post)
368
+ # class Post < Ohm::Model
369
+ # list :comments, Comment
370
+ # end
371
+ #
372
+ # p = Post.create
373
+ # c = Comment.create
374
+ #
375
+ # p.comments.push(c)
376
+ # p.comments.push(c)
377
+ #
378
+ # p.comments.delete(c)
379
+ #
380
+ # p.comments.size == 0
381
+ # # => true
305
382
  #
306
383
  def delete(model)
307
- key.srem(model.id)
384
+ # LREM key 0 <id> means remove all elements matching <id>
385
+ # @see http://redis.io/commands/lrem
386
+ key.lrem(0, model.id)
387
+ end
388
+
389
+ private
390
+ def ids
391
+ key.lrange(0, -1)
392
+ end
393
+
394
+ def fetch(ids)
395
+ arr = model.db.pipelined do
396
+ ids.each { |id| model.db.hgetall(namespace[id]) }
397
+ end
398
+
399
+ return [] if arr.nil?
400
+
401
+ arr.map.with_index do |atts, idx|
402
+ model.new(Hash[*atts].update(id: ids[idx]))
403
+ end
308
404
  end
405
+ end
406
+
407
+ class Set < Struct.new(:key, :namespace, :model)
408
+ include Collection
309
409
 
310
410
  # Chain new fiters on an existing set.
311
411
  #
@@ -349,6 +449,39 @@ module Ohm
349
449
  MultiSet.new([key], namespace, model).union(dict)
350
450
  end
351
451
 
452
+ private
453
+ def execute
454
+ yield key
455
+ end
456
+ end
457
+
458
+ class MutableSet < Set
459
+ # Add a model directly to the set.
460
+ #
461
+ # Example:
462
+ #
463
+ # user = User.create
464
+ # post = Post.create
465
+ #
466
+ # user.posts.add(post)
467
+ #
468
+ def add(model)
469
+ key.sadd(model.id)
470
+ end
471
+
472
+ # Remove a model directly from the set.
473
+ #
474
+ # Example:
475
+ #
476
+ # user = User.create
477
+ # post = Post.create
478
+ #
479
+ # user.posts.delete(post)
480
+ #
481
+ def delete(model)
482
+ key.srem(model.id)
483
+ end
484
+
352
485
  # Replace all the existing elements of a set with a different
353
486
  # collection of models. This happens atomically in a MULTI-EXEC
354
487
  # block.
@@ -373,13 +506,9 @@ module Ohm
373
506
  ids.each { |id| key.sadd(id) }
374
507
  end
375
508
  end
376
-
377
- private
378
- def execute
379
- yield key
380
- end
381
509
  end
382
510
 
511
+
383
512
  # Anytime you filter a set with more than one requirement, you
384
513
  # internally use a `MultiSet`. `MutiSet` is a bit slower than just
385
514
  # a `Set` because it has to `SINTERSTORE` all the keys prior to
@@ -693,7 +822,37 @@ module Ohm
693
822
  define_method name do
694
823
  model = Utils.const(self.class, model)
695
824
 
696
- Ohm::Set.new(key[name], model.key, model)
825
+ Ohm::MutableSet.new(key[name], model.key, model)
826
+ end
827
+ end
828
+
829
+ # Declare an Ohm::List with the given name.
830
+ #
831
+ # Example:
832
+ #
833
+ # class Comment < Ohm::Model
834
+ # end
835
+ #
836
+ # class Post < Ohm::Model
837
+ # list :comments, :Comment
838
+ # end
839
+ #
840
+ # p = Post.create
841
+ # p.comments.push(Comment.create)
842
+ # p.comments.unshift(Comment.create)
843
+ # p.comments.size == 2
844
+ # # => true
845
+ #
846
+ # Note: You can't use the list until you save the model. If you try
847
+ # to do it, you'll receive an Ohm::MissingID error.
848
+ #
849
+ def self.list(name, model)
850
+ collections << name unless collections.include?(name)
851
+
852
+ define_method name do
853
+ model = Utils.const(self.class, model)
854
+
855
+ Ohm::List.new(key[name], model.key, model)
697
856
  end
698
857
  end
699
858
 
@@ -1027,11 +1186,6 @@ module Ohm
1027
1186
  return attrs
1028
1187
  end
1029
1188
 
1030
- # Export a JSON representation of the model by encoding `to_hash`.
1031
- def to_json(*args)
1032
- to_hash.to_json(*args)
1033
- end
1034
-
1035
1189
  # Persist the model attributes and update indices and unique
1036
1190
  # indices. The `counter`s and `set`s are not touched during save.
1037
1191
  #
@@ -1210,6 +1364,8 @@ module Ohm
1210
1364
  atts.each do |att, val|
1211
1365
  ret[att] = send(att).to_s unless val.to_s.empty?
1212
1366
  end
1367
+
1368
+ throw :empty if ret.empty?
1213
1369
  end
1214
1370
  end
1215
1371
 
@@ -1218,8 +1374,10 @@ module Ohm
1218
1374
  end
1219
1375
 
1220
1376
  def _save
1221
- key.del
1222
- key.hmset(*_skip_empty(attributes).flatten)
1377
+ catch :empty do
1378
+ key.del
1379
+ key.hmset(*_skip_empty(attributes).flatten)
1380
+ end
1223
1381
  end
1224
1382
 
1225
1383
  def _verify_uniques
data/lib/ohm/json.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "json"
2
+
3
+ module Ohm
4
+ class Model
5
+ # Export a JSON representation of the model by encoding `to_hash`.
6
+ def to_json(*args)
7
+ to_hash.to_json(*args)
8
+ end
9
+ end
10
+
11
+ module Collection
12
+ # Sugar for to_a.to_json for all types of Sets
13
+ def to_json(*args)
14
+ to_a.to_json(*args)
15
+ end
16
+ end
17
+
18
+ class List
19
+ # Sugar for to_a.to_json for lists.
20
+ def to_json(*args)
21
+ to_a.to_json(*args)
22
+ end
23
+ end
24
+ end
@@ -46,7 +46,7 @@ module Ohm
46
46
  class EntryAlreadyExistsError < ::RuntimeError
47
47
  end
48
48
 
49
- def method_missing(writer, value)
49
+ def method_missing(writer, value = nil)
50
50
  super unless writer[-1] == "="
51
51
 
52
52
  reader = writer[0..-2].to_sym
@@ -69,14 +69,14 @@ module Ohm
69
69
  attr :phase
70
70
 
71
71
  def initialize
72
- @phase = Hash.new { |h, k| h[k] = ::Set.new }
72
+ @phase = Hash.new { |h, k| h[k] = Array.new }
73
73
 
74
74
  yield self if block_given?
75
75
  end
76
76
 
77
77
  def append(t)
78
78
  t.phase.each do |key, values|
79
- phase[key].merge(values)
79
+ phase[key].concat(values - phase[key])
80
80
  end
81
81
 
82
82
  self
data/test/connection.rb CHANGED
@@ -4,13 +4,27 @@ require File.expand_path("./helper", File.dirname(__FILE__))
4
4
 
5
5
  prepare.clear
6
6
 
7
+ test "no rewriting of settings hash when using Ohm.connect" do
8
+ settings = { url: "redis://127.0.0.1:6379/15" }.freeze
9
+
10
+ ex = nil
11
+
12
+ begin
13
+ Ohm.connect(settings)
14
+ rescue RuntimeError => e
15
+ ex = e
16
+ end
17
+
18
+ assert_equal ex, nil
19
+ end
20
+
7
21
  test "connects lazily" do
8
22
  Ohm.connect(:port => 9876)
9
23
 
10
24
  begin
11
25
  Ohm.redis.get "foo"
12
26
  rescue => e
13
- assert_equal Redis::CannotConnectError, e.class
27
+ assert_equal Errno::ECONNREFUSED, e.class
14
28
  end
15
29
  end
16
30
 
@@ -40,7 +54,7 @@ test "supports connecting by URL" do
40
54
  begin
41
55
  Ohm.redis.get "foo"
42
56
  rescue => e
43
- assert_equal Redis::CannotConnectError, e.class
57
+ assert_equal Errno::ECONNREFUSED, e.class
44
58
  end
45
59
  end
46
60
 
@@ -54,6 +68,22 @@ test "connection class" do
54
68
  assert conn.redis.kind_of?(Redis)
55
69
  end
56
70
 
71
+ test "issue #46" do
72
+ class B < Ohm::Model
73
+ connect(:url => "redis://localhost:6379/15")
74
+ end
75
+
76
+ # We do this since we did prepare.clear above.
77
+ B.db.flushall
78
+
79
+ b1, b2 = nil, nil
80
+
81
+ Thread.new { b1 = B.create }.join
82
+ Thread.new { b2 = B.create }.join
83
+
84
+ assert_equal [b1, b2], B.all.sort.to_a
85
+ end
86
+
57
87
  test "model can define its own connection" do
58
88
  class B < Ohm::Model
59
89
  connect(:url => "redis://localhost:6379/1")
data/test/filtering.rb CHANGED
@@ -24,6 +24,16 @@ test "findability" do |john, jane|
24
24
  assert User.find(lname: "Doe", fname: "Jane").include?(jane)
25
25
  end
26
26
 
27
+ test "sets aren't mutable" do |john, jane|
28
+ assert_raise NoMethodError do
29
+ User.find(lname: "Doe").add(john)
30
+ end
31
+
32
+ assert_raise NoMethodError do
33
+ User.find(lname: "Doe", fname: "John").add(john)
34
+ end
35
+ end
36
+
27
37
  test "#first" do |john, jane|
28
38
  set = User.find(lname: "Doe", status: "active")
29
39
 
data/test/helper.rb CHANGED
@@ -21,5 +21,5 @@ $VERBOSE = true
21
21
  require "ohm"
22
22
 
23
23
  prepare do
24
- Ohm.flush
24
+ Ohm.redis.flushall
25
25
  end
data/test/json.rb CHANGED
@@ -3,9 +3,11 @@
3
3
  require File.expand_path("./helper", File.dirname(__FILE__))
4
4
 
5
5
  require "json"
6
+ require "ohm/json"
6
7
 
7
8
  class Venue < Ohm::Model
8
9
  attribute :name
10
+ list :programmers, :Programmer
9
11
 
10
12
  def validate
11
13
  assert_present :name
@@ -70,5 +72,15 @@ test "export an array of records to json" do
70
72
  Programmer.create(language: "Python")
71
73
 
72
74
  expected = [{ id: "1", language: "Ruby" }, { id: "2", language: "Python"}].to_json
73
- assert_equal expected, Programmer.all.to_a.to_json
75
+ assert_equal expected, Programmer.all.to_json
76
+ end
77
+
78
+ test "export an array of lists to json" do
79
+ venue = Venue.create(name: "Foo")
80
+
81
+ venue.programmers.push(Programmer.create(language: "Ruby"))
82
+ venue.programmers.push(Programmer.create(language: "Python"))
83
+
84
+ expected = [{ id: "1", language: "Ruby" }, { id: "2", language: "Python"}].to_json
85
+ assert_equal expected, venue.programmers.to_json
74
86
  end
data/test/list.rb ADDED
@@ -0,0 +1,69 @@
1
+ require File.expand_path("./helper", File.dirname(__FILE__))
2
+
3
+ class Post < Ohm::Model
4
+ list :comments, :Comment
5
+ end
6
+
7
+ class Comment < Ohm::Model
8
+ end
9
+
10
+ setup do
11
+ post = Post.create
12
+
13
+ post.comments.push(c1 = Comment.create)
14
+ post.comments.push(c2 = Comment.create)
15
+ post.comments.push(c3 = Comment.create)
16
+
17
+ [post, c1, c2, c3]
18
+ end
19
+
20
+ test "include?" do |p, c1, c2, c3|
21
+ assert p.comments.include?(c1)
22
+ assert p.comments.include?(c2)
23
+ assert p.comments.include?(c3)
24
+ end
25
+
26
+ test "first / last / size / empty?" do |p, c1, c2, c3|
27
+ assert_equal 3, p.comments.size
28
+ assert_equal c1, p.comments.first
29
+ assert_equal c3, p.comments.last
30
+ assert ! p.comments.empty?
31
+ end
32
+
33
+ test "replace" do |p, c1, c2, c3|
34
+ c4 = Comment.create
35
+
36
+ p.comments.replace([c4])
37
+
38
+ assert_equal [c4], p.comments.to_a
39
+ end
40
+
41
+ test "push / unshift" do |p, c1, c2, c3|
42
+ c4 = Comment.create
43
+ c5 = Comment.create
44
+
45
+ p.comments.unshift(c4)
46
+ p.comments.push(c5)
47
+
48
+ assert_equal c4, p.comments.first
49
+ assert_equal c5, p.comments.last
50
+ end
51
+
52
+ test "delete" do |p, c1, c2, c3|
53
+ p.comments.delete(c1)
54
+ assert_equal 2, p.comments.size
55
+ assert ! p.comments.include?(c1)
56
+
57
+ p.comments.delete(c2)
58
+ assert_equal 1, p.comments.size
59
+ assert ! p.comments.include?(c2)
60
+
61
+ p.comments.delete(c3)
62
+ assert p.comments.empty?
63
+ end
64
+
65
+ test "deleting main model cleans up the collection" do |p, _, _, _|
66
+ p.delete
67
+
68
+ assert ! Ohm.redis.exists(p.key[:comments])
69
+ end
data/test/model.rb CHANGED
@@ -51,6 +51,28 @@ class Meetup < Ohm::Model
51
51
  end
52
52
  end
53
53
 
54
+ class Invoice < Ohm::Model
55
+ def _initialize_id
56
+ @id = "_custom_id"
57
+ end
58
+ end
59
+
60
+ test "customized ID" do
61
+ inv = Invoice.create
62
+ assert_equal "_custom_id", inv.id
63
+
64
+ i = Invoice.create(id: "_diff_id")
65
+ assert_equal "_diff_id", i.id
66
+ assert_equal i, Invoice["_diff_id"]
67
+ end
68
+
69
+ test "empty model is ok" do
70
+ class Foo < Ohm::Model
71
+ end
72
+
73
+ foo = Foo.create
74
+ end
75
+
54
76
  test "counters are cleaned up during deletion" do
55
77
  e = Event.create(name: "Foo")
56
78
  e.incr :votes, 10
data/test/transactions.rb CHANGED
@@ -95,7 +95,7 @@ test "composed transaction" do |db|
95
95
 
96
96
  assert_equal "bar", db.get("foo")
97
97
 
98
- assert_equal Set.new(["foo"]), t5.phase[:watch]
98
+ assert_equal ["foo"], t5.phase[:watch]
99
99
  assert_equal 2, t5.phase[:write].size
100
100
  end
101
101
 
@@ -173,6 +173,18 @@ test "storage in composed transactions" do |db|
173
173
  assert_equal "enon", db.get("foo")
174
174
  end
175
175
 
176
+ test "reading an storage entries that doesn't exist raises" do |db|
177
+ t1 = Ohm::Transaction.new do |t|
178
+ t.read do |s|
179
+ s.foo
180
+ end
181
+ end
182
+
183
+ assert_raise NoMethodError do
184
+ t1.commit(db)
185
+ end
186
+ end
187
+
176
188
  test "storage entries can't be overriden" do |db|
177
189
  t1 = Ohm::Transaction.new do |t|
178
190
  t.read do |s|
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: 1.0.0.alpha2
4
+ version: 1.0.0.rc1
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -10,11 +10,11 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-03-23 00:00:00.000000000 Z
13
+ date: 2012-04-03 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: nest
17
- requirement: &2156281500 !ruby/object:Gem::Requirement
17
+ requirement: &2155966540 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ~>
@@ -22,10 +22,10 @@ dependencies:
22
22
  version: '1.0'
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *2156281500
25
+ version_requirements: *2155966540
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: scrivener
28
- requirement: &2156297300 !ruby/object:Gem::Requirement
28
+ requirement: &2155963980 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ~>
@@ -33,10 +33,10 @@ dependencies:
33
33
  version: 0.0.3
34
34
  type: :runtime
35
35
  prerelease: false
36
- version_requirements: *2156297300
36
+ version_requirements: *2155963980
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: cutest
39
- requirement: &2156296800 !ruby/object:Gem::Requirement
39
+ requirement: &2155978980 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ~>
@@ -44,10 +44,10 @@ dependencies:
44
44
  version: '0.1'
45
45
  type: :development
46
46
  prerelease: false
47
- version_requirements: *2156296800
47
+ version_requirements: *2155978980
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: batch
50
- requirement: &2156296280 !ruby/object:Gem::Requirement
50
+ requirement: &2155978440 !ruby/object:Gem::Requirement
51
51
  none: false
52
52
  requirements:
53
53
  - - ~>
@@ -55,7 +55,7 @@ dependencies:
55
55
  version: 0.0.1
56
56
  type: :development
57
57
  prerelease: false
58
- version_requirements: *2156296280
58
+ version_requirements: *2155978440
59
59
  description: Ohm is a library that allows to store an object in Redis, a persistent
60
60
  key-value database. It includes an extensible list of validations and has very good
61
61
  performance.
@@ -66,10 +66,11 @@ executables: []
66
66
  extensions: []
67
67
  extra_rdoc_files: []
68
68
  files:
69
+ - lib/ohm/json.rb
69
70
  - lib/ohm/transaction.rb
70
71
  - lib/ohm/utils/upgrade.rb
71
72
  - lib/ohm.rb
72
- - README.markdown
73
+ - README.md
73
74
  - LICENSE
74
75
  - Rakefile
75
76
  - test/1.8.6_test.rb
@@ -83,6 +84,7 @@ files:
83
84
  - test/helper.rb
84
85
  - test/indices.rb
85
86
  - test/json.rb
87
+ - test/list.rb
86
88
  - test/lua-save.rb
87
89
  - test/lua.rb
88
90
  - test/model.rb
@@ -115,7 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
117
  version: 1.3.1
116
118
  requirements: []
117
119
  rubyforge_project: ohm
118
- rubygems_version: 1.8.10
120
+ rubygems_version: 1.8.11
119
121
  signing_key:
120
122
  specification_version: 3
121
123
  summary: Object-hash mapping library for Redis.