chewy 7.2.3 → 7.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,21 @@
1
1
  require 'spec_helper'
2
2
 
3
+ SimpleComment = Class.new do
4
+ attr_reader :content, :comment_type, :commented_id, :updated_at, :id
5
+
6
+ def initialize(hash)
7
+ @id = hash['id']
8
+ @content = hash['content']
9
+ @comment_type = hash['comment_type']
10
+ @commented_id = hash['commented_id']
11
+ @updated_at = hash['updated_at']
12
+ end
13
+
14
+ def derived
15
+ "[derived] #{content}"
16
+ end
17
+ end
18
+
3
19
  describe Chewy::Index::Import::BulkBuilder do
4
20
  before { Chewy.massacre }
5
21
 
@@ -169,6 +185,294 @@ describe Chewy::Index::Import::BulkBuilder do
169
185
  end
170
186
  end
171
187
  end
188
+
189
+ context 'with parents' do
190
+ let(:index) { CommentsIndex }
191
+ before do
192
+ stub_model(:comment)
193
+ stub_index(:comments) do
194
+ index_scope Comment
195
+
196
+ crutch :content_with_crutches do |collection| # collection here is a current batch of products
197
+ collection.map { |comment| [comment.id, "[crutches] #{comment.content}"] }.to_h
198
+ end
199
+
200
+ field :content
201
+ field :content_with_crutches, value: ->(comment, crutches) { crutches.content_with_crutches[comment.id] }
202
+ field :comment_type, type: :join, relations: {question: %i[answer comment], answer: :vote, vote: :subvote}, join: {type: :comment_type, id: :commented_id}
203
+ end
204
+ end
205
+
206
+ let!(:existing_comments) do
207
+ [
208
+ Comment.create!(id: 1, content: 'Where is Nemo?', comment_type: :question),
209
+ Comment.create!(id: 2, content: 'Here.', comment_type: :answer, commented_id: 1),
210
+ Comment.create!(id: 31, content: 'What is the best programming language?', comment_type: :question)
211
+ ]
212
+ end
213
+
214
+ def do_raw_index_comment(options:, data:)
215
+ CommentsIndex.client.index(options.merge(index: 'comments', type: '_doc', refresh: true, body: data))
216
+ end
217
+
218
+ def raw_index_comment(comment)
219
+ options = {id: comment.id, routing: root(comment).id}
220
+ comment_type = comment.commented_id.present? ? {name: comment.comment_type, parent: comment.commented_id} : comment.comment_type
221
+ do_raw_index_comment(
222
+ options: options,
223
+ data: {content: comment.content, comment_type: comment_type}
224
+ )
225
+ end
226
+
227
+ def root(comment)
228
+ current = comment
229
+ # slow, but it's OK, as we don't have too deep trees
230
+ current = Comment.find(current.commented_id) while current.commented_id
231
+ current
232
+ end
233
+
234
+ before do
235
+ CommentsIndex.reset! # initialize index
236
+ end
237
+
238
+ let(:comments) do
239
+ [
240
+ Comment.create!(id: 3, content: 'There!', comment_type: :answer, commented_id: 1),
241
+ Comment.create!(id: 4, content: 'Yes, he is here.', comment_type: :vote, commented_id: 2),
242
+
243
+ Comment.create!(id: 11, content: 'What is the sense of the universe?', comment_type: :question),
244
+ Comment.create!(id: 12, content: 'I don\'t know.', comment_type: :answer, commented_id: 11),
245
+ Comment.create!(id: 13, content: '42', comment_type: :answer, commented_id: 11),
246
+ Comment.create!(id: 14, content: 'I think that 42 is a correct answer', comment_type: :vote, commented_id: 13),
247
+
248
+ Comment.create!(id: 21, content: 'How are you?', comment_type: :question),
249
+
250
+ Comment.create!(id: 32, content: 'Ruby', comment_type: :answer, commented_id: 31)
251
+ ]
252
+ end
253
+
254
+ context 'when indexing a single object' do
255
+ let(:to_index) { [comments[0]] }
256
+
257
+ specify do
258
+ expect(subject.bulk_body).to eq([
259
+ {index: {_id: 3, routing: '1', data: {'content' => 'There!', 'content_with_crutches' => '[crutches] There!', 'comment_type' => {'name' => 'answer', 'parent' => 1}}}}
260
+ ])
261
+ end
262
+ end
263
+
264
+ context 'with raw import' do
265
+ before do
266
+ stub_index(:comments) do
267
+ index_scope Comment
268
+ default_import_options raw_import: ->(hash) { SimpleComment.new(hash) }
269
+
270
+ crutch :content_with_crutches do |collection| # collection here is a current batch of products
271
+ collection.map { |comment| [comment.id, "[crutches] #{comment.content}"] }.to_h
272
+ end
273
+
274
+ field :content
275
+ field :content_with_crutches, value: ->(comment, crutches) { crutches.content_with_crutches[comment.id] }
276
+ field :derived
277
+ field :comment_type, type: :join, relations: {question: %i[answer comment], answer: :vote, vote: :subvote}, join: {type: :comment_type, id: :commented_id}
278
+ end
279
+ end
280
+
281
+ let(:to_index) { [comments[0]].map { |c| SimpleComment.new(c.attributes) } } # id: 3
282
+ let(:delete) { [existing_comments[0]].map { |c| c } } # id: 1
283
+
284
+ specify do
285
+ expected_data = {'content' => 'There!', 'content_with_crutches' => '[crutches] There!', 'derived' => '[derived] There!', 'comment_type' => {'name' => 'answer', 'parent' => 1}}
286
+ expect(subject.bulk_body).to eq([
287
+ {index: {_id: 3, routing: '1', data: expected_data}},
288
+ {delete: {_id: 1, routing: '1'}}
289
+ ])
290
+ end
291
+ end
292
+
293
+ context 'when switching parents' do
294
+ let(:switching_parent_comment) { comments[0].tap { |c| c.update!(commented_id: 31) } } # id: 3
295
+ let(:removing_parent_comment) { comments[1].tap { |c| c.update!(commented_id: nil, comment_type: nil) } } # id: 4
296
+ let(:converting_to_parent_comment) { comments[3].tap { |c| c.update!(commented_id: nil, comment_type: :question) } } # id: 12
297
+ let(:converting_to_child_comment) { comments[6].tap { |c| c.update!(commented_id: 1, comment_type: :answer) } } # id: 21
298
+ let(:fields) { %w[commented_id comment_type] }
299
+
300
+ let(:to_index) { [switching_parent_comment, removing_parent_comment, converting_to_parent_comment, converting_to_child_comment] }
301
+
302
+ before do
303
+ existing_comments.each { |c| raw_index_comment(c) }
304
+ comments.each { |c| raw_index_comment(c) }
305
+ end
306
+
307
+ specify do
308
+ expect(subject.bulk_body).to eq([
309
+ {delete: {_id: 3, routing: '1', parent: 1}},
310
+ {index: {_id: 3, routing: '31', data: {'content' => 'There!', 'content_with_crutches' => '[crutches] There!', 'comment_type' => {'name' => 'answer', 'parent' => 31}}}},
311
+ {delete: {_id: 4, routing: '1', parent: 2}},
312
+ {index: {_id: 4, routing: '4', data: {'content' => 'Yes, he is here.', 'content_with_crutches' => '[crutches] Yes, he is here.', 'comment_type' => nil}}},
313
+ {delete: {_id: 12, routing: '11', parent: 11}},
314
+ {index: {_id: 12, routing: '12', data: {'content' => 'I don\'t know.', 'content_with_crutches' => '[crutches] I don\'t know.', 'comment_type' => 'question'}}},
315
+ {delete: {_id: 21, routing: '21'}},
316
+ {index: {_id: 21, routing: '1', data: {'content' => 'How are you?', 'content_with_crutches' => '[crutches] How are you?', 'comment_type' => {'name' => 'answer', 'parent' => 1}}}}
317
+ ])
318
+ end
319
+ end
320
+
321
+ context 'when indexing with grandparents' do
322
+ let(:comments) do
323
+ [
324
+ Comment.create!(id: 3, content: 'Yes, he is here.', comment_type: :vote, commented_id: 2),
325
+ Comment.create!(id: 4, content: 'What?', comment_type: :subvote, commented_id: 3)
326
+ ]
327
+ end
328
+ let(:to_index) { comments }
329
+
330
+ before do
331
+ existing_comments.each { |c| raw_index_comment(c) }
332
+ end
333
+
334
+ specify do
335
+ expected_data3 = {'content' => 'Yes, he is here.', 'content_with_crutches' => '[crutches] Yes, he is here.', 'comment_type' => {'name' => 'vote', 'parent' => 2}}
336
+ expected_data4 = {'content' => 'What?', 'content_with_crutches' => '[crutches] What?', 'comment_type' => {'name' => 'subvote', 'parent' => 3}}
337
+ expect(subject.bulk_body).to eq([
338
+ {index: {_id: 3, routing: '1', data: expected_data3}},
339
+ {index: {_id: 4, routing: '1', data: expected_data4}}
340
+ ])
341
+ end
342
+ end
343
+
344
+ context 'when switching grandparents' do
345
+ let(:comments) do
346
+ [
347
+ Comment.create!(id: 3, content: 'Yes, he is here.', comment_type: :vote, commented_id: 2),
348
+ Comment.create!(id: 4, content: 'What?', comment_type: :subvote, commented_id: 3)
349
+ ]
350
+ end
351
+ let(:switching_parent_comment) { existing_comments[1].tap { |c| c.update!(commented_id: 31) } } # id: 2
352
+ let(:fields) { %w[commented_id comment_type] }
353
+ let(:to_index) { [switching_parent_comment] }
354
+
355
+ before do
356
+ existing_comments.each { |c| raw_index_comment(c) }
357
+ comments.each { |c| raw_index_comment(c) }
358
+ end
359
+
360
+ it 'reindexes children and grandchildren' do
361
+ expected_data2 = {'content' => 'Here.', 'content_with_crutches' => '[crutches] Here.', 'comment_type' => {'name' => 'answer', 'parent' => 31}}
362
+ expected_data3 = {'content' => 'Yes, he is here.', 'content_with_crutches' => '[crutches] Yes, he is here.', 'comment_type' => {'name' => 'vote', 'parent' => 2}}
363
+ expected_data4 = {'content' => 'What?', 'content_with_crutches' => '[crutches] What?', 'comment_type' => {'name' => 'subvote', 'parent' => 3}}
364
+ expect(subject.bulk_body).to eq([
365
+ {delete: {_id: 2, routing: '1', parent: 1}},
366
+ {index: {_id: 2, routing: '31', data: expected_data2}},
367
+ {delete: {_id: 3, routing: '1', parent: 2}},
368
+ {index: {_id: 3, routing: '31', data: expected_data3}},
369
+ {delete: {_id: 4, routing: '1', parent: 3}},
370
+ {index: {_id: 4, routing: '31', data: expected_data4}}
371
+ ])
372
+ end
373
+ end
374
+
375
+ describe 'when removing parents or grandparents' do
376
+ let(:comments) do
377
+ [
378
+ Comment.create!(id: 3, content: 'Yes, he is here.', comment_type: :vote, commented_id: 2),
379
+ Comment.create!(id: 4, content: 'What?', comment_type: :subvote, commented_id: 3)
380
+ ]
381
+ end
382
+ let(:delete) { [existing_comments[0]] } # id: 1
383
+
384
+ before do
385
+ existing_comments.each { |c| raw_index_comment(c) }
386
+ comments.each { |c| raw_index_comment(c) }
387
+ end
388
+
389
+ it 'does not remove all descendants' do
390
+ expect(subject.bulk_body).to eq([
391
+ {delete: {_id: 1, routing: '1'}}
392
+ ])
393
+ end
394
+ end
395
+
396
+ context 'when indexing' do
397
+ let(:to_index) { comments }
398
+
399
+ specify do
400
+ expected_data3 = {'content' => 'There!', 'content_with_crutches' => '[crutches] There!', 'comment_type' => {'name' => 'answer', 'parent' => 1}}
401
+ expected_data4 = {'content' => 'Yes, he is here.', 'content_with_crutches' => '[crutches] Yes, he is here.', 'comment_type' => {'name' => 'vote', 'parent' => 2}}
402
+
403
+ expected_data11 = {'content' => 'What is the sense of the universe?', 'content_with_crutches' => '[crutches] What is the sense of the universe?', 'comment_type' => 'question'}
404
+ expected_data12 = {'content' => 'I don\'t know.', 'content_with_crutches' => '[crutches] I don\'t know.', 'comment_type' => {'name' => 'answer', 'parent' => 11}}
405
+ expected_data13 = {'content' => '42', 'content_with_crutches' => '[crutches] 42', 'comment_type' => {'name' => 'answer', 'parent' => 11}}
406
+ expected_data14 = {'content' => 'I think that 42 is a correct answer', 'content_with_crutches' => '[crutches] I think that 42 is a correct answer',
407
+ 'comment_type' => {'name' => 'vote', 'parent' => 13}}
408
+
409
+ expected_data21 = {'content' => 'How are you?', 'content_with_crutches' => '[crutches] How are you?', 'comment_type' => 'question'}
410
+
411
+ expected_data32 = {'content' => 'Ruby', 'content_with_crutches' => '[crutches] Ruby', 'comment_type' => {'name' => 'answer', 'parent' => 31}}
412
+
413
+ expect(subject.bulk_body).to eq([
414
+ {index: {_id: 3, routing: '1', data: expected_data3}},
415
+ {index: {_id: 4, routing: '1', data: expected_data4}},
416
+
417
+ {index: {_id: 11, routing: '11', data: expected_data11}},
418
+ {index: {_id: 12, routing: '11', data: expected_data12}},
419
+ {index: {_id: 13, routing: '11', data: expected_data13}},
420
+ {index: {_id: 14, routing: '11', data: expected_data14}},
421
+
422
+ {index: {_id: 21, routing: '21', data: expected_data21}},
423
+
424
+ {index: {_id: 32, routing: '31', data: expected_data32}}
425
+ ])
426
+ end
427
+ end
428
+
429
+ context 'when deleting' do
430
+ before do
431
+ existing_comments.each { |c| raw_index_comment(c) }
432
+ comments.each { |c| raw_index_comment(c) }
433
+ end
434
+
435
+ let(:delete) { comments }
436
+ specify do
437
+ expect(subject.bulk_body).to eq([
438
+ {delete: {_id: 3, routing: '1', parent: 1}},
439
+ {delete: {_id: 4, routing: '1', parent: 2}},
440
+
441
+ {delete: {_id: 11, routing: '11'}},
442
+ {delete: {_id: 12, routing: '11', parent: 11}},
443
+ {delete: {_id: 13, routing: '11', parent: 11}},
444
+ {delete: {_id: 14, routing: '11', parent: 13}},
445
+
446
+ {delete: {_id: 21, routing: '21'}},
447
+
448
+ {delete: {_id: 32, routing: '31', parent: 31}}
449
+ ])
450
+ end
451
+ end
452
+
453
+ context 'when updating' do
454
+ before do
455
+ comments.each { |c| raw_index_comment(c) }
456
+ end
457
+ let(:fields) { %w[content] }
458
+ let(:to_index) { comments }
459
+ specify do
460
+ expect(subject.bulk_body).to eq([
461
+ {update: {_id: 3, routing: '1', data: {doc: {'content' => comments[0].content}}}},
462
+ {update: {_id: 4, routing: '1', data: {doc: {'content' => comments[1].content}}}},
463
+
464
+ {update: {_id: 11, routing: '11', data: {doc: {'content' => comments[2].content}}}},
465
+ {update: {_id: 12, routing: '11', data: {doc: {'content' => comments[3].content}}}},
466
+ {update: {_id: 13, routing: '11', data: {doc: {'content' => comments[4].content}}}},
467
+ {update: {_id: 14, routing: '11', data: {doc: {'content' => comments[5].content}}}},
468
+
469
+ {update: {_id: 21, routing: '21', data: {doc: {'content' => comments[6].content}}}},
470
+
471
+ {update: {_id: 32, routing: '31', data: {doc: {'content' => comments[7].content}}}}
472
+ ])
473
+ end
474
+ end
475
+ end
172
476
  end
