Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support procs for required_if; add present_if #115

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
76 changes: 51 additions & 25 deletions lib/attributor/attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ def dump(value, **opts)
type.dump(value, opts)
end

def present_in_object?(object, context=Attributor::DEFAULT_ROOT_CONTEXT)
return true unless self.options.has_key?(:present_if)

AttributeResolver.current.register('present_if', object)
check_requirement(self.options[:present_if], Attributor::DEFAULT_ROOT_CONTEXT + ['present_if'] + context[1..-1])
end

def validate_type(value, context)
# delegate check to type subclass if it exists
Expand All @@ -96,7 +102,7 @@ def validate_type(value, context)
end


TOP_LEVEL_OPTIONS = [ :description, :values, :default, :example, :required, :required_if ]
TOP_LEVEL_OPTIONS = [ :description, :values, :default, :example, :required, :required_if, :present_if ]
INTERNAL_OPTIONS = [:dsl_compiler,:dsl_compiler_options] # Options we don't want to expose when describing attributes
def describe(shallow=true)
description = { }
Expand Down Expand Up @@ -213,34 +219,21 @@ def validate_missing_value(context)
requirement = self.options[:required_if]
return [] unless requirement

case requirement
when ::String
key_path = requirement
predicate = nil
when ::Hash
# TODO: support multiple dependencies?
key_path = requirement.keys.first
predicate = requirement.values.first
else
# should never get here if the option validation worked...
raise AttributorException.new("unknown type of dependency: #{requirement.inspect} for #{Attributor.humanize_context(context)}")
end

# chop off the last part
requirement_context = context[0..-2]
requirement_context_string = requirement_context.join(Attributor::SEPARATOR)
if (required = check_requirement(requirement, context))
key_path = required[:key_path]
key_path = required[:requirement_context_string] if key_path == ''

# FIXME: we're having to reconstruct a string context just to use the resolver...smell.
if AttributeResolver.current.check(requirement_context_string, key_path, predicate)
message = "Attribute #{Attributor.humanize_context(context)} is required when #{key_path} "

# give a hint about what the full path for a relative key_path would be
unless key_path[0..0] == Attributor::AttributeResolver::ROOT_PREFIX
message << "(for #{Attributor.humanize_context(requirement_context)}) "
message << "(for #{Attributor.humanize_context(required[:requirement_context])}) "
end

if predicate
message << "matches #{predicate.inspect}."
if (predicate = required[:predicate])
predicate_display = predicate.is_a?(::Proc) ? "the proc" : predicate.inspect

message << "matches #{predicate_display}."
else
message << "is present."
end
Expand Down Expand Up @@ -277,9 +270,9 @@ def check_option!(name, definition)
when :required
raise AttributorException.new("Required must be a boolean") unless !!definition == definition # Boolean check
raise AttributorException.new("Required cannot be enabled in combination with :default") if definition == true && options.has_key?(:default)
when :required_if
raise AttributorException.new("Required_if must be a String, a Hash definition or a Proc") unless definition.is_a?(::String) || definition.is_a?(::Hash) || definition.is_a?(::Proc)
raise AttributorException.new("Required_if cannot be specified together with :required") if self.options[:required]
when :required_if, :present_if
raise AttributorException.new("#{name} must be a String, a Hash definition or a Proc") unless definition.is_a?(::String) || definition.is_a?(::Hash) || definition.is_a?(::Proc)
raise AttributorException.new("#{name} cannot be specified together with :required") if self.options[:required]
when :example
unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? || self.type.valid_type?(definition)
raise AttributorException.new("Invalid example type (got: #{definition.class.name}). It must always match the type of the attribute (except if passing Regex that is allowed for some types)")
Expand All @@ -291,5 +284,38 @@ def check_option!(name, definition)
:ok # passes
end

private

def check_requirement(requirement, context)
case requirement
when ::String
key_path = requirement
predicate = nil
when ::Hash
# TODO: support multiple dependencies?
key_path = requirement.keys.first
predicate = requirement.values.first
when ::Proc
key_path = ''
predicate = requirement
else
# should never get here if the option validation worked...
raise AttributorException.new("unknown type of dependency: #{requirement.inspect} for #{Attributor.humanize_context(context)}")
end

