mongoid_slug 0.10.0 → 1.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ # require 'moped/bson/object_id'
2
+
3
+ module Mongoid
4
+ module Slug
5
+ class Criteria < Mongoid::Criteria
6
+ # Find the matchind document(s) in the criteria for the provided ids or slugs.
7
+ #
8
+ # If the document _ids are of the type Moped::BSON::ObjectId, and all the supplied parameters are
9
+ # convertible to Moped::BSON::ObjectId (via Moped::BSON::ObjectId#from_string), finding will be
10
+ # performed via _ids.
11
+ #
12
+ # If the document has any other type of _id field, and all the supplied parameters are of the same
13
+ # type, finding will be performed via _ids.
14
+ #
15
+ # Otherwise finding will be performed via slugs.
16
+ #
17
+ # @example Find by an id.
18
+ # criteria.find(Moped::BSON::ObjectId.new)
19
+ #
20
+ # @example Find by multiple ids.
21
+ # criteria.find([ Moped::BSON::ObjectId.new, Moped::BSON::ObjectId.new ])
22
+ #
23
+ # @example Find by a slug.
24
+ # criteria.find('some-slug')
25
+ #
26
+ # @example Find by multiple slugs.
27
+ # criteria.find([ 'some-slug', 'some-other-slug' ])
28
+ #
29
+ # @param [ Array<Object> ] args The ids or slugs to search for.
30
+ #
31
+ # @return [ Array<Document>, Document ] The matching document(s).
32
+ def find(*args)
33
+ look_like_slugs?(args.__find_args__) ? find_by_slug!(*args) : super
34
+ end
35
+
36
+ # Find the matchind document(s) in the criteria for the provided slugs.
37
+ #
38
+ # @example Find by a slug.
39
+ # criteria.find('some-slug')
40
+ #
41
+ # @example Find by multiple slugs.
42
+ # criteria.find([ 'some-slug', 'some-other-slug' ])
43
+ #
44
+ # @param [ Array<Object> ] args The slugs to search for.
45
+ #
46
+ # @return [ Array<Document>, Document ] The matching document(s).
47
+ def find_by_slug!(*args)
48
+ slugs = args.__find_args__
49
+ raise_invalid if slugs.any?(&:nil?)
50
+ for_slugs(slugs).execute_or_raise_for_slugs(slugs, args.multi_arged?)
51
+ end
52
+
53
+
54
+ # True if all supplied args look like slugs. Will only attempt to type cast for Moped::BSON::ObjectId.
55
+ # Thus '123' will be interpreted as a slug even if the _id is an Integer field, etc.
56
+ def look_like_slugs?(args)
57
+ if args.all? { |id| id.is_a?(String) }
58
+ id_type = @klass.fields['_id'].type
59
+ case
60
+ when id_type == Moped::BSON::ObjectId
61
+ args.any? { |id| !Moped::BSON::ObjectId.legal?(id) }
62
+ else args.any? { |id| id.class != id_type }
63
+ end
64
+ else
65
+ false
66
+ end
67
+ end
68
+
69
+ protected
70
+
71
+ def for_slugs(slugs)
72
+ where({ _slugs: { '$in' => slugs } }).limit(slugs.length)
73
+ end
74
+
75
+ def execute_or_raise_for_slugs(slugs, multi)
76
+ result = uniq
77
+ check_for_missing_documents_for_slugs!(result, slugs)
78
+ multi ? result : result.first
79
+ end
80
+
81
+ def check_for_missing_documents_for_slugs!(result, slugs)
82
+ missing_slugs = slugs - result.map(&:slugs).flatten
83
+
84
+ if !missing_slugs.blank? && Mongoid.raise_not_found_error
85
+ raise Errors::DocumentNotFound.new(klass, slugs, missing_slugs)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,5 +1,5 @@
1
1
  module Mongoid #:nodoc:
2
2
  module Slug
3
- VERSION = '0.10.0'
3
+ VERSION = '1.0.0.rc1'
4
4
  end
5
5
  end
@@ -1 +1,4 @@
1
+ require 'mongoid'
2
+ require 'stringex'
1
3
  require 'mongoid/slug'
4
+ require 'mongoid/slug/criteria'
@@ -3,9 +3,9 @@ class Author
3
3
  include Mongoid::Slug
4
4
  field :first_name
5
5
  field :last_name
