Skip to content

Commit 7a537be

Browse files
committed
Implement #dup and #clone for structs
1 parent fe38e8f commit 7a537be

File tree

7 files changed

+142
-0
lines changed

7 files changed

+142
-0
lines changed

lib/concurrent/immutable_struct.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ def select(&block)
7070
ns_select(&block)
7171
end
7272

73+
# @!macro struct_initialize_copy
74+
#
75+
# @!visibility private
76+
def initialize_copy(original)
77+
super
78+
ns_initialize_copy
79+
end
80+
7381
# @!macro struct_new
7482
def self.new(*args, &block)
7583
clazz_name = nil

lib/concurrent/mutable_struct.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,16 @@ def select(&block)
169169
synchronize { ns_select(&block) }
170170
end
171171

172+
# @!macro struct_initialize_copy
173+
#
174+
# @!visibility private
175+
def initialize_copy(original)
176+
synchronize do
177+
super
178+
ns_initialize_copy
179+
end
180+
end
181+
172182
# @!macro struct_set
173183
#
174184
# Attribute Assignment

lib/concurrent/settable_struct.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ def select(&block)
6969
synchronize { ns_select(&block) }
7070
end
7171

72+
# @!macro struct_initialize_copy
73+
#
74+
# @!visibility private
75+
def initialize_copy(original)
76+
synchronize do
77+
super
78+
ns_initialize_copy
79+
end
80+
end
81+
7282
# @!macro struct_set
7383
#
7484
# @raise [Concurrent::ImmutabilityError] if the given member has already been set

lib/concurrent/synchronization/abstract_struct.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ def ns_merge(other, &block)
115115
self.class.new(*self.to_h.merge(other, &block).values)
116116
end
117117

118+
# @!macro struct_initialize_copy
119+
#
120+
# @!visibility private
121+
def ns_initialize_copy
122+
@values = @values.map(&:clone)
123+
end
124+
118125
# @!visibility private
119126
def pr_underscore(clazz)
120127
word = clazz.to_s.dup # dup string to workaround JRuby 9.2.0.0 bug https://github.com/jruby/jruby/issues/5229

spec/concurrent/mutable_struct_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,25 @@ module Concurrent
149149
expect(subject).to receive(:synchronize).at_least(:once).with(no_args).and_call_original
150150
subject.select{|value| false }
151151
end
152+
153+
it 'protects #initialize_copy' do
154+
expect(subject).to receive(:synchronize).at_least(:once).with(no_args).and_call_original
155+
subject.clone
156+
end
157+
end
158+
159+
context 'copy' do
160+
context '#dup' do
161+
it 'mutates only the copy' do
162+
expect(subject.dup.name = 'Jane').not_to eq(subject.name)
163+
end
164+
end
165+
166+
context '#clone' do
167+
it 'mutates only the copy' do
168+
expect(subject.clone.name = 'Jane').not_to eq(subject.name)
169+
end
170+
end
152171
end
153172
end
154173
end

spec/concurrent/settable_struct_spec.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,31 @@ module Concurrent
171171
expect(subject).to receive(:synchronize).at_least(:once).with(no_args).and_call_original
172172
subject.select{|value| false }
173173
end
174+
175+
it 'protects #initialize_copy' do
176+
expect(subject).to receive(:synchronize).at_least(:once).with(no_args).and_call_original
177+
subject.clone
178+
end
179+
end
180+
181+
context 'copy' do
182+
context '#dup' do
183+
it 'retains settability of members' do
184+
subject['name'] = 'John'
185+
expect { subject.dup['name'] = 'Jane' }
186+
.to raise_error(ImmutabilityError)
187+
expect(subject['address'] = 'Earth').to eq('Earth')
188+
end
189+
end
190+
191+
context '#clone' do
192+
it 'retains settability of members' do
193+
subject['name'] = 'John'
194+
expect { subject.clone['name'] = 'Jane' }
195+
.to raise_error(ImmutabilityError)
196+
expect(subject['address'] = 'Earth').to eq('Earth')
197+
end
198+
end
174199
end
175200
end
176201
end

spec/concurrent/struct_shared.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,69 @@ def baz(foo, bar) foo + bar; end
437437
end
438438
end
439439
end
440+
441+
context 'copy' do
442+
let(:this) do
443+
described_class.new(:foo, :bar, :baz).new('foo'.freeze, ['bar'], 42)
444+
end
445+
446+
context '#dup' do
447+
it 'shallowly duplicates all members along with the struct' do
448+
copy = this.dup
449+
expect(copy.foo).not_to be this.foo
450+
expect(copy.bar).not_to be this.bar
451+
expect(copy.bar.first).to be this.bar.first
452+
expect(copy.baz).to be this.baz
453+
end
454+
455+
it 'discards frozen state of the struct' do
456+
expect(this.freeze.dup).not_to be_frozen
457+
end
458+
459+
it 'retains frozen state of members' do
460+
expect(this.dup.foo).to be_frozen
461+
end
462+
463+
it 'discards singleton class' do
464+
this.define_singleton_method(:qux) { 'qux' }
465+
expect(this.qux).to eq('qux')
466+
expect{this.dup.qux}.to raise_error(NoMethodError)
467+
end
468+
469+
it 'copies the singleton class of members' do
470+
this.bar.define_singleton_method(:qux) { 'qux' }
471+
expect(this.bar.qux).to eq('qux')
472+
expect(this.dup.bar.qux).to eq('qux')
473+
end
474+
end
475+
476+
context '#clone' do
477+
it 'shallowly clones all members along with the struct' do
478+
copy = this.clone
479+
expect(copy.foo).not_to be this.foo
480+
expect(copy.bar).not_to be this.bar
481+
expect(copy.bar.first).to be this.bar.first
482+
expect(copy.baz).to be this.baz
483+
end
484+
485+
it 'retains frozen state' do
486+
expect(this.freeze.clone).to be_frozen
487+
expect(this.clone.foo).to be_frozen
488+
end
489+
490+
it 'copies the singleton class' do
491+
this.define_singleton_method(:qux) { 'qux' }
492+
expect(this.qux).to eq('qux')
493+
expect(this.clone.qux).to eq('qux')
494+
end
495+
496+
it 'copies the singleton class of members' do
497+
this.bar.define_singleton_method(:qux) { 'qux' }
498+
expect(this.bar.qux).to eq('qux')
499+
expect(this.clone.bar.qux).to eq('qux')
500+
end
501+
end
502+
end
440503
end
441504

442505
RSpec.shared_examples :mergeable_struct do

0 commit comments

Comments
 (0)