173
477
 
174
478
  describe '#index_objects_by_id' do
@@ -11,8 +11,8 @@ describe Chewy::Index::Import::Routine do
11
11
  CitiesIndex.create!
12
12
  end
13
13
 
14
- let(:index) { [double(id: 1, name: 'Name', object: {}), double(id: 2, name: 'Name', object: {})] }
15
- let(:delete) { [double(id: 3, name: 'Name')] }
14
+ let(:index) { [double('city_1', id: 1, name: 'Name', object: {}), double('city_2', id: 2, name: 'Name', object: {})] }
15
+ let(:delete) { [double('city_3', id: 3, name: 'Name', object: {})] }
16
16
 
17
17
  describe '#options' do
18
18
  specify do
@@ -93,7 +93,7 @@ describe Chewy::Index::Import::Routine do
93
93
 
94
94
  describe '#errors' do
95
95
  subject { described_class.new(CitiesIndex) }
96
- let(:index) { [double(id: 1, name: 'Name', object: ''), double(id: 2, name: 'Name', object: {})] }
96
+ let(:index) { [double('city_1', id: 1, name: 'Name', object: ''), double('city_2', id: 2, name: 'Name', object: {})] }
97
97
 
98
98
  specify { expect(subject.errors).to eq([]) }
99
99
  specify do
