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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ class Page

# default from a singleton value (boolean in this case)
attribute :published, Boolean, :default => false

# default from a singleton value (string in this case),
# that doesn't allow a nil value
attribute :author, String, :default => 'Mies', :allow_nil => false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the example should use a default, because it's much less likely that you'd need to worry about nil if you've got a default.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you're suggesting to use the default if nil is passed in. Hmm. That's a harder sell.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need :allow_nil => false when you send a default?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@envygeeks Maybe this example explains it the best:

class Page
  include Virtus.model

  attribute :title, String, :default => 'New page', :allow_nil => true
  attribute :author, String, :default => 'Mies', :allow_nil => false
end

page = Page.new(title: nil, author: nil)
page.title     # => nil
page.author    # => 'Mies'

With the :allow_nil => false option, the default is used when the attributes is assigned as nil.

Edit: Just saw that I published a gist that describes the behaviour as well.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you talk more about the differences between page = Page.new(title: nil, author: nil) and page = Page.new?


# default from a callable object (proc in this case)
attribute :slug, String, :default => lambda { |page, attribute| page.title.downcase.gsub(' ', '-') }
Expand All @@ -169,10 +173,11 @@ class Page
end
end

page = Page.new(:title => 'Virtus README')
page = Page.new(:title => 'Virtus README', :author => nil)
page.slug # => 'virtus-readme'
page.views # => 0
page.published # => false
page.author # => 'Mies'
page.editor_title # => "UNPUBLISHED: Virtus README"

page.views = 10
Expand Down
20 changes: 19 additions & 1 deletion lib/virtus/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ class Attribute

include ::Equalizer.new(:type, :options)

accept_options :primitive, :accessor, :default, :lazy, :strict, :required, :finalize
accept_options :primitive, :accessor, :default, :lazy, :strict, :required, :finalize, :allow_nil

strict false
required true
accessor :public
finalize true
allow_nil true

# @see Virtus.coerce
#
Expand Down Expand Up @@ -210,6 +211,23 @@ def finalized?
frozen?
end

# Return if the attribute accepts nil as value when default value is set
#
# @example
#
# attr = Virtus::Attribute.build(String, :default => 'foo', :allow_nil => true)
# attr.allow_nil? # => true
#
# attr = Virtus::Attribute.build(String, :default => 'foo', :allow_nil => false)
# attr.allow_nil? # => false
#
# @return [Boolean]
#
# @api public
def allow_nil?
options[:allow_nil]
end

# @api private
def define_accessor_methods(attribute_set)
attribute_set.define_reader_method(self, name, options[:reader])
Expand Down
12 changes: 12 additions & 0 deletions lib/virtus/attribute/accessor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ def self.extended(descendant)
descendant.instance_variable_set('@instance_variable_name', "@#{name}")
end

# Return if attribute value is something other than
# a nil value when attribute does not accept nil values
#
# @param [Object] instance
#
# @return [Boolean]
#
# @api public
def present?(instance)
self.allow_nil? ? self.defined?(instance) : !instance.instance_variable_get(instance_variable_name).nil?
end

# Return if attribute value is defined
#
# @param [Object] instance
Expand Down
2 changes: 1 addition & 1 deletion lib/virtus/attribute/lazy_default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module LazyDefault

# @api public
def get(instance)
if instance.instance_variable_defined?(instance_variable_name)
if present?(instance)
super
else
set_default_value(instance)
Expand Down
2 changes: 1 addition & 1 deletion lib/virtus/attribute_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def finalize

# @api private
def skip_default?(object, attribute)
attribute.lazy? || attribute.defined?(object)
attribute.lazy? || attribute.present?(object)
end

# Merge the attributes into the index
Expand Down
8 changes: 8 additions & 0 deletions spec/integration/default_values_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Page
include Virtus

attribute :title, String
attribute :author, String, :default => 'Mies', :allow_nil => false
attribute :slug, String, :default => lambda { |post, attribute| post.title.downcase.gsub(' ', '-') }, :lazy => true
attribute :view_count, Integer, :default => 0
attribute :published, Boolean, :default => false, :accessor => :private
Expand Down Expand Up @@ -59,6 +60,13 @@ def default_editor_title
end.to change { subject.view_count }.to(0)
end

context 'when attribute is assigned as nil' do
specify 'you can set a default with the :allow_nil option' do
page = Examples::Page.new :author => nil
expect(page.author).to eql 'Mies'
end
end

context 'a ValueObject' do
it 'does not duplicate the ValueObject' do
page1 = Examples::Page.new
Expand Down
19 changes: 19 additions & 0 deletions spec/unit/virtus/attribute/allow_nil_predicate_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require 'spec_helper'

describe Virtus::Attribute, '#allow_nil?' do
subject { object.allow_nil? }

let(:object) { described_class.build(String, :allow_nil => allow_nil) }

context 'when allow_nil option is true' do
let(:allow_nil) { true }

it { should be(true) }
end

context 'when allow_nil option is false' do
let(:allow_nil) { false }

it { should be(false) }
end
end
40 changes: 40 additions & 0 deletions spec/unit/virtus/attribute/present_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'spec_helper'

describe Virtus::Attribute, '#present?' do
subject { object.present?(instance) }

let(:model) { Class.new { attr_accessor :test } }
let(:name) { :test }
let(:instance) { model.new }

context 'when the attribute allows nil values' do
let(:object) { described_class.build(String, :name => name, :allow_nil => true) }

context 'and the attribute value is defined as nil' do
before { instance.test = nil }
it { should be(true) }
end

context 'and the attribute value is NOT defined' do
it { should be(false) }
end
end

context 'when the attribute does NOT allow nil values' do
let(:object) { described_class.build(String, :name => name, :allow_nil => false) }

context 'and the attribute value is defined' do
before { instance.test = 'foo' }
it { should be(true) }
end

context 'and the attribute value is defined as nil' do
before { instance.test = nil }
it { should be(false) }
end

context 'and the attribute value is NOT defined' do
it { should be(false) }
end
end
end
14 changes: 13 additions & 1 deletion spec/unit/virtus/class_methods/new_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

attribute :id, Integer
attribute :name, String, :default => 'John Doe'
attribute :alias, String, :default => 'jdoe', :allow_nil => false
attribute :email, String, :default => '[email protected]', :lazy => true, :writer => :private
attribute :age, Integer, :default => 18, :allow_nil => false, :lazy => true
}
}

Expand All @@ -16,21 +18,31 @@

it 'sets default values for non-lazy attributes' do
expect(subject.instance_variable_get('@name')).to eql('John Doe')
expect(subject.instance_variable_get('@alias')).to eql('jdoe')
end

it 'skips setting default values for lazy attributes' do
expect(subject.instance_variable_get('@email')).to be(nil)
expect(subject.instance_variable_get('@age')).to be(nil)
end
end

context 'with attribute hash' do
subject { model.new(:id => 1, :name => 'Jane Doe') }
subject { model.new(:id => 1, :name => 'Jane Doe', :alias => nil, :age => nil) }

it 'sets attributes with public writers' do
expect(subject.id).to be(1)
expect(subject.name).to eql('Jane Doe')
end

it 'sets default values for attributes assigned as nil without allow_nil' do
expect(subject.alias).to eql('jdoe')
end

it 'sets default values for lazy attributes assigned as nil without allow_nil' do
expect(subject.age).to eql(18)
end

it 'skips setting attributes with private writers' do
expect(subject.instance_variable_get('@email')).to be(nil)
end
Expand Down