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 3c8a4bee14..42965a890b 100644
--- a/lib/active_resource/base.rb
+++ b/lib/active_resource/base.rb
@@ -1472,6 +1472,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.
@@ -1484,7 +1490,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
@@ -1495,16 +1501,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(nil, self))
+ def save!(**options)
+ save(**options) || raise(ResourceInvalid.new(nil, self))
end
##
diff --git a/lib/active_resource/validations.rb b/lib/active_resource/validations.rb
index 4ad5ea6b50..1c86154a42 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
@@ -314,6 +314,14 @@ def load_remote_errors(remote_errors, save_cache = false) # :nodoc:
# saved.
# Remote errors can only be cleared by trying to re-save the resource.
#
+ # If the argument is +false+ (default is +nil+), the context is set to :create if
+ # {new_record?}[rdoc-ref:Base#new_record?] is +true+, and to :update if it is not.
+ # If the argument is an array of contexts, post.valid?([:create, :update]), the validations are
+ # run within multiple contexts.
+ #
+ # \Validations with no :on option will run no matter the context. \Validations with
+ # some :on option will only run in the specified context.
+ #
# ==== Examples
# my_person = Person.create(params[:person])
# my_person.valid?
@@ -324,6 +332,8 @@ def load_remote_errors(remote_errors, save_cache = false) # :nodoc:
# # => false
#
def valid?(context = nil)
+ context ||= new_record? ? :create : :update
+
run_callbacks :validate do
super
load_remote_errors(@remote_errors, true) if defined?(@remote_errors) && @remote_errors.present?
diff --git a/test/cases/validations_test.rb b/test/cases/validations_test.rb
index 84e1d5fbe5..8317e00e83 100644
--- a/test/cases/validations_test.rb
+++ b/test/cases/validations_test.rb
@@ -8,6 +8,13 @@
# This test case simply makes sure that they are all accessible by
# Active Resource objects.
class ValidationsTest < ActiveSupport::TestCase
+ class DraftProject < ::Project
+ self.element_name = "project"
+
+ validates :name, format: { with: /\ACREATE:/, on: :create }
+ validates :name, format: { with: /\AUPDATE:/, on: :update }
+ end
+
VALID_PROJECT_HASH = { name: "My Project", description: "A project" }
def setup
@my_proj = { "person" => VALID_PROJECT_HASH }.to_json
@@ -32,12 +39,57 @@ 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_with_default_create_context
+ p = DraftProject.new VALID_PROJECT_HASH.merge(name: "Invalid")
+ assert_not p.save
+ assert_equal [ "is invalid" ], p.errors.messages_for(:name)
+
+ p.name = "CREATE: Valid"
+ assert p.save, "should save"
+ assert_empty p.errors
+ end
+
+ def test_save_with_default_update_context
+ attributes = VALID_PROJECT_HASH.merge(id: 1, name: "CREATE: Created")
+
+ ActiveResource::HttpMock.respond_to do |mock|
+ mock.get "/projects/1.json", {}, attributes.to_json
+ mock.put "/projects/1.json", {}, attributes.to_json
+ end
+
+ p = DraftProject.find(1)
+ assert_not p.save
+ assert_equal [ "is invalid" ], p.errors.messages_for(:name)
+
+ p.name = "UPDATE: Updated"
+ assert p.save, "should save"
+ assert_empty p.errors
+ 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 +108,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