@@ -493,6 +493,46 @@ describe Chewy::Index::Import do
493
493
 
494
494
  it_behaves_like 'importing'
495
495
  end
496
+
497
+ context 'with parent-child relationship' do
498
+ before do
499
+ stub_model(:comment)
500
+ stub_index(:comments) do
501
+ index_scope Comment
502
+ field :content
503
+ field :comment_type, type: :join, relations: {question: %i[answer comment], answer: :vote}, join: {type: :comment_type, id: :commented_id}
504
+ end
505
+ end
506
+
507
+ let!(:comments) do
508
+ [
509
+ Comment.create!(id: 1, content: 'Where is Nemo?', comment_type: :question),
510
+ Comment.create!(id: 2, content: 'Here.', comment_type: :answer, commented_id: 1),
511
+ Comment.create!(id: 3, content: 'There!', comment_type: :answer, commented_id: 1),
512
+ Comment.create!(id: 4, content: 'Yes, he is here.', comment_type: :vote, commented_id: 2)
513
+ ]
514
+ end
515
+
516
+ def imported_comments
517
+ CommentsIndex.all.map do |comment|
518
+ comment.attributes.except('_score', '_explanation')
519
+ end
520
+ end
521
+
522
+ it 'imports parent and children' do
523
+ CommentsIndex.import!(comments.map(&:id))
524
+
525
+ expect(imported_comments).to match_array([
526
+ {'id' => '1', 'content' => 'Where is Nemo?', 'comment_type' => 'question'},
527
+ {'id' => '2', 'content' => 'Here.', 'comment_type' => {'name' => 'answer', 'parent' => 1}},
528
+ {'id' => '3', 'content' => 'There!', 'comment_type' => {'name' => 'answer', 'parent' => 1}},
529
+ {'id' => '4', 'content' => 'Yes, he is here.', 'comment_type' => {'name' => 'vote', 'parent' => 2}}
530
+ ])
531
+
532
+ answer_ids = CommentsIndex.query(has_parent: {parent_type: 'question', query: {match: {content: 'Where'}}}).pluck(:_id)
533
+ expect(answer_ids).to match_array(%w[2 3])
534
+ end
535
+ end
496
536
  end