6
- slug :first_name, :last_name, :scope => :book, :index => true
7
- referenced_in :book
8
- references_many :characters,
9
- :class_name => 'Person',
10
- :foreign_key => :author_id
6
+ slug :first_name, :last_name, :scope => :book
7
+ belongs_to :book
8
+ has_many :characters,
9
+ :class_name => 'Person',
10
+ :foreign_key => :author_id
11
11
  end
@@ -2,9 +2,10 @@ class Book
2
2
  include Mongoid::Document
3
3
  include Mongoid::Slug
4
4
  field :title
5
- slug :title, :index => true, :history => true
5
+
6
+ slug :title, :history => true
6
7
  embeds_many :subjects
7
- references_many :authors
8
+ has_many :authors
8
9
  end
9
10
 
10
11
  class ComicBook < Book
@@ -1,17 +1,17 @@
1
1
  class Caption
2
2
  include Mongoid::Document
3
3
  include Mongoid::Slug
4
- field :identity
4
+ field :my_identity, :type => String
5
5
  field :title
6
6
  field :medium
7
7
 
8
8
  # A fairly complex scenario, where we want to create a slug out of an
9
- # identity field, which comprises name of artist and some more bibliographic
9
+ # my_identity field, which comprises name of artist and some more bibliographic
10
10
  # info in parantheses, and the title of the work.
11
11
  #
12
12
  # We are only interested in the name of the artist so we remove the
13
13
  # paranthesized details.
14
- slug :identity, :title do |doc|
15
- [doc.identity.gsub(/\s*\([^)]+\)/, ''), doc.title].join(' ')
14
+ slug :my_identity, :title do |cur_object|
15
+ cur_object.slug_builder.gsub(/\s*\([^)]+\)/, '').to_url
16
16
  end
17
17
  end
@@ -2,5 +2,6 @@ class Friend
2
2
  include Mongoid::Document
3
3
  include Mongoid::Slug
4
4
  field :name
5
- slug :name, :reserve => ['foo', 'bar', /^[a-z]{2}$/i]
5
+ field :slug_history, :type => Array
6
+ slug :name, :reserve => ['foo', 'bar', /^[a-z]{2}$/i], :history => true
6
7
  end
@@ -0,0 +1,9 @@
1
+ class IntegerId
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+
5
+ field :_id, type: Integer
6
+ field :name, type: String
7
+
8
+ slug :name, history: true
9
+ end
@@ -2,7 +2,7 @@ class Person
2
2
  include Mongoid::Document
3
3
  include Mongoid::Slug
4
4
  field :name
5
- slug :name, :as => :permalink, :permanent => true, :scope => :author
5
+ slug :name, :permanent => true, :scope => :author
6
6
  embeds_many :relationships
7
- referenced_in :author, :inverse_of => :characters
7
+ belongs_to :author, :inverse_of => :characters
8
8
  end
@@ -0,0 +1,9 @@
1
+ class StringId
2
+ include Mongoid::Document
3
+ include Mongoid::Slug
4
+
5
+ field :_id, type: String
6
+ field :name, type: String
7
+
8
+ slug :name, history: true
9
+ end
@@ -0,0 +1,5 @@
1
+ class WithoutSlug
2
+ include Mongoid::Document
3
+
4
+ field :_id, type: Integer
5
+ end
@@ -25,6 +25,11 @@ module Mongoid
25
25
  }
26
26
  end
27
27
 
28
+ it "does not allow a Moped::BSON::ObjectId as use for a slug" do
29
+ bad = Book.create(:title => "4ea0389f0364313d79104fb3")
30
+ bad.slugs.should_not include("4ea0389f0364313d79104fb3")
31
+ end
32
+
28
33
  it "does not update slug if slugged fields have not changed" do
29
34
  book.save
30
35
  book.to_param.should eql "a-thousand-plateaus"
@@ -36,8 +41,26 @@ module Mongoid
36
41
  book.to_param.should eql "a-thousand-plateaus"
37
42
  end
38
43
 
39
- it "finds by slug" do
40
- Book.find_by_slug(book.to_param).should eql book
44
+ context "using find" do
45
+ it "finds by id as string" do
46
+ Book.find(book.id.to_s).should eql book
47
+ end
48
+
49
+ it "finds by id as array of strings" do
50
+ Book.find([book.id.to_s]).should eql [book]
51
+ end
52
+
53
+ it "finds by id as Moped::BSON::ObjectId" do
54
+ Book.find(book.id).should eql book
55
+ end
56
+
57
+ it "finds by id as an array of Moped::BSON::ObjectIds" do
58
+ Book.find([book.id]).should eql [book]
59
+ end
60
+
61
+ it "returns an empty array if given an empty array" do
62
+ Book.find([]).should eql []
63
+ end
41
64
  end
