Skip to content

Commit c85e099

Browse files
authored
Merge pull request #838 from kaikuchn/master
Implement #dup and #clone for Structs
2 parents fe38e8f + 70a74b1 commit c85e099

File tree

7 files changed

+146
-0
lines changed

7 files changed

+146
-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+
private
74+
75+
# @!visibility private
76+
def initialize_copy(original)
77+
super(original)
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
@@ -196,6 +196,16 @@ def []=(member, value)
196196
raise NameError.new("no member '#{member}' in struct")
197197
end
198198

199+
private
200+
201+
# @!visibility private
202+
def initialize_copy(original)
203+
synchronize do
204+
super(original)
205+
ns_initialize_copy
206+
end
207+
end
208+
199209
# @!macro struct_new
200210
def self.new(*args, &block)
201211
clazz_name = nil

lib/concurrent/settable_struct.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ def []=(member, value)
9191
raise NameError.new("no member '#{member}' in struct")
9292
end
9393

94+
private
95+
96+
# @!visibility private
97+
def initialize_copy(original)
98+
synchronize do
99+
super(original)
100+
ns_initialize_copy
101+
end
102+
end
103+
94104
# @!macro struct_new
95105
def self.new(*args, &block)
96106
clazz_name = nil

lib/concurrent/synchronization/abstract_struct.rb

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

118+
# @!visibility private
119+
def ns_initialize_copy
120+
@values = @values.map do |val|
121+
begin
122+
val.clone
123+
rescue TypeError
124+
val
125+
end
126+
end
127+
end
128+
118129
# @!visibility private
119130
def pr_underscore(clazz)
120131
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)