497
537
 
498
538
  describe '.import!', :orm do
@@ -1,6 +1,6 @@
1
1
  require 'database_cleaner'
2
2
 
3
- ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: 'file::memory:?cache=shared', pool: 10)
3
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:', pool: 10)
4
4
  ActiveRecord::Base.logger = Logger.new('/dev/null')
5
5
  if ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks)
6
6
  ActiveRecord::Base.raise_in_transactional_callbacks = true
@@ -31,6 +31,13 @@ ActiveRecord::Schema.define do
31
31
  t.column :lat, :string
32
32
  t.column :lon, :string
33
33
  end
34
+
35
+ create_table :comments do |t|
36
+ t.column :content, :string
37
+ t.column :comment_type, :string
38
+ t.column :commented_id, :integer
39
+ t.column :updated_at, :datetime
40
+ end
34
41
  end
35
42
 
36
43
  module ActiveRecordClassHelpers
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chewy
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.3
4
+ version: 7.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Toptal, LLC
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-10-29 00:00:00.000000000 Z
12
+ date: 2022-02-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: database_cleaner
@@ -244,9 +244,8 @@ files:
244
244
  - gemfiles/rails.5.2.activerecord.gemfile
245
245
  - gemfiles/rails.6.0.activerecord.gemfile
246
246
  - gemfiles/rails.6.1.activerecord.gemfile