# chop off the last part
requirement_context = context[0..-2]
requirement_context_string = requirement_context.join(Attributor::SEPARATOR)

# FIXME: we're having to reconstruct a string context just to use the resolver...smell.
if AttributeResolver.current.check(requirement_context_string, key_path, predicate)
{
:key_path => key_path,
:predicate => predicate,
:requirement_context => requirement_context,
:requirement_context_string => requirement_context_string
}
end
end
end
end
12 changes: 10 additions & 2 deletions lib/attributor/types/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,22 @@ def dump(context: Attributor::DEFAULT_ROOT_CONTEXT, **opts)

self.attributes.each_with_object({}) do |(name, value), result|
attribute = self.class.attributes[name]

current_context = context + [name]

# skip dumping undefined attributes
unless attribute
warn "WARNING: Trying to dump unknown attribute: #{name.inspect} with context: #{context.inspect}"
next
end

result[name.to_sym] = attribute.dump(value, context: context + [name] )
# If the present_if condition is not met, set the value to nil so that renderers
# can choose how to handle that.
#
if attribute.options[:present_if] && !attribute.present_in_object?(self, current_context)
result[name.to_sym] = nil
else
result[name.to_sym] = attribute.dump(value, context: current_context)
end
end
ensure
@dumping = false
Expand Down
25 changes: 24 additions & 1 deletion spec/attribute_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@

before { Attributor::AttributeResolver.current.register('instance', instance) }

let(:attribute_context) { ['$','params','key_material'] }
let(:attribute_context) { ['$', 'instance', 'ssh_key'] }
subject(:errors) { attribute.validate_missing_value(attribute_context) }


Expand Down Expand Up @@ -422,6 +422,29 @@
end
end

context 'with a proc dependency' do
let(:attribute_options) { {:required_if => lambda { |val| val.ssh_key.name == 'default_ssh_key_name' }} }

context 'where the target attribute exists, and matches the predicate' do
let(:value) { 'default_ssh_key_name' }

it { should_not be_empty }

its(:first) { should =~ /Attribute #{Regexp.quote(Attributor.humanize_context( attribute_context ))} is required when \$\.instance matches/ }
end

context 'where the target attribute exists, but does not match the predicate' do
let(:value) { 'non_default_ssh_key_name' }

it { should be_empty }
end

context 'where the target attribute does not exist' do
let(:ssh_key) { double("ssh_key", :name => nil) }

it { should be_empty }
end
end
end

end
Expand Down
73 changes: 73 additions & 0 deletions spec/types/model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,79 @@


end

context 'with attributes which have present_if set' do
let(:type_example) { default_type_example }
let(:default_type_example) { 'example type' }

let(:model_class) do
type_example_binding = type_example
present_if_binding = present_if

Class.new(Attributor::Model) do
attributes do
attribute :type, String, :example => type_example_binding
attribute :length, Integer, :present_if => present_if_binding
end
end
end

subject(:example) { model_class.example.dump }

context 'a key condition' do
let(:present_if) { 'type' }

context 'the condition is met' do
it 'keeps the attribute' do
example[:length].should_not be(nil)
end
end

context 'the condition is not met' do
let(:type_example) { nil }

it 'sets the attribute to nil' do
example[:length].should be(nil)
end
end
end

context 'a hash condition' do
let(:present_if) { {'type' => default_type_example} }

context 'the condition is met' do
it 'keeps the attribute' do
example[:length].should_not be(nil)
end
end

context 'the condition is not met' do
let(:type_example) { 'not the example' }

it 'sets the attribute to nil' do
example[:length].should be(nil)
end
end
end

context 'a proc condition' do
let(:present_if) { lambda { |val| val.type == default_type_example } }

context 'the condition is met' do
it 'keeps the attribute' do
example[:length].should_not be(nil)
end
end

context 'the condition is not met' do
let(:type_example) { 'not the example' }

it 'sets the attribute to nil' do
example[:length].should be(nil)
end
end
end
end
end


Expand Down