diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4e0828..59183c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,14 @@ jobs: strategy: fail-fast: false matrix: - ruby-version: ['2.7', '3.0', '3.1', '3.2'] + ruby-version: + - '3.2' + - '3.3' + - '3.4' + - 'ruby-head' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 diff --git a/Gemfile b/Gemfile index ea82bc2..87a1fb0 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,8 @@ group :development, :test do gem 'mutant', github: 'mbj/mutant' gem 'mutant-rspec', github: 'mbj/mutant' + gem 'bigdecimal' + source 'https://oss:sxCL1o1navkPi2XnGB5WYBrhpY9iKIPL@gem.mutant.dev' do gem 'mutant-license' end diff --git a/config/mutant.yml b/config/mutant.yml index 54be0c4..68d0e60 100644 --- a/config/mutant.yml +++ b/config/mutant.yml @@ -10,3 +10,4 @@ mutation: timeout: 1.0 coverage_criteria: timeout: true +usage: opensource diff --git a/lib/ice_nine.rb b/lib/ice_nine.rb index a659785..eece797 100644 --- a/lib/ice_nine.rb +++ b/lib/ice_nine.rb @@ -6,6 +6,7 @@ require 'ice_nine/freezer/object' require 'ice_nine/freezer/no_freeze' require 'ice_nine/freezer/array' +require 'ice_nine/freezer/data' require 'ice_nine/freezer/false_class' require 'ice_nine/freezer/hash' diff --git a/lib/ice_nine/freezer/data.rb b/lib/ice_nine/freezer/data.rb new file mode 100644 index 0000000..4b8857e --- /dev/null +++ b/lib/ice_nine/freezer/data.rb @@ -0,0 +1,30 @@ +# encoding: utf-8 + +module IceNine + class Freezer + + # A freezer class for handling Data objects + class Data < Object + + # Deep Freeze a Data object + # + # @example + # Person = Data.define(:name, :age) + # person = Person.new(name: 'John', age: 30) + # frozen_person = IceNine::Freezer::Data.deep_freeze(person) + # frozen_person.name.frozen? # => true + # + # @param [Data] data + # @param [RecursionGuard] recursion_guard + # + # @return [Data] + def self.guarded_deep_freeze(data, recursion_guard) + data.to_h.each_value do |value| + Freezer.guarded_deep_freeze(value, recursion_guard) + end + data + end + + end # Data + end # Freezer +end # IceNine diff --git a/spec/shared/data_deep_freeze.rb b/spec/shared/data_deep_freeze.rb new file mode 100644 index 0000000..c3ba086 --- /dev/null +++ b/spec/shared/data_deep_freeze.rb @@ -0,0 +1,17 @@ +# encoding: utf-8 + +shared_examples 'IceNine::Freezer::Data.deep_freeze' do + it 'returns the object' do + should be(value) + end + + it 'keeps the object frozen' do + expect(subject).to be_frozen + end + + it 'freezes each attribute value' do + subject.to_h.each_value do |attribute_value| + expect(attribute_value).to be_frozen + end + end +end diff --git a/spec/unit/ice_nine/freezer/data/class_methods/deep_freeze_spec.rb b/spec/unit/ice_nine/freezer/data/class_methods/deep_freeze_spec.rb new file mode 100644 index 0000000..2018bf5 --- /dev/null +++ b/spec/unit/ice_nine/freezer/data/class_methods/deep_freeze_spec.rb @@ -0,0 +1,58 @@ +# encoding: utf-8 + +require 'spec_helper' +require 'ice_nine' + +describe IceNine::Freezer::Data, '.deep_freeze' do + subject { object.deep_freeze(value) } + + let(:object) { described_class } + + context 'with a Data object' do + let(:klass) { Data.define(:name, :age) } + let(:value) { klass.new(name: 'John', age: 30) } + + context 'without a circular reference' do + it_behaves_like 'IceNine::Freezer::Data.deep_freeze' + end + + context 'with a circular reference' do + let(:klass) { Data.define(:name, :age, :refs) } + let(:refs) { [] } + let(:value) { klass.new(name: 'John', age: 30, refs: refs) } + + before { refs << value } + + it_behaves_like 'IceNine::Freezer::Data.deep_freeze' + end + + context 'with nested Data objects' do + let(:address_class) { Data.define(:street, :city) } + let(:person_class) { Data.define(:name, :address) } + let(:address) { address_class.new(street: '123 Main St', city: 'Anytown') } + let(:value) { person_class.new(name: 'Jane', address: address) } + + it_behaves_like 'IceNine::Freezer::Data.deep_freeze' + + it 'deeply freezes nested Data objects' do + expect(subject.address).to be_frozen + expect(subject.address.street).to be_frozen + expect(subject.address.city).to be_frozen + end + end + + context 'with mutable values' do + let(:klass) { Data.define(:items, :metadata) } + let(:value) { klass.new(items: ['apple', 'banana'], metadata: { count: 2 }) } + + it_behaves_like 'IceNine::Freezer::Data.deep_freeze' + + it 'deeply freezes attribute values' do + expect(subject.items).to be_frozen + expect(subject.items.first).to be_frozen + expect(subject.metadata).to be_frozen + expect(subject.metadata[:count]).to be_frozen + end + end + end +end