42
65
  end
43
66
 
@@ -61,6 +84,11 @@ module Mongoid
61
84
  dup.to_param.should eql "psychoanalysis-1"
62
85
  end
63
86
 
87
+ it "does not allow a Moped::BSON::ObjectId as use for a slug" do
88
+ bad = book.subjects.create(:name => "4ea0389f0364313d79104fb3")
89
+ bad.slugs.should_not eql "4ea0389f0364313d79104fb3"
90
+ end
91
+
64
92
  it "does not update slug if slugged fields have not changed" do
65
93
  subject.save
66
94
  subject.to_param.should eql "psychoanalysis"
@@ -71,9 +99,28 @@ module Mongoid
71
99
  subject.to_param.should eql "psychoanalysis"
72
100
  end
73
101
 
74
- it "finds by slug" do
75
- book.subjects.find_by_slug(subject.to_param).should eql subject
102
+ context "using find" do
103
+ it "finds by id as string" do
104
+ book.subjects.find(subject.id.to_s).should eql subject
105
+ end
106
+
107
+ it "finds by id as array of strings" do
108
+ book.subjects.find([subject.id.to_s]).should eql [subject]
109
+ end
110
+
111
+ it "finds by id as Moped::BSON::ObjectId" do
112
+ book.subjects.find(subject.id).should eql subject
113
+ end
114
+
115
+ it "finds by id as an array of Moped::BSON::ObjectIds" do
116
+ book.subjects.find([subject.id]).should eql [subject]
117
+ end
118
+
119
+ it "returns an empty array if given an empty array" do
120
+ book.subjects.find([]).should eql []
121
+ end
76
122
  end
123
+
77
124
  end
78
125
 
79
126
  context "when the object is embedded in another embedded object" do
@@ -104,6 +151,11 @@ module Mongoid
104
151
  dup.to_param.should eql "jane-smith-1"
105
152
  end
106
153
 
154
+ it "does not allow a Moped::BSON::ObjectId as use for a slug" do
155
+ bad = relationship.partners.create(:name => "4ea0389f0364313d79104fb3")
156
+ bad.slugs.should_not eql "4ea0389f0364313d79104fb3"
157
+ end
158
+
107
159
  it "does not update slug if slugged fields have not changed" do
108
160
  partner.save
109
161
  partner.to_param.should eql "jane-smith"
@@ -120,9 +172,28 @@ module Mongoid
120
172
  lover.to_param.should eql partner.to_param
121
173
  end
122
174
 
123
- it "finds by slug" do
124
- relationship.partners.find_by_slug(partner.to_param).should eql partner
175
+ context "using find" do
176
+ it "finds by id as string" do
177
+ relationship.partners.find(partner.id.to_s).should eql partner
178
+ end
179
+
180
+ it "finds by id as array of strings" do
181
+ relationship.partners.find([partner.id.to_s]).should eql [partner]
182
+ end
183
+
184
+ it "finds by id as Moped::BSON::ObjectId" do
185
+ relationship.partners.find(partner.id).should eql partner
186
+ end
187
+
188
+ it "finds by id as an array of Moped::BSON::ObjectIds" do
189
+ relationship.partners.find([partner.id]).should eql [partner]
190
+ end
191
+
192
+ it "returns an empty array if given an empty array" do
193
+ relationship.partners.find([]).should eql []
194
+ end
125
195
  end
196
+
126
197
  end
127
198
 
128
199
  context "when the slug is composed of multiple fields" do
@@ -157,15 +228,17 @@ module Mongoid
157
228
  dup2.to_param.should eql "gilles-deleuze-2"
158
229
  end
159
230
 
231
+ it "does not allow a Moped::BSON::ObjectId as use for a slug" do
232
+ bad = Author.create(:first_name => "4ea0389f0364",
233
+ :last_name => "313d79104fb3")
234
+ bad.to_param.should_not eql "4ea0389f0364313d79104fb3"
235
+ end
236
+
160
237
  it "does not update slug if slugged fields have changed but generated slug is identical" do
161
238
  author.last_name = "DELEUZE"
162
239
  author.save
163
240
  author.to_param.should eql "gilles-deleuze"
164
241
  end
