chewy 7.2.3 → 7.2.4

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.
@@ -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