From 41100b0c2a2ffe505f420a58af4efb2c9ab3423d Mon Sep 17 00:00:00 2001 From: Edem Topuzov Date: Fri, 5 Jul 2024 18:13:05 +0300 Subject: [PATCH 1/5] Freeze transform_to_value --- README.md | 2 +- datacaster.gemspec | 2 +- lib/datacaster/absent.rb | 4 ++++ lib/datacaster/predefined.rb | 2 +- lib/datacaster/utils.rb | 4 ++++ spec/datacaster_spec.rb | 23 +++++++++++++++++++++-- 6 files changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1697744..f194bc6 100644 --- a/README.md +++ b/README.md @@ -847,7 +847,7 @@ I18n keys: #### `transform_to_value(value)` -Always returns ValidResult. The value is transformed to provided argument (disregarding the original value). See also [`default`](#defaultdefault_value-on-nil). +Always returns ValidResult. The value is transformed to provided argument (disregarding the original value). Returned value deeply freezes with [`Ractor::make_shareable`](https://docs.ruby-lang.org/en/master/Ractor.html#method-c-make_shareable). See also [`default`](#defaultdefault_value-on-nil). ### "Web-form" types diff --git a/datacaster.gemspec b/datacaster.gemspec index 76276c0..eaf615f 100644 --- a/datacaster.gemspec +++ b/datacaster.gemspec @@ -8,7 +8,7 @@ Gem::Specification.new do |spec| spec.summary = %q{Run-time type checker and transformer for Ruby} spec.license = 'MIT' - spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0') + spec.required_ruby_version = Gem::Requirement.new('>= 3') spec.metadata['source_code_uri'] = 'https://github.com/EugZol/datacaster' spec.homepage = 'https://github.com/EugZol/datacaster' diff --git a/lib/datacaster/absent.rb b/lib/datacaster/absent.rb index 517eb05..29ece75 100644 --- a/lib/datacaster/absent.rb +++ b/lib/datacaster/absent.rb @@ -19,5 +19,9 @@ def to_s def present? false end + + def ==(other) + other.is_a?(self.class) + end end end diff --git a/lib/datacaster/predefined.rb b/lib/datacaster/predefined.rb index 8b15aca..832046c 100644 --- a/lib/datacaster/predefined.rb +++ b/lib/datacaster/predefined.rb @@ -276,7 +276,7 @@ def switch(base = nil, **on_clauses) end def transform_to_value(value) - transform { value } + transform { Datacaster::Utils.deep_freeze(value) } end def with(keys, caster) diff --git a/lib/datacaster/utils.rb b/lib/datacaster/utils.rb index cd19596..e1f41fc 100644 --- a/lib/datacaster/utils.rb +++ b/lib/datacaster/utils.rb @@ -2,6 +2,10 @@ module Datacaster module Utils extend self + def deep_freeze(value, copy: true) + Ractor.make_shareable(value, copy:) + end + def merge_errors(left, right) add_error_to_base = ->(hash, error) { hash[:base] ||= [] diff --git a/spec/datacaster_spec.rb b/spec/datacaster_spec.rb index bb6b386..c6f9198 100644 --- a/spec/datacaster_spec.rb +++ b/spec/datacaster_spec.rb @@ -1033,13 +1033,32 @@ end describe "constant mapping" do + let(:transform_to_value_caster) do + Datacaster.schema { transform_to_value({ a: { b: { c: "d" } } }) } + end + + let(:returned_value) do + transform_to_value_caster.(123).value! + end + it "returns exact value" do - t = Datacaster.schema { transform_to_value("Test") } + expect(returned_value).to eq({ a: { b: { c: "d" } } }) + end - expect(t.(123).to_dry_result).to eq Success("Test") + it "freezes returned value" do + expect(returned_value).to be_frozen + end + + it "freezes deeply" do + expect { returned_value[:a][:b][:c].upcase! }.to raise_error FrozenError + end + + it "becomes shareable" do + expect(Ractor.shareable?(returned_value)).to be true end end + describe "pass_if casting" do subject { Datacaster.schema { pass_if(check { |x| x[:test] == 0 }) } } From d5ee56cc9008989941d09363be31814ed30d7667 Mon Sep 17 00:00:00 2001 From: Edem Topuzov Date: Fri, 5 Jul 2024 18:18:44 +0300 Subject: [PATCH 2/5] Bump Ruby version --- .github/workflows/rspec.yml | 2 +- datacaster.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rspec.yml b/.github/workflows/rspec.yml index c4872f1..c5e52be 100644 --- a/.github/workflows/rspec.yml +++ b/.github/workflows/rspec.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby-version: ['2.7', '3.0', '3.1'] + ruby-version: ['3.1', '3.2', '3.3'] steps: - uses: actions/checkout@v3 - name: Set up Ruby diff --git a/datacaster.gemspec b/datacaster.gemspec index eaf615f..97bab05 100644 --- a/datacaster.gemspec +++ b/datacaster.gemspec @@ -8,7 +8,7 @@ Gem::Specification.new do |spec| spec.summary = %q{Run-time type checker and transformer for Ruby} spec.license = 'MIT' - spec.required_ruby_version = Gem::Requirement.new('>= 3') + spec.required_ruby_version = Gem::Requirement.new('>= 3.1') spec.metadata['source_code_uri'] = 'https://github.com/EugZol/datacaster' spec.homepage = 'https://github.com/EugZol/datacaster' From 4cea18b1940c29fd35d69ee0e3f86cde88ba6434 Mon Sep 17 00:00:00 2001 From: Edem Topuzov Date: Fri, 5 Jul 2024 18:21:03 +0300 Subject: [PATCH 3/5] Remove extra line --- spec/datacaster_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/datacaster_spec.rb b/spec/datacaster_spec.rb index c6f9198..0bc92cf 100644 --- a/spec/datacaster_spec.rb +++ b/spec/datacaster_spec.rb @@ -1058,7 +1058,6 @@ end end - describe "pass_if casting" do subject { Datacaster.schema { pass_if(check { |x| x[:test] == 0 }) } } From 155120a7f6ffc7b3de9ac0bc196a18d4d2be019b Mon Sep 17 00:00:00 2001 From: Edem Topuzov Date: Fri, 5 Jul 2024 18:32:19 +0300 Subject: [PATCH 4/5] Change test case --- spec/datacaster_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/datacaster_spec.rb b/spec/datacaster_spec.rb index 0bc92cf..b70168b 100644 --- a/spec/datacaster_spec.rb +++ b/spec/datacaster_spec.rb @@ -1034,7 +1034,7 @@ describe "constant mapping" do let(:transform_to_value_caster) do - Datacaster.schema { transform_to_value({ a: { b: { c: "d" } } }) } + Datacaster.schema { transform_to_value({ a: { b: { c: ["d"] } } }) } end let(:returned_value) do @@ -1042,7 +1042,7 @@ end it "returns exact value" do - expect(returned_value).to eq({ a: { b: { c: "d" } } }) + expect(returned_value).to eq({ a: { b: { c: ["d"] } } }) end it "freezes returned value" do @@ -1050,7 +1050,7 @@ end it "freezes deeply" do - expect { returned_value[:a][:b][:c].upcase! }.to raise_error FrozenError + expect { returned_value[:a][:b][:c].pop }.to raise_error FrozenError end it "becomes shareable" do From 0aa51e278c160421595233ce3c8bb1a449fc577d Mon Sep 17 00:00:00 2001 From: Edem Topuzov Date: Fri, 5 Jul 2024 18:38:50 +0300 Subject: [PATCH 5/5] Review changes --- README.md | 6 +++++- lib/datacaster/version.rb | 2 +- spec/datacaster_spec.rb | 4 ---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f194bc6..e094675 100644 --- a/README.md +++ b/README.md @@ -847,7 +847,11 @@ I18n keys: #### `transform_to_value(value)` -Always returns ValidResult. The value is transformed to provided argument (disregarding the original value). Returned value deeply freezes with [`Ractor::make_shareable`](https://docs.ruby-lang.org/en/master/Ractor.html#method-c-make_shareable). See also [`default`](#defaultdefault_value-on-nil). +Always returns ValidResult. The value is transformed to provided argument (disregarding the original value). + +Returned value is deeply freezed with [`Ractor::make_shareable`](https://docs.ruby-lang.org/en/master/Ractor.html#method-c-make_shareable) to prevent application bugs due to modification of unintentionally shared value. If that effect is undesired, use [`transform { value }`](#transform--value--) instead. + +See also [`default`](#defaultdefault_value-on-nil). ### "Web-form" types diff --git a/lib/datacaster/version.rb b/lib/datacaster/version.rb index c11d121..f2deeb7 100644 --- a/lib/datacaster/version.rb +++ b/lib/datacaster/version.rb @@ -1,3 +1,3 @@ module Datacaster - VERSION = "3.2.8" + VERSION = "3.3.0" end diff --git a/spec/datacaster_spec.rb b/spec/datacaster_spec.rb index b70168b..2a140b8 100644 --- a/spec/datacaster_spec.rb +++ b/spec/datacaster_spec.rb @@ -1052,10 +1052,6 @@ it "freezes deeply" do expect { returned_value[:a][:b][:c].pop }.to raise_error FrozenError end - - it "becomes shareable" do - expect(Ractor.shareable?(returned_value)).to be true - end end describe "pass_if casting" do