Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ gem "rubocop-performance"
gem "rubocop-rails"
gem "rubocop-rails-omakase"

gem "minitest", "< 6"
gem "minitest-bisect"

gemspec
Expand Down
17 changes: 12 additions & 5 deletions lib/active_resource/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tt>save</tt> 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 <tt>validate: false</tt>. To
# validate withing a custom context, pass the <tt>:context</tt> option.
# See ActiveResource::Validations for more information.
#
# There's a series of callbacks associated with <tt>save</tt>. If any
# of the <tt>before_*</tt> callbacks throw +:abort+ the action is
# cancelled and <tt>save</tt> raises ActiveResource::ResourceInvalid.
Expand All @@ -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
Expand All @@ -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 <tt>save!</tt> validations always run. If any of them fail
# With <tt>save!</tt> 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 <tt>validate: false</tt>. To
# validate withing a custom context, pass the <tt>:context</tt> option.
# See ActiveResource::Validations for more information.
#
# There's a series of callbacks associated with <tt>save!</tt>. If any
# of the <tt>before_*</tt> callbacks throw +:abort+ the action is
# cancelled and <tt>save!</tt> raises ActiveResource::ResourceInvalid.
def save!
save || raise(ResourceInvalid.new(nil, self))
def save!(**options)
save(**options) || raise(ResourceInvalid.new(nil, self))
end

##
Expand Down
12 changes: 11 additions & 1 deletion lib/active_resource/validations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <tt>:create</tt> if
# {new_record?}[rdoc-ref:Base#new_record?] is +true+, and to <tt>:update</tt> if it is not.
# If the argument is an array of contexts, <tt>post.valid?([:create, :update])</tt>, the validations are
# run within multiple contexts.
#
# \Validations with no <tt>:on</tt> option will run no matter the context. \Validations with
# some <tt>:on</tt> option will only run in the specified context.
#
# ==== Examples
# my_person = Person.create(params[:person])
# my_person.valid?
Expand All @@ -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?
Expand Down
60 changes: 60 additions & 0 deletions test/cases/validations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions test/fixtures/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down