247
+ - gemfiles/rails.7.0.activerecord.gemfile
247
248
  - lib/chewy.rb
248
- - lib/chewy/backports/deep_dup.rb
249
- - lib/chewy/backports/duplicable.rb
250
249
  - lib/chewy/config.rb
251
250
  - lib/chewy/errors.rb
252
251
  - lib/chewy/fields/base.rb
@@ -454,7 +453,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
454
453
  - !ruby/object:Gem::Version
455
454
  version: '0'
456
455
  requirements: []
457
- rubygems_version: 3.1.2
456
+ rubygems_version: 3.2.32
458
457
  signing_key:
459
458
  specification_version: 4
460
459
  summary: Elasticsearch ODM client wrapper
@@ -1,46 +0,0 @@
1
- require 'chewy/backports/duplicable'
2
-
3
- class Object
4
- # Returns a deep copy of object if it's duplicable. If it's
5
- # not duplicable, returns +self+.
6
- #
7
- # object = Object.new
8
- # dup = object.deep_dup
9
- # dup.instance_variable_set(:@a, 1)
10
- #
11
- # object.instance_variable_defined?(:@a) # => false
12
- # dup.instance_variable_defined?(:@a) # => true
13
- def deep_dup
14
- duplicable? ? dup : self
15
- end
16
- end
17
-
18
- class Array
19
- # Returns a deep copy of array.
20
- #
21
- # array = [1, [2, 3]]
22
- # dup = array.deep_dup
23
- # dup[1][2] = 4
24
- #
25
- # array[1][2] # => nil
26
- # dup[1][2] # => 4
27
- def deep_dup
28
- map(&:deep_dup)
29
- end
30
- end
31
-
32
- class Hash
33
- # Returns a deep copy of hash.
34
- #
35
- # hash = { a: { b: 'b' } }
36
- # dup = hash.deep_dup
37
- # dup[:a][:c] = 'c'
38
- #
39
- # hash[:a][:c] # => nil
40
- # dup[:a][:c] # => "c"
41
- def deep_dup
42
- each_with_object(dup) do |(key, value), hash|
43
- hash[key.deep_dup] = value.deep_dup
44
- end
45
- end
46
- end
@@ -1,91 +0,0 @@
1
- #--
2
- # Most objects are cloneable, but not all. For example you can't dup +nil+:
3
- #
4
- # nil.dup # => TypeError: can't dup NilClass
5
- #
6
- # Classes may signal their instances are not duplicable removing +dup+/+clone+
7
- # or raising exceptions from them. So, to dup an arbitrary object you normally
8
- # use an optimistic approach and are ready to catch an exception, say:
9
- #
10
- # arbitrary_object.dup rescue object
11
- #
12
- # Rails dups objects in a few critical spots where they are not that arbitrary.
13
- # That rescue is very expensive (like 40 times slower than a predicate), and it
14
- # is often triggered.
15
- #
16
- # That's why we hardcode the following cases and check duplicable? instead of
17
- # using that rescue idiom.
18
- #++
19
- class Object
20
- # Can you safely dup this object?
21
- #
22
- # False for +nil+, +false+, +true+, symbol, and number objects;
23
- # true otherwise.
24
- def duplicable?
25
- true
26
- end
27
- end
28
-
29
- class NilClass
30
- # +nil+ is not duplicable:
31
- #
32
- # nil.duplicable? # => false
33
- # nil.dup # => TypeError: can't dup NilClass
34
- def duplicable?
35
- false
36
- end
37
- end
38
-
39
- class FalseClass
40
- # +false+ is not duplicable:
41
- #
42
- # false.duplicable? # => false
43
- # false.dup # => TypeError: can't dup FalseClass
44
- def duplicable?
45
- false
46
- end
47
- end
48
-
49
- class TrueClass
50
- # +true+ is not duplicable:
51
- #
52
- # true.duplicable? # => false
53
- # true.dup # => TypeError: can't dup TrueClass
54
- def duplicable?
55
- false
56
- end
57
- end
58
-
59
- class Symbol
60
- # Symbols are not duplicable:
61
- #
62
- # :my_symbol.duplicable? # => false
63
- # :my_symbol.dup # => TypeError: can't dup Symbol
64
- def duplicable?
65
- false
66
- end
67
- end
68
-
69
- class Numeric
70
- # Numbers are not duplicable:
71
- #
72
- # 3.duplicable? # => false
73
- # 3.dup # => TypeError: can't dup Fixnum
74
- def duplicable?
75
- false
76
- end
77
- end
78
-
79
- require 'bigdecimal'
80
- class BigDecimal
81
- begin
82
- BigDecimal('4.56').dup
83
-
84
- def duplicable?
85
- true
86
- end
87
- rescue TypeError
88
- # can't dup, so use superclass implementation
89
- nil
90
- end
91
- end