From 492ed1279706a7fa039bc8397628cab4747c5e9b Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Thu, 18 Dec 2025 14:09:49 -0500 Subject: [PATCH] Forward `:context` option from `#save` and `#save!` Forward the `:context` option from `Base#save` and `Base#save!` to the underlying `Validations#valid?` call. This commit alos includes test coverage for directly calling `Validations#valid?` with a `context` value, since that behavior wasn't covered by tests elsewhere in the suite. --- Gemfile | 1 + lib/active_resource/base.rb | 17 ++++++++++++----- lib/active_resource/validations.rb | 2 +- test/cases/validations_test.rb | 26 ++++++++++++++++++++++++++ test/fixtures/project.rb | 1 + 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 473d9b60fd..215b3df34c 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,7 @@ gem "rubocop-performance" gem "rubocop-rails" gem "rubocop-rails-omakase" +gem "minitest", "< 6" gem "minitest-bisect" gemspec diff --git a/lib/active_resource/base.rb b/lib/active_resource/base.rb index 10312bb6f5..4e4a155243 100644 --- a/lib/active_resource/base.rb +++ b/lib/active_resource/base.rb @@ -1477,6 +1477,12 @@ def dup # is Json for the final object as it looked after the \save (which would include attributes like +created_at+ # that weren't part of the original submit). # + # With save validations run by default. If any of them fail + # ActiveResource::ResourceInvalid gets raised, and nothing is POSTed to + # the remote system. To skip validations, pass validate: false. To + # validate withing a custom context, pass the :context option. + # See ActiveResource::Validations for more information. + # # There's a series of callbacks associated with save. If any # of the before_* callbacks throw +:abort+ the action is # cancelled and save raises ActiveResource::ResourceInvalid. @@ -1489,7 +1495,7 @@ def dup # my_company.new? # => false # my_company.size = 10 # my_company.save # sends PUT /companies/1 (update) - def save + def save(**options) run_callbacks :save do new? ? create : _update end @@ -1500,16 +1506,17 @@ def save # If the resource is new, it is created via +POST+, otherwise the # existing resource is updated via +PUT+. # - # With save! validations always run. If any of them fail + # With save! validations run by default. If any of them fail # ActiveResource::ResourceInvalid gets raised, and nothing is POSTed to - # the remote system. + # the remote system. To skip validations, pass validate: false. To + # validate withing a custom context, pass the :context option. # See ActiveResource::Validations for more information. # # There's a series of callbacks associated with save!. If any # of the before_* callbacks throw +:abort+ the action is # cancelled and save! raises ActiveResource::ResourceInvalid. - def save! - save || raise(ResourceInvalid.new(self)) + def save!(**options) + save(**options) || raise(ResourceInvalid.new(self)) end ## diff --git a/lib/active_resource/validations.rb b/lib/active_resource/validations.rb index 4ad5ea6b50..9fa3e42fbb 100644 --- a/lib/active_resource/validations.rb +++ b/lib/active_resource/validations.rb @@ -283,7 +283,7 @@ def save_with_validation(options = {}) # ones. Otherwise we get an endless loop and can never change the # fields so as to make the resource valid. @remote_errors = nil - if perform_validation && valid? || !perform_validation + if perform_validation && valid?(options[:context]) || !perform_validation save_without_validation true else diff --git a/test/cases/validations_test.rb b/test/cases/validations_test.rb index 84e1d5fbe5..b8d6b55d1a 100644 --- a/test/cases/validations_test.rb +++ b/test/cases/validations_test.rb @@ -32,12 +32,30 @@ def test_fails_save! assert_raise(ActiveResource::ResourceInvalid) { p.save! } end + def test_save_with_context + p = new_project(summary: nil) + assert_not p.save(context: :completed) + assert_equal [ "can't be blank" ], p.errors.messages_for(:summary) + end + + def test_save_bang_with_context + p = new_project(summary: nil) + assert_raise(ActiveResource::ResourceInvalid) { p.save!(context: :completed) } + assert_equal [ "can't be blank" ], p.errors.messages_for(:summary) + end + def test_save_without_validation p = new_project(name: nil) assert_not p.save assert p.save(validate: false) end + def test_save_bang_without_validation + p = new_project(name: nil) + assert_raises(ActiveResource::ResourceInvalid) { p.save! } + assert p.save!(validate: false) + end + def test_validate_callback # we have a callback ensuring the description is longer than three letters p = new_project(description: "a") @@ -56,6 +74,14 @@ def test_client_side_validation_maximum assert_equal [ "is too long (maximum is 10 characters)" ], project.errors[:description] end + def test_validation_context + project = new_project(summary: "") + + assert_predicate project, :valid? + assert_not project.valid?(:completed) + assert_equal [ "can't be blank" ], project.errors.messages_for(:summary) + end + def test_invalid_method p = new_project diff --git a/test/fixtures/project.rb b/test/fixtures/project.rb index e9f41f0672..58c5e0a13f 100644 --- a/test/fixtures/project.rb +++ b/test/fixtures/project.rb @@ -11,6 +11,7 @@ class Project < ActiveResource::Base validates :name, presence: true validates :description, presence: false, length: { maximum: 10 } validate :description_greater_than_three_letters + validates :summary, presence: { on: :completed } # to test the validate *callback* works def description_greater_than_three_letters