165
-
166
- it "finds by slug" do
167
- Author.find_by_slug("gilles-deleuze").should eql author
168
- end
169
242
  end
170
243
 
171
244
  context "when :as is passed as an argument" do
@@ -174,12 +247,20 @@ module Mongoid
174
247
  end
175
248
 
176
249
  it "sets an alternative slug field name" do
177
- person.should respond_to(:permalink)
178
- person.permalink.should eql "john-doe"
250
+ person.should respond_to(:_slugs)
251
+ person.slugs.should eql ["john-doe"]
252
+ end
253
+
254
+ it 'defines #slug' do
255
+ person.should respond_to :slugs
179
256
  end
180
257
 
181
- it "finds by slug" do
182
- Person.find_by_permalink("john-doe").should eql person
258
+ it 'defines #slug_changed?' do
259
+ person.should respond_to :_slugs_changed?
260
+ end
261
+
262
+ it 'defines #slug_was' do
263
+ person.should respond_to :_slugs_was
183
264
  end
184
265
  end
185
266
 
@@ -206,15 +287,7 @@ module Mongoid
206
287
  end
207
288
 
208
289
  it "saves the old slug in the owner's history" do
209
- book.slug_history.should include("book-title")
210
- end
211
-
212
- it "returns the document for the old slug" do
213
- Book.find_by_slug("book-title").should == book
214
- end
215
-
216
- it "returns the document for the new slug" do
217
- Book.find_by_slug("other-book-title").should == book
290
+ book.slugs.should include("book-title")
218
291
  end
219
292
 
220
293
  it "generates a unique slug by appending a counter to duplicate text" do
@@ -222,10 +295,15 @@ module Mongoid
222
295
  dup.to_param.should eql "book-title-1"
223
296
  end
224
297
 
298
+ it "does not allow a Moped::BSON::ObjectId as use for a slug" do
299
+ bad = Book.create(:title => "4ea0389f0364313d79104fb3")
300
+ bad.to_param.should_not eql "4ea0389f0364313d79104fb3"
301
+ end
302
+
225
303
  it "ensures no duplicate values are stored in history" do
226
304
  book.update_attributes :title => 'Book Title'
227
305
  book.update_attributes :title => 'Foo'
228
- book.slug_history.find_all { |slug| slug == 'book-title' }.size.should eql 1
306
+ book.slugs.find_all { |slug| slug == 'book-title' }.size.should eql 1
229
307
  end
230
308
  end
231
309
 
@@ -250,6 +328,12 @@ module Mongoid
250
328
  dup.to_param.should eql "gilles-deleuze-1"
251
329
  end
252
330
 
331
+ it "does not allow a Moped::BSON::ObjectId as use for a slug" do
332
+ bad = book.authors.create(:first_name => "4ea0389f0364",
333
+ :last_name => "313d79104fb3")
334
+ bad.to_param.should_not eql "4ea0389f0364313d79104fb3"
335
+ end
336
+
253
337
  context "with an irregular association name" do
254
338
  let(:character) do
255
339
  # well we've got to make up something... :-)
@@ -285,11 +369,11 @@ module Mongoid
285
369
  end
286
370
 
287
371
  it "allows using the slug" do
288
- subject2.slug.should == "a-subject"
372
+ subject2.slugs.should include("a-subject")
289
373
  end
290
374
 
291
375
  it "removes the slug from the old owner's history" do
292
- subject1.slug_history.should_not include("a-subject")
376
+ subject1.slugs.should_not include("a-subject")
293
377
  end
294
378
  end
295
379
  end
@@ -309,11 +393,17 @@ module Mongoid
309
393
  dup = Magazine.create(:title => "Big Weekly", :publisher_id => "abc123")
310
394
  dup.to_param.should eql "big-weekly-1"
311
395
  end
396
+
397
+ it "does not allow a Moped::BSON::ObjectId as use for a slug" do
398
+ bad = Magazine.create(:title => "4ea0389f0364313d79104fb3", :publisher_id => "abc123")
399
+ bad.to_param.should_not eql "4ea0389f0364313d79104fb3"
400
+ end
401
+
312
402
  end
313
403
 
314
404
  context "when #slug is given a block" do
315
405
  let(:caption) do
316
- Caption.create(:identity => "Edward Hopper (American, 1882-1967)",
406
+ Caption.create(:my_identity => "Edward Hopper (American, 1882-1967)",
317
407
  :title => "Soir Bleu, 1914",
318
408
  :medium => "Oil on Canvas")
319
409
  end
@@ -329,14 +419,10 @@ module Mongoid
329
419
  end
330
420
 
331
421
  it "does not change slug if slugged fields have changed but generated slug is identical" do
332
- caption.identity = "Edward Hopper"
422
+ caption.my_identity = "Edward Hopper"
333
423
  caption.save
334
424
  caption.to_param.should eql "edward-hopper-soir-bleu-1914"
335
425
  end
336
-
337
- it "finds by slug" do
338
- Caption.find_by_slug(caption.to_param).should eql caption
339
- end
340
426
  end
341
427
 
342
428
  context "when slugged field contains non-ASCII characters" do
@@ -365,59 +451,54 @@ module Mongoid
365
451
  end
366
452
  end
367
453
 
368
- context "when :index is passed as an argument" do
454
+ context "when indexes are created" do
369
455
  before do
370
- Book.collection.drop_indexes
371
- Author.collection.drop_indexes
372
- end
373
-
374
- it "defines an index on the slug in top-level objects" do
456
+ Author.create_indexes
375
457
  Book.create_indexes
376
- Book.collection.index_information.should have_key "slug_1"
377
458
  end
378
459
 
379
- context "when slug is scoped by a reference association" do
380
- it "defines a non-unique index" do
381
- Author.create_indexes
382
- Author.index_information["slug_1"]["unique"].should be_false
383
- end
460
+ after do
461
+ Author.remove_indexes
462
+ Book.remove_indexes
384
463
  end
385
464
 
386
465
  context "when slug is not scoped by a reference association" do
466
+ it "defines an index on the slug" do
467
+ Book.index_options.should have_key( :_slugs => 1 )
468
+ end
469
+
387
470
  it "defines a unique index" do
388
- Book.create_indexes
389
- Book.index_information["slug_1"]["unique"].should be_true
471
+ Book.index_options[ :_slugs => 1 ][:unique].should be_true
390
472
  end
391
473
  end
392
- end
393
474
 
394
- context "when :index is not passed as an argument" do
395
- it "does not define an index on the slug" do
396
- Person.create_indexes
397
- Person.collection.index_information.should_not have_key "permalink_1"
475
+ context "when slug is scoped by a reference association" do
476
+ it "does not define an index on the slug" do
477
+ Author.index_options.should_not have_key(:_slugs => 1 )
478
+ end
398
479
  end
399
480
  end
400
-
481
+
401
482
  context "when :reserve is passed" do
402
483
  it "does not use the the reserved slugs" do
403
484
  friend1 = Friend.create(:name => "foo")
404
- friend1.slug.should_not eql("foo")
405
- friend1.slug.should eql("foo-1")
485
+ friend1.slugs.should_not eql("foo")
486
+ friend1.slugs.should include("foo-1")
406
487
 
407
488
  friend2 = Friend.create(:name => "bar")
408
- friend2.slug.should_not eql("bar")
409
- friend2.slug.should eql("bar-1")
489
+ friend2.slugs.should_not eql("bar")
490
+ friend2.slugs.should include("bar-1")
410
491
 
411
492
  friend3 = Friend.create(:name => "en")
412
- friend3.slug.should_not eql("en")
413
- friend3.slug.should eql("en-1")
493
+ friend3.slugs.should_not eql("en")
494
+ friend3.slugs.should include("en-1")
414
495
  end
415
496
 
416
497
  it "should start with concatenation -1" do
417
498
  friend1 = Friend.create(:name => "foo")
418
- friend1.slug.should eql("foo-1")
499
+ friend1.slugs.should include("foo-1")
419
500
  friend2 = Friend.create(:name => "foo")
420
- friend2.slug.should eql("foo-2")
501
+ friend2.slugs.should include("foo-2")
421
502
  end
422
503
  end
423
504
 
@@ -425,112 +506,264 @@ module Mongoid
425
506
  it "scopes by the superclass" do
426
507
  book = Book.create(:title => "Anti Oedipus")
427
508
  comic_book = ComicBook.create(:title => "Anti Oedipus")
428
- comic_book.slug.should_not eql(book.slug)
509
+ comic_book.slugs.should_not eql(book.slugs)
429
510
  end
430
511
  end
431
512
 
432
513
  context "when slug defined on alias of field" do
433
514
  it "should use accessor, not alias" do
434
515
  pseudonim = Alias.create(:author_name => "Max Stirner")
435
- pseudonim.slug.should eql("max-stirner")
516
+ pseudonim.slugs.should include("max-stirner")
436
517
  end
437
518
  end
438
519
 
439
- describe ".by_slug scope" do
440
- let!(:author) { book.authors.create(:first_name => "Gilles", :last_name => "Deleuze") }
441
-
442
- it "returns an empty array if no document is found" do
443
- book.authors.by_slug("never-heard-of").should == []
520
+ describe ".find" do
521
+ let!(:book) { Book.create(:title => "A Working Title").tap { |d| d.update_attribute(:title, "A Thousand Plateaus") } }
522
+ let!(:book2) { Book.create(:title => "Difference and Repetition") }
523
+ let!(:friend) { Friend.create(:name => "Jim Bob") }
524
+ let!(:friend2) { Friend.create(:name => friend.id.to_s) }
525
+ let!(:integer_id) { IntegerId.new(:name => "I have integer ids").tap { |d| d.id = 123; d.save } }
526
+ let!(:integer_id2) { IntegerId.new(:name => integer_id.id.to_s).tap { |d| d.id = 456; d.save } }
527
+ let!(:string_id) { StringId.new(:name => "I have string ids").tap { |d| d.id = 'abc'; d.save } }
528
+ let!(:string_id2) { StringId.new(:name => string_id.id.to_s).tap { |d| d.id = 'def'; d.save } }
529
+ let!(:subject) { Subject.create(:title => "A Subject", :book => book) }
530
+ let!(:subject2) { Subject.create(:title => "A Subject", :book => book2) }
531
+ let!(:without_slug) { WithoutSlug.new().tap { |d| d.id = 456; d.save } }
532
+
533
+ context "when the model does not use mongoid slugs" do
534
+ it "should not use mongoid slug's custom find methods" do
535
+ Mongoid::Slug::Criteria.any_instance.should_not_receive(:find)
536
+ WithoutSlug.find(without_slug.id.to_s).should == without_slug
537
+ end
444
538
  end
445
539
 
446
- it "returns an array containing the document if it is found" do
447
- book.authors.by_slug(author.slug).should == [author]
448
- end
449
- end
540
+ context "using slugs" do
541
+ context "(single)" do
542
+ context "and a document is found" do
543
+ it "returns the document as an object" do
544
+ Book.find(book.slugs.first).should == book
545
+ end
546
+ end
547
+
548
+ context "but no document is found" do
549
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
550
+ lambda {
551
+ Book.find("Anti Oedipus")
552
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
553
+ end
554
+ end
555
+ end
556
+
557
+ context "(multiple)" do
558
+ context "and all documents are found" do
559
+ it "returns the documents as an array without duplication" do
560
+ Book.find(book.slugs + book2.slugs).should =~ [book, book2]
561
+ end
562
+ end
563
+
564
+ context "but not all documents are found" do
565
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
566
+ lambda {
567
+ Book.find(book.slugs + ['something-nonexistent'])
568
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
569
+ end
570
+ end
571
+ end
572
+
573
+ context "when no documents match" do
574
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
575
+ lambda {
576
+ Book.find("Anti Oedipus")
577
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
578
+ end
579
+ end
580
+
581
+ context "when ids are BSON::ObjectIds and the supplied argument looks like a BSON::ObjectId" do
582
+ it "it should find based on ids not slugs" do # i.e. it should type cast the argument
583
+ Friend.find(friend.id.to_s).should == friend
584
+ end
585
+ end
586
+
587
+ context "when ids are Strings" do
588
+ it "it should find based on ids not slugs" do # i.e. string ids should take precedence over string slugs
589
+ StringId.find(string_id.id.to_s).should == string_id
590
+ end
591
+ end
592
+
593
+ context "when ids are Integers and the supplied arguments looks like an Integer" do
594
+ it "it should find based on slugs not ids" do # i.e. it should not type cast the argument
595
+ IntegerId.find(integer_id.id.to_s).should == integer_id2
596
+ end
597
+ end
450
598
 
451
- describe ".find_by_slug" do
452
- let!(:book) { Book.create(:title => "A Thousand Plateaus") }
599
+ context "models that does not use slugs, should find using the original find" do
600
+ it "it should find based on ids" do # i.e. it should not type cast the argument
601
+ WithoutSlug.find(without_slug.id.to_s).should == without_slug
602
+ end
603
+ end
453
604
 
454
- it "returns nil if no document is found" do
455
- Book.find_by_slug(:title => "Anti Oedipus").should be_nil
605
+ context "when scoped" do
606
+ context "and a document is found" do
607
+ it "returns the document as an object" do
608
+ book.subjects.find(subject.slugs.first).should == subject
609
+ book2.subjects.find(subject.slugs.first).should == subject2
610
+ end
611
+ end
612
+
613
+ context "but no document is found" do
614
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
615
+ lambda {
616
+ book.subjects.find('Another Subject')
617
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
618
+ end
619
+ end
620
+ end
456
621
  end
457
622
 
458
- it "returns the document if it is found" do
459
- Book.find_by_slug(book.slug).should == book
623
+ context "using ids" do
624
+ it "raises a Mongoid::Errors::DocumentNotFound error if no document is found" do
625
+ lambda {
626
+ Book.find(friend.id)
627
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
628
+ end
629
+
630
+ context "given a single document" do
631
+ it "returns the document" do
632
+ Friend.find(friend.id).should == friend
633
+ end
634
+ end
635
+
636
+ context "given multiple documents" do
637
+ it "returns the documents" do
638
+ Book.find([book.id, book2.id]).should =~ [book, book2]
639
+ end
640
+ end
460
641
  end
461
642
  end
462
643
 
463
644
  describe ".find_by_slug!" do
464
- let!(:book) { Book.create(:title => "A Thousand Plateaus") }
645
+ let!(:book) { Book.create(:title => "A Working Title").tap { |d| d.update_attribute(:title, "A Thousand Plateaus") } }
646
+ let!(:book2) { Book.create(:title => "Difference and Repetition") }
647
+ let!(:friend) { Friend.create(:name => "Jim Bob") }
648
+ let!(:friend2) { Friend.create(:name => friend.id.to_s) }
649
+ let!(:integer_id) { IntegerId.new(:name => "I have integer ids").tap { |d| d.id = 123; d.save } }
650
+ let!(:integer_id2) { IntegerId.new(:name => integer_id.id.to_s).tap { |d| d.id = 456; d.save } }
651
+ let!(:string_id) { StringId.new(:name => "I have string ids").tap { |d| d.id = 'abc'; d.save } }
652
+ let!(:string_id2) { StringId.new(:name => string_id.id.to_s).tap { |d| d.id = 'def'; d.save } }
653
+ let!(:subject) { Subject.create(:title => "A Subject", :book => book) }
654
+ let!(:subject2) { Subject.create(:title => "A Subject", :book => book2) }
655
+
656
+ context "(single)" do
657
+ context "and a document is found" do
658
+ it "returns the document as an object" do
659
+ Book.find_by_slug!(book.slugs.first).should == book
660
+ end
661
+ end
465
662
 
466
- it "raises a Mongoid::Errors::DocumentNotFound error if no document is found" do
467
- lambda {
468
- Book.find_by_slug!(:title => "Anti Oedipus")
469
- }.should raise_error(Mongoid::Errors::DocumentNotFound)
663
+ context "but no document is found" do
664
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
665
+ lambda {
666
+ Book.find_by_slug!("Anti Oedipus")
667
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
668
+ end
669
+ end
470
670
  end
471
671
 
472
- it "returns the document when it is found" do
473
- Book.find_by_slug!(book.slug).should == book
474
- end
475
- end
672
+ context "(multiple)" do
673
+ context "and all documents are found" do
674
+ it "returns the documents as an array without duplication" do
675
+ Book.find_by_slug!(book.slugs + book2.slugs).should =~ [book, book2]
676
+ end
677
+ end
476
678
 
477
- context "when #to_param is called on an existing record with no slug" do
478
- before do
479
- Book.collection.insert(:title => "Proust and Signs")
679
+ context "but not all documents are found" do
680
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
681
+ lambda {
682
+ Book.find_by_slug!(book.slugs + ['something-nonexistent'])
683
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
684
+ end
685
+ end
480
686
  end
481
687
 
482
- it "generates the missing slug" do
483
- book = Book.first
484
- book.to_param
485
- book.reload.slug.should eql "proust-and-signs"
688
+ context "when scoped" do
689
+ context "and a document is found" do
690
+ it "returns the document as an object" do
691
+ book.subjects.find_by_slug!(subject.slugs.first).should == subject
692
+ book2.subjects.find_by_slug!(subject.slugs.first).should == subject2
693
+ end
694
+ end
695
+
696
+ context "but no document is found" do
697
+ it "raises a Mongoid::Errors::DocumentNotFound error" do
698
+ lambda {
699
+ book.subjects.find_by_slug!('Another Subject')
700
+ }.should raise_error(Mongoid::Errors::DocumentNotFound)
701
+ end
702
+ end
486
703
  end
487
704
  end
488
705
 
489
- describe ".for_unique_slug_for" do
490
- it "returns the unique slug" do
491
- Book.find_unique_slug_for("A Thousand Plateaus").should eq("a-thousand-plateaus")
706
+ describe "#to_param" do
707
+ context "when called on an existing record with no slug" do
708
+ before do
709
+ Book.collection.insert(:title => "Proust and Signs")
710
+ end
711
+
712
+ it "generates the missing slug" do
713
+ book = Book.first
714
+ book.to_param
715
+ book.reload.slugs.should include("proust-and-signs")
716
+ end
492
717
  end
718
+ end
493
719
 
494
- it "returns the unique slug with a counter if necessary" do
720
+ describe "#_slugs_changed?" do
721
+ before do
495
722
  Book.create(:title => "A Thousand Plateaus")
496
- Book.find_unique_slug_for("A Thousand Plateaus").should eq("a-thousand-plateaus-1")
497
723
  end
498
724
 
499
- it "returns the unique slug as if it were the provided object" do
500
- book = Book.create(:title => "A Thousand Plateaus")
501
- Book.find_unique_slug_for("A Thousand Plateaus", :model => book).should eq("a-thousand-plateaus")
725
+ let(:book) { Book.first }
726
+
727
+ it "is initially unchanged" do
728
+ book._slugs_changed?.should be_false
729
+ end
730
+
731
+ it "tracks changes" do
732
+ book.slugs = ["Anti Oedipus"]
733
+ book._slugs_changed?.should be_true
502
734
  end
503
735
  end
504
736
 
505
- describe "#find_unique_slug_for" do
506
- let!(:book) { Book.create(:title => "A Thousand Plateaus") }
737
+ describe "when regular expression matches, but document does not" do
738
+ let!(:book_1) { Book.create(:title => "book-1") }
739
+ let!(:book_2) { Book.create(:title => "book") }
740
+ let!(:book_3) { Book.create(:title => "book") }
507
741
 
508
- it "returns the unique slug" do
509
- book.find_unique_slug_for("Anti Oedipus").should eq("anti-oedipus")
742
+ it "book_2 should have the user supplied title without -1 after it" do
743
+ book_2.to_param.should eql "book"
510
744
  end
511
745
 
512
- it "returns the unique slug with a counter if necessary" do
513
- Book.create(:title => "Anti Oedipus")
514
- book.find_unique_slug_for("Anti Oedipus").should eq("anti-oedipus-1")
746
+ it "book_3 should have a generated slug" do
747
+ book_3.to_param.should eql "book-2"
515
748
  end
516
749
  end
517
750
 
518
751
  context "when the slugged field is set manually" do
519
752
  context "when it set to a non-empty string" do
520
753
  it "respects the provided slug" do
521
- book = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
754
+ book = Book.create(:title => "A Thousand Plateaus", :slugs => ["not-what-you-expected"])
522
755
  book.to_param.should eql "not-what-you-expected"
523
756
  end
524
-
757
+
525
758
  it "ensures uniqueness" do
526
- book1 = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
527
- book2 = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
759
+ book1 = Book.create(:title => "A Thousand Plateaus", :slugs => ["not-what-you-expected"])
760
+ book2 = Book.create(:title => "A Thousand Plateaus", :slugs => ["not-what-you-expected"])
528
761
  book2.to_param.should eql "not-what-you-expected-1"
529
762
  end
530
-
763
+
531
764
  it "updates the slug when a new one is passed in" do
532
- book = Book.create(:title => "A Thousand Plateaus", :slug => "not-what-you-expected")
533
- book.slug = "not-it-either"
765
+ book = Book.create(:title => "A Thousand Plateaus", :slugs => ["not-what-you-expected"])
766
+ book.slugs = ["not-it-either"]
534
767
  book.save
535
768
  book.to_param.should eql "not-it-either"
536
769
  end
@@ -538,7 +771,7 @@ module Mongoid
538
771
 
539
772
  context "when it is set to an empty string" do
540
773
  it "generate a new one" do
541
- book = Book.create(:title => "A Thousand Plateaus", :slug => "")
774
+ book = Book.create(:title => "A Thousand Plateaus")
542
775
  book.to_param.should eql "a-thousand-plateaus"
543
776
  end
544
777
  end