Skip to content

Commit

Permalink
Merge pull request #16056 from sgrif/sg-required-associations
Browse files Browse the repository at this point in the history
Add a `required` option to singular associations
  • Loading branch information
dhh committed Jul 5, 2014
2 parents a6cc7b0 + 00f5551 commit 3ac8118
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 1 deletion.
10 changes: 10 additions & 0 deletions activerecord/lib/active_record/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,10 @@ def has_many(name, scope = nil, options = {}, &extension)
# that is the inverse of this <tt>has_one</tt> association. Does not work in combination
# with <tt>:through</tt> or <tt>:as</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:required]
# When set to +true+, the association will also have its presence validated.
# This will validate the association itself, not the id. You can use
# +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
Expand All @@ -1320,6 +1324,7 @@ def has_many(name, scope = nil, options = {}, &extension)
# has_one :boss, readonly: :true
# has_one :club, through: :membership
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
# has_one :credit_card, required: true
def has_one(name, scope = nil, options = {})
reflection = Builder::HasOne.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
Expand Down Expand Up @@ -1421,6 +1426,10 @@ def has_one(name, scope = nil, options = {})
# object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
# combination with the <tt>:polymorphic</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:required]
# When set to +true+, the association will also have its presence validated.
# This will validate the association itself, not the id. You can use
# +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# belongs_to :firm, foreign_key: "client_of"
Expand All @@ -1433,6 +1442,7 @@ def has_one(name, scope = nil, options = {})
# belongs_to :post, counter_cache: true
# belongs_to :company, touch: true
# belongs_to :company, touch: :employees_last_updated_at
# belongs_to :company, required: true
def belongs_to(name, scope = nil, options = {})
reflection = Builder::BelongsTo.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def self.build(model, name, scope, options, &block)
reflection = builder.build(model)
define_accessors model, reflection
define_callbacks model, reflection
define_validations model, reflection
builder.define_extensions model
reflection
end
Expand Down Expand Up @@ -124,6 +125,10 @@ def #{name}=(value)
CODE
end

def self.define_validations(model, reflection)
# noop
end

def self.valid_dependent_options
raise NotImplementedError
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
def valid_options
super + [:remote, :dependent, :primary_key, :inverse_of]
super + [:remote, :dependent, :primary_key, :inverse_of, :required]
end

def self.define_accessors(model, reflection)
Expand All @@ -27,5 +27,12 @@ def create_#{name}!(*args, &block)
end
CODE
end

def self.define_validations(model, reflection)
super
if reflection.options[:required]
model.validates_presence_of reflection.name
end
end
end
end
82 changes: 82 additions & 0 deletions activerecord/test/cases/associations/required_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require "cases/helper"

class RequiredAssociationsTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false

class Parent < ActiveRecord::Base
end

class Child < ActiveRecord::Base
end

setup do
@connection = ActiveRecord::Base.connection
@connection.create_table :parents, force: true
@connection.create_table :children, force: true do |t|
t.belongs_to :parent
end
end

teardown do
@connection.execute("DROP TABLE IF EXISTS parents")
@connection.execute("DROP TABLE IF EXISTS children")
end

test "belongs_to associations are not required by default" do
model = subclass_of(Child) do
belongs_to :parent, inverse_of: false,
class_name: "RequiredAssociationsTest::Parent"
end

assert model.new.save
assert model.new(parent: Parent.new).save
end

test "required belongs_to associations have presence validated" do
model = subclass_of(Child) do
belongs_to :parent, required: true, inverse_of: false,
class_name: "RequiredAssociationsTest::Parent"
end

record = model.new
assert_not record.save
assert_equal ["Parent can't be blank"], record.errors.full_messages

record.parent = Parent.new
assert record.save
end

test "has_one associations are not required by default" do
model = subclass_of(Parent) do
has_one :child, inverse_of: false,
class_name: "RequiredAssociationsTest::Child"
end

assert model.new.save
assert model.new(child: Child.new).save
end

test "required has_one associations have presence validated" do
model = subclass_of(Parent) do
has_one :child, required: true, inverse_of: false,
class_name: "RequiredAssociationsTest::Child"
end

record = model.new
assert_not record.save
assert_equal ["Child can't be blank"], record.errors.full_messages

record.child = Child.new
assert record.save
end

private

def subclass_of(klass, &block)
subclass = Class.new(klass, &block)
def subclass.name
superclass.name
end
subclass
end
end

0 comments on commit 3ac8118

Please sign in to comment.