summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Keiser <john@johnkeiser.com>2015-05-29 11:44:25 -0700
committerJohn Keiser <john@johnkeiser.com>2015-06-02 19:33:49 -0700
commit8cf24935c261ae26b74fbcfd74f9c5e7f002f3f8 (patch)
tree534946ad8dbdca14123268b7aba4d3672c9fea07
parente3c455bd083ec7f87b8935a54cfc53692fdb1311 (diff)
downloadchef-jk/property.tar.gz
Create property and alias attribute to itjk/property
-rw-r--r--lib/chef/delayed_evaluator.rb33
-rw-r--r--lib/chef/mixin/params_validate.rb6
-rw-r--r--lib/chef/resource.rb343
-rw-r--r--lib/chef/resource/deploy.rb2
-rw-r--r--lib/chef/resource/implicit_property_type.rb22
-rw-r--r--lib/chef/resource/lwrp_base.rb11
-rw-r--r--lib/chef/resource/property_type.rb583
-rw-r--r--spec/integration/recipes/property_spec.rb16
-rw-r--r--spec/integration/recipes/property_validation_spec.rb16
-rw-r--r--spec/unit/resource_property_spec.rb744
-rw-r--r--spec/unit/resource_property_state_spec.rb492
-rw-r--r--spec/unit/resource_property_validation_spec.rb333
-rw-r--r--spec/unit/resource_spec.rb4
13 files changed, 2545 insertions, 60 deletions
diff --git a/lib/chef/delayed_evaluator.rb b/lib/chef/delayed_evaluator.rb
new file mode 100644
index 0000000000..640b2c7b69
--- /dev/null
+++ b/lib/chef/delayed_evaluator.rb
@@ -0,0 +1,33 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Chef
+ #
+ # A lazy value which may be assigned to a Resource property value or default.
+ #
+ # The proc will be run to determine the value, passed no parameters, and the
+ # result will be used as the value.
+ #
+ # Run no more than once per instance, but may be run multiple times per
+ # instance.
+ #
+ # @see Chef::Resource.lazy
+ # @see Chef::Mixin::ParamsValidate#lazy
+ #
+ class DelayedEvaluator < Proc
+ end
+end
diff --git a/lib/chef/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb
index 78d72dc801..3e4a61b417 100644
--- a/lib/chef/mixin/params_validate.rb
+++ b/lib/chef/mixin/params_validate.rb
@@ -15,12 +15,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+require 'chef/delayed_evaluator'
+
class Chef
- class DelayedEvaluator < Proc
- end
module Mixin
module ParamsValidate
-
# Takes a hash of options, along with a map to validate them. Returns the original
# options hash, plus any changes that might have been made (through things like setting
# default values in the validation map)
@@ -239,4 +238,3 @@ class Chef
end
end
end
-
diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb
index ac98df5513..9bcfb1e5e6 100644
--- a/lib/chef/resource.rb
+++ b/lib/chef/resource.rb
@@ -34,6 +34,8 @@ require 'chef/platform'
require 'chef/resource/resource_notification'
require 'chef/provider_resolver'
require 'chef/resource_resolver'
+require 'chef/resource/property_type'
+require 'chef/resource/implicit_property_type'
require 'chef/mixin/deprecation'
require 'chef/mixin/provides'
@@ -97,12 +99,12 @@ class Chef
#
# Create a new Resource.
#
- # @param name The name of this resource (corresponds to the #name attribute,
+ # @param name The name of this resource (corresponds to the #name property,
# used for notifications to this resource).
# @param run_context The context of the Chef run. Corresponds to #run_context.
#
def initialize(name, run_context=nil)
- name(name)
+ name(name) if name
@run_context = run_context
@noop = nil
@before = nil
@@ -131,6 +133,199 @@ class Chef
end
#
+ # Properties
+ #
+
+ #
+ # Find out whether a property has been set on this resource or not.
+ #
+ # This will be true if:
+ # - The user explicitly set the value
+ # - The property has a default, and the value has been retrieved.
+ # - The property has name_property = true and name is set.
+ #
+ # From this point of view, it is worth looking at this as "what does the
+ # user think this value should be." In order words, if the user grabbed
+ # the value, even if it was a default, they probably based calculations on
+ # it. If they based calculations on it and the value changes, the rest of
+ # the world gets inconsistent.
+ #
+ # As far as name_property goes, the option name implies that it is more of
+ # an alias: when `name` is set, this property gets set as well.
+ #
+ # @param name [Symbol] The property name to check.
+ #
+ # @return [Boolean] Whether the property name has been set or not.
+ #
+ # @see Chef::Resource::PropertyType#is_set?
+ #
+ def property_is_set?(name)
+ name = name.to_sym
+ raise ArgumentError, "Property #{name} does not exist on #{self.class}" if !self.class.properties[name]
+ type = self.class.properties[name]
+ type.is_set?(self, name)
+ end
+
+ #
+ # Create a property on this resource class.
+ #
+ # If a superclass has this property, or if this property has already been
+ # defined by this resource, this will *override* the previous value.
+ #
+ # @param name [Symbol] The name of the property.
+ # @param type [Object,Array<Object>] The type(s) of this property.
+ # If present, this is prepended to the `is` validation option.
+ # @param options [Hash<Symbol,Object>] Validation options.
+ # @option options [Object,Array] :is An object, or list of
+ # objects, that must match the value using Ruby's `===` operator
+ # (`options[:is].any? { |v| v === value }`).
+ # @option options [Object,Array] :equal_to An object, or list
+ # of objects, that must be equal to the value using Ruby's `==`
+ # operator (`options[:is].any? { |v| v == value }`)
+ # @option options [Regexp,Array<Regexp>] :regex An object, or
+ # list of objects, that must match the value with `regex.match(value)`.
+ # @option options [Class,Array<Class>] :kind_of A class, or
+ # list of classes, that the value must be an instance of.
+ # @option options [Hash<String,Proc>] :callbacks A hash of
+ # messages -> procs, all of which match the value. The proc must
+ # return a truthy or falsey value (true means it matches).
+ # @option options [Symbol,Array<Symbol>] :respond_to A method
+ # name, or list of method names, the value must respond to.
+ # @option options [Symbol,Array<Symbol>] :cannot_be A property,
+ # or a list of properties, that the value cannot have (such as `:nil` or
+ # `:empty`). The method with a questionmark at the end is called on the
+ # value (e.g. `value.empty?`). If the value does not have this method,
+ # it is considered valid (i.e. if you don't respond to `empty?` we
+ # assume you are not empty).
+ # @option options [Proc] :coerce A proc which will be called to
+ # transform the user input to canonical form. The value is passed in,
+ # and the transformed value returned as output. Lazy values will *not*
+ # be passed to this method until after they are evaluated. Called in the
+ # context of the resource (meaning you can access other properties).
+ # @option options [Boolean] :required `true` if this property
+ # must be present; `false` otherwise. This is checked after the resource
+ # is fully initialized.
+ # @option options [Boolean] :name_property `true` if this
+ # property defaults to the same value as `name`. Equivalent to
+ # `default: lazy { name }`, except that #property_is_set? will
+ # return `true` if the property is set *or* if `name` is set.
+ # @option options [Boolean] :name_attribute Same as `name_property`.
+ # @option options [Object] :default The value this property
+ # will return if the user does not set one. If this is `lazy`, it will
+ # be run in the context of the instance (and able to access other
+ # properties).
+ # @option options [Boolean] :desired_state `true` if this property is
+ # part of desired state. Defaults to `true`.
+ # @option options [Boolean] :identity `true` if this property
+ # is part of object identity. Defaults to `false`.
+ # @option desired_state Whether this property is desired state or not.
+ # Defaults to true.
+ #
+ # @return [Chef::Resource::PropertyType] The property type.
+ #
+ # @example With nothing
+ # property :x
+ #
+ # @example With just a type
+ # property :x, String
+ #
+ # @example With just options
+ # property :x, default: 'hi'
+ #
+ # @example With type and options
+ # property :x, String, default: 'hi'
+ #
+ def self.property(name, type=NULL_ARG, **options)
+ name = name.to_sym
+
+ # Handle name_attribute -> name_property
+ options[:name_property] ||= options.delete(:name_attribute) if options.has_key?(:name_attribute)
+
+ # Create the type that will drive the get/set
+ local_properties[name] = property_type(type, options)
+
+ # Make the getter and setter methods
+ #
+ # NOTE: We eval a string so that the name of the property will show up in
+ # the stack rather than "property"
+ class_eval <<-EOM, __FILE__, __LINE__+1
+ def #{name}(value=NULL_ARG)
+ if value == NULL_ARG
+ self.class.properties[#{name.inspect}].get(self, #{name.inspect})
+ elsif value.nil? && !self.class.properties[#{name.inspect}].explicitly_accepts_nil?(self, #{name.inspect})
+ # If you say "my_property nil" and the property explicitly accepts
+ # nil values, we consider this a get.
+ Chef::Log.deprecation("#{name} nil currently does not overwrite the value of #{name}. This will change in Chef 13, and the value will be set to nil instead. Please change your code to explicitly accept nil using \\"property :#{name}, [MyType, nil]\\", or stop setting this value to nil.")
+ self.class.properties[#{name.inspect}].get(self, #{name.inspect})
+ else
+ self.class.properties[#{name.inspect}].set(self, #{name.inspect}, value)
+ end
+ end
+ def #{name}=(value)
+ self.class.properties[#{name.inspect}].set(self, #{name.inspect}, value)
+ end
+ EOM
+ end
+
+ #
+ # Create a property type that can be attached to multiple properties.
+ #
+ # @param name [Symbol] The name of the property.
+ # @param type [Object,Array<Object>] The type(s) of this property.
+ # If present, this is prepended to the `is` validation option.
+ # @param options [Hash<Symbol,Object>] Validation options.
+ # See #property for a list of valid options.
+ #
+ # @return [Chef::Resource::PropertyType] The created property type
+ #
+ def self.property_type(type=NULL_ARG, **options)
+ return type if type.is_a?(PropertyType) && options.empty?
+
+ # Add type to options[:is]
+ if type != NULL_ARG
+ is = options[:is]
+
+ type = [ type ] if !type.is_a?(Array)
+ options[:is] = type
+ if is
+ is = [ is ] if !is.is_a?(Array)
+ options[:is] += is
+ end
+ end
+
+ PropertyType.new(options)
+ end
+
+ #
+ # The list of properties on this resource.
+ #
+ # Includes properties from the superclass.
+ #
+ # @return [Hash<Symbol,Chef::Resource::PropertyType>] A hash from property
+ # name to property type.
+ #
+ def self.properties
+ if superclass.respond_to?(:properties)
+ superclass.properties.merge(local_properties)
+ else
+ local_properties
+ end
+ end
+
+ #
+ # The list of properties on this resource.
+ #
+ # Does *not* include properties from the superclass.
+ #
+ # @return [Hash<Symbol,Chef::Resource::PropertyType>] A hash from property
+ # name to property type.
+ #
+ def self.local_properties
+ @local_properties ||= {}
+ end
+ class<<self; protected :local_properties; end
+
+ #
# The name of this particular resource.
#
# This special resource attribute is set automatically from the declaration
@@ -153,16 +348,7 @@ class Chef
# @param name [Object] The name to set, typically a String or Array
# @return [String] The name of this Resource.
#
- def name(name=nil)
- if !name.nil?
- if name.is_a?(Array)
- @name = name.join(', ')
- else
- @name = name.to_s
- end
- end
- @name
- end
+ property :name, String, coerce: proc { |name| name.is_a?(Array) ? name.join(', ') : name.to_s }, desired_state: false
#
# The action or actions that will be taken when this resource is run.
@@ -475,13 +661,18 @@ class Chef
#
# Get the value of the state attributes in this resource as a hash.
#
+ # Does not include properties that are not set.
+ #
# @return [Hash{Symbol => Object}] A Hash of attribute => value for the
# Resource class's `state_attrs`.
+ #
def state_for_resource_reporter
- self.class.state_attrs.inject({}) do |state_attrs, attr_name|
- state_attrs[attr_name] = send(attr_name)
- state_attrs
+ state = {}
+ self.class.state_attrs.each do |name|
+ next if self.class.properties[name] && !property_is_set?(name)
+ state[name] = send(name)
end
+ state
end
#
@@ -494,17 +685,25 @@ class Chef
alias_method :state, :state_for_resource_reporter
#
- # The value of the identity attribute, if declared. Falls back to #name if
- # no identity attribute is declared.
+ # The value of the identity of this resource.
#
- # @return The value of the identity attribute.
+ # - If there are no identity properties on the resource, `name` is returned.
+ # - If there is exactly one identity property on the resource, it is returned.
+ # - If there are more than one, they are returned in a hash. Properties that
+ # are not set are not included in the hash.
+ #
+ # @return [Object,Hash<Symbol,Object>] The identity of this resource.
#
def identity
- if identity_attr = self.class.identity_attr
- send(identity_attr)
- else
- name
+ identity_properties = self.class.properties.select { |name,type| type.identity? }
+ identity_properties = { name: self.class.properties[:name] } if identity_properties.empty?
+
+ result = {}
+ identity_properties.each do |name, type|
+ result[name] = send(name) if property_is_set?(name)
end
+ return result.values.first if identity_properties.size == 1
+ result
end
#
@@ -703,6 +902,7 @@ class Chef
provider(arg)
end
+ #
# Set or return the list of "state attributes" implemented by the Resource
# subclass. State attributes are attributes that describe the desired state
# of the system, such as file permissions or ownership. In general, state
@@ -715,36 +915,93 @@ class Chef
#
# This list is used by the Chef client auditing system to extract
# information from resources to describe changes made to the system.
+ #
+ # @deprecated This is not deprecated, but it is no longer preferred:
+ # you should use the `desired_state` option to Chef::Resource.property
+ # to denote non-shared state instead:
+ #
+ # ```ruby
+ # property :x, desired_state: false
+ # ```
+ #
def self.state_attrs(*attr_names)
- @state_attrs ||= []
- @state_attrs = attr_names unless attr_names.empty?
+ if !attr_names.empty?
+ attr_names = attr_names.map { |name| name.to_sym }
+
+ # attr_names *always* includes superclass.attr_names
+ attr_names -= superclass.attr_names if superclass.respond_to?(:attr_names)
+
+ # Add new properties to the list.
+ attr_names.each do |name|
+ type = properties[name]
+ if type
+ local_properties[name] = type.specialize(desired_state: true) if !type.desired_state?
+ else
+ local_properties[name] = ImplicitPropertyType.new
+ end
+ end
- # Return *all* state_attrs that this class has, including inherited ones
- if superclass.respond_to?(:state_attrs)
- superclass.state_attrs + @state_attrs
- else
- @state_attrs
+ # If state_attrs *excludes* something which is currently desired state,
+ # mark it as not desired state.
+ local_properties.each do |name,type|
+ if type.desired_state? && !attr_names.include?(name)
+ local_properties[name] = type.specialize(desired_state: false)
+ end
+ end
end
+
+ # Grab properties representing desired state
+ properties.select { |name,type| type.desired_state? }.map { |name,type| name }
end
- # Set or return the "identity attribute" for this resource class. This is
- # generally going to be the "name attribute" for this resource. In other
- # words, the resource type plus this attribute uniquely identify a given
- # bit of state that chef manages. For a File resource, this would be the
- # path, for a package resource, it will be the package name. This will show
- # up in chef-client's audit records as a searchable field.
- def self.identity_attr(attr_name=nil)
- @identity_attr ||= nil
- @identity_attr = attr_name if attr_name
+ #
+ # Create a lazy value.
+ #
+ # @param block The block to run to get the value. This block will be invoked
+ # with no parameters and the returned value will be used as the lazy value.
+ #
+ # @return [Chef::DelayedEvaluator] A lazy value object assignable to resource
+ # properties and defaults.
+ #
+ def self.lazy(&block)
+ Chef::DelayedEvaluator.new(&block)
+ end
- # If this class doesn't have an identity attr, we'll defer to the superclass:
- if @identity_attr || !superclass.respond_to?(:identity_attr)
- @identity_attr
- else
- superclass.identity_attr
+ #
+ # Set a property as the "identity attribute" for this resource.
+ #
+ # Unsets "identity attribute" on all other property.
+ #
+ # @param name [Symbol]
+ #
+ # @return [Symbol]
+ #
+ # @deprecated This is no longer the preferred way of doing this: instead,
+ # pass identity: true to `Chef::Resource.property`.
+ #
+ def self.identity_attr(name=nil)
+ if name
+ name = name.to_sym
+ # Switch off
+ properties.each do |prop_name,type|
+ if type.identity? && prop_name != name
+ local_properties[prop_name] = type.specialize(identity: false)
+ end
+ end
+ # Grab the existing type, and specialize it if it exists.
+ type = properties[name]
+ if type
+ type = type.specialize(identity: true)
+ else
+ type = ImplicitPropertyType.new(identity: true)
+ end
+ local_properties[name] = type
end
+
+ properties.select { |name,type| type.identity? }.map { |name,type| name }.first || :name
end
+
#
# The guard interpreter that will be used to process `only_if` and `not_if`
# statements by default. If left unset, or set to `:default`, the guard
diff --git a/lib/chef/resource/deploy.rb b/lib/chef/resource/deploy.rb
index 8d007df348..067c6f5533 100644
--- a/lib/chef/resource/deploy.rb
+++ b/lib/chef/resource/deploy.rb
@@ -52,7 +52,7 @@ class Chef
class Deploy < Chef::Resource
use_automatic_resource_name
- identity_attr :repository
+ identity_attr :repo
state_attrs :deploy_to, :revision
diff --git a/lib/chef/resource/implicit_property_type.rb b/lib/chef/resource/implicit_property_type.rb
new file mode 100644
index 0000000000..f9920f9216
--- /dev/null
+++ b/lib/chef/resource/implicit_property_type.rb
@@ -0,0 +1,22 @@
+require 'chef/resource/property_type'
+
+class Chef
+ class Resource
+ #
+ # When the Resource class creates a property by itself, the user is using
+ # their own methods to manage state. We don't make any assumptions about
+ # where the data is stored, in that case.
+ #
+ class ImplicitPropertyType < PropertyType
+ def get_value(resource, name)
+ resource.send(name)
+ end
+ def set_value(resource, name, value)
+ resource.send(name, value)
+ end
+ def value_is_set?(resource, name)
+ true
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/lwrp_base.rb b/lib/chef/resource/lwrp_base.rb
index 129fc38d6f..5fe427cc28 100644
--- a/lib/chef/resource/lwrp_base.rb
+++ b/lib/chef/resource/lwrp_base.rb
@@ -25,7 +25,6 @@ require 'chef/log'
require 'chef/exceptions'
require 'chef/mixin/convert_to_class_name'
require 'chef/mixin/from_file'
-require 'chef/mixin/params_validate' # for DelayedEvaluator
class Chef
class Resource
@@ -76,11 +75,7 @@ class Chef
# Define an attribute on this resource, including optional validation
# parameters.
- def attribute(attr_name, validation_opts={})
- define_method(attr_name) do |arg=nil|
- set_or_return(attr_name.to_sym, arg, validation_opts)
- end
- end
+ alias :attribute :property
# Sets the default action
def default_action(action_name=NULL_ARG)
@@ -128,10 +123,6 @@ class Chef
run_context ? run_context.node : nil
end
- def lazy(&block)
- DelayedEvaluator.new(&block)
- end
-
protected
def loaded_lwrps
diff --git a/lib/chef/resource/property_type.rb b/lib/chef/resource/property_type.rb
new file mode 100644
index 0000000000..8393f8cefd
--- /dev/null
+++ b/lib/chef/resource/property_type.rb
@@ -0,0 +1,583 @@
+require 'chef/exceptions'
+require 'chef/delayed_evaluator'
+
+class Chef
+ class Resource
+ #
+ # Type and validation information for a property on a resource.
+ #
+ # A property named "x" manipulates the "@x" instance variable on a
+ # resource. The *presence* of the variable (`instance_variable_defined?(@x)`)
+ # tells whether the variable is defined; it may have any actual value,
+ # constrained only by validation.
+ #
+ # Properties may have validation, defaults, and coercion, and have fully
+ # support for lazy values.
+ #
+ # @see Chef::Resource.property
+ # @see Chef::DelayedEvaluator
+ #
+ class PropertyType
+ #
+ # Create a new property type.
+ #
+ # @raise ArgumentError If `:callbacks` is not a Hash.
+ #
+ def initialize(
+ is: NULL_ARG,
+ equal_to: NULL_ARG,
+ regex: NULL_ARG,
+ kind_of: NULL_ARG,
+ respond_to: NULL_ARG,
+ cannot_be: NULL_ARG,
+ callbacks: NULL_ARG,
+
+ coerce: NULL_ARG,
+ required: NULL_ARG,
+ name_property: NULL_ARG,
+ default: NULL_ARG,
+ desired_state: NULL_ARG,
+ identity: NULL_ARG
+ )
+ # Validation args
+ @is = [ is ].flatten(1) unless is == NULL_ARG
+ @equal_to = [ equal_to ].flatten(1) unless equal_to == NULL_ARG
+ @regex = [ regex ].flatten(1) unless regex == NULL_ARG
+ @kind_of = [ kind_of ].flatten(1) unless kind_of == NULL_ARG
+ @respond_to = [ respond_to ].flatten(1).map { |v| v.to_sym } unless respond_to == NULL_ARG
+ @cannot_be = [ cannot_be ].flatten(1).map { |v| v.to_sym } unless cannot_be == NULL_ARG
+ @callbacks = callbacks unless callbacks == NULL_ARG
+
+ # Other properties
+ @coerce = coerce unless coerce == NULL_ARG
+ @required = required unless required == NULL_ARG
+ @name_property = name_property unless name_property == NULL_ARG
+ @default = default unless default == NULL_ARG
+ @desired_state = desired_state unless desired_state == NULL_ARG
+ @identity = identity unless identity == NULL_ARG
+
+ raise ArgumentError, "Callback list must be a hash, is #{callbacks.inspect}!" if callbacks != NULL_ARG && !callbacks.is_a?(Hash)
+ end
+
+ #
+ # List of valid things values can be.
+ #
+ # Uses Ruby's `===` to evaluate (is === value). At least one must match
+ # for the value to be valid.
+ #
+ # If a proc is passed, it is instance_eval'd in the resource, passed the
+ # value, and must return a truthy or falsey value.
+ #
+ # @example Class
+ # ```ruby
+ # property :x, String
+ # x 'valid' #=> valid
+ # x 1 #=> invalid
+ # x nil #=> invalid
+ #
+ # @example Value
+ # ```ruby
+ # property :x, [ :a, :b, :c, nil ]
+ # x :a #=> valid
+ # x nil #=> valid
+ # ```
+ #
+ # @example Regex
+ # ```ruby
+ # property :x, /bar/
+ # x 'foobar' #=> valid
+ # x 'foo' #=> invalid
+ # x nil #=> invalid
+ # ```
+ #
+ # @example Proc
+ # ```ruby
+ # property :x, proc { |x| x > y }
+ # property :y, default: 2
+ # x 3 #=> valid
+ # x 1 #=> invalid
+ # ```
+ #
+ # @example PropertyType
+ # ```ruby
+ # type = PropertyType.new(is: String)
+ # property :x, type
+ # x 'foo' #=> valid
+ # x 1 #=> invalid
+ # x nil #=> invalid
+ # ```
+ #
+ # @example RSpec Matcher
+ # ```ruby
+ # include RSpec::Matchers
+ # property :x, a_string_matching /bar/
+ # x 'foobar' #=> valid
+ # x 'foo' #=> invalid
+ # x nil #=> invalid
+ # ```
+ #
+ # @return [Array,nil] List of things this is, or nil if "is" is unspecified.
+ #
+ attr_reader :is
+
+ #
+ # List of things values must be equal to.
+ #
+ # Uses Ruby's `==` to evaluate (equal_to == value). At least one must
+ # match for the value to be valid.
+ #
+ # @return [Array,nil] List of things values must be equal to, or nil if
+ # equal_to is unspecified.
+ #
+ attr_reader :equal_to
+
+ #
+ # List of regexes values must match.
+ #
+ # Uses regex.match() to evaluate. At least one must match for the value to
+ # be valid.
+ #
+ # @return [Array<Regex>,nil] List of regexes values must match, or nil if
+ # regex is unspecified.
+ #
+ attr_reader :regex
+
+ #
+ # List of things values must be equal to.
+ #
+ # Uses value.kind_of?(kind_of) to evaluate. At least one must match for
+ # the value to be valid.
+ #
+ # @return [Array<Class>,nil] List of classes values must be equal to, or nil if
+ # kind_of is unspecified.
+ #
+ attr_reader :kind_of
+
+ #
+ # List of method names values must respond to.
+ #
+ # Uses value.respond_to?(respond_to) to evaluate. At least one must match
+ # for the value to be valid.
+ #
+ # @return [Array<Symbol>,nil] List of classes values must be equal to, or
+ # `nil` if respond_to is unspecified.
+ #
+ attr_reader :respond_to
+
+ #
+ # List of things that must not be true about the value.
+ #
+ # Calls `value.<thing>?` All responses must be false. Values which do not
+ # respond to <thing>? are considered valid (because if a value doesn't
+ # respond to `:readable?`, then it probably isn't readable.)
+ #
+ # @return [Array<Symbol>,nil] List of classes values must be equal to, or
+ # `nil` if cannot_be is unspecified.
+ #
+ # @example
+ # ```ruby
+ # property :x, cannot_be: [ :nil, :empty ]
+ # x [ 1, 2 ] #=> valid
+ # x 1 #=> valid
+ # x [] #=> invalid
+ # x nil #=> invalid
+ # ```
+ #
+ attr_reader :cannot_be
+
+ #
+ # List of procs we pass the value to.
+ #
+ # All procs must return true for the value to be valid. If any procs do
+ # not return true, the key will be used for the message: `"Property x's
+ # value :y <message>"`.
+ #
+ # @return [Hash<String,Proc>,nil] Hash of procs which must match, with
+ # their messages as the key. `nil` if callbacks is unspecified.
+ #
+ attr_reader :callbacks
+
+ #
+ # Whether this is required or not.
+ #
+ # @return [Boolean]
+ #
+ # @deprecated use default: lazy { name } instead.
+ def required?
+ @required
+ end
+
+ #
+ # Whether this is part of the resource's natural identity or not.
+ #
+ # @return [Boolean]
+ #
+ # @deprecated use default: lazy { name } instead.
+ def identity?
+ @identity
+ end
+
+ #
+ # Whether this is part of desired state or not.
+ #
+ # @return [Boolean]
+ #
+ # @deprecated use default: lazy { name } instead.
+ def desired_state?
+ defined?(@desired_state) ? @desired_state : true
+ end
+
+ #
+ # Whether this is name_property or not.
+ #
+ # @return [Boolean]
+ #
+ # @deprecated use default: lazy { name } instead.
+ def name_property?
+ @name_property
+ end
+
+ #
+ # Whether this has a default value or not.
+ #
+ # @return [Boolean]
+ #
+ def default?
+ defined?(@default)
+ end
+
+ #
+ # Get the property value from the resource, handling lazy values,
+ # defaults, and validation.
+ #
+ # - If the property's value is lazy, the lazy value is evaluated, coerced
+ # and validated, and the result stored in the property (it will not be
+ # evaluated twice).
+ # - If the property has no value, but has a default, the default value
+ # will be returned. If the default value is lazy, it will be evaluated,
+ # coerced and validated, and the result stored in the property.
+ # - If the property has no value, but is name_property, `resource.name`
+ # is retrieved, coerced, validated and stored in the property.
+ # - Otherwise, `nil` is returned.
+ #
+ # @param resource [Chef::Resource] The resource to get the property from.
+ # @param name [Symbol] The name of the property to set.
+ #
+ # @return The value of the property.
+ #
+ # @raise Chef::Exceptions::ValidationFailed If the value is invalid for
+ # this property.
+ #
+ def get(resource, name)
+ # Grab the value
+ if value_is_set?(resource, name)
+ value = get_value(resource, name)
+
+ # Use the default if it is there.
+ elsif default?
+ value = set(resource, name, default)
+
+ # Last ditch: if name_property is set, get that
+ elsif name_property? && name != :name
+ value = set(resource, name, resource.name)
+ end
+
+ # If the value is lazy, pop it open and store it
+ if value.is_a?(DelayedEvaluator)
+ value = set(resource, name, resource.instance_eval(&value))
+ end
+
+ value
+ end
+
+ #
+ # Get the default value for this property.
+ #
+ # - If the property has no value, but has a default, the default value
+ # will be returned. If the default value is lazy, it will be evaluated,
+ # coerced and validated.
+ # - If the property has no value, but is name_property, `resource.name`
+ # is returned.
+ # - Otherwise, `nil` is returned.
+ #
+ # This differs from `get` in that it will *not* store the default value in
+ # the given resource.
+ #
+ # If resource and name are not passed, the default is returned without
+ # evaluation, coercion or validation, and name_property is not honored.
+ #
+ # @param resource [Chef::Resource] The resource to get the default against.
+ # @param name [Symbol] The name of the property to get the default of.
+ #
+ # @return The default value for the property.
+ #
+ # @raise Chef::Exceptions::ValidationFailed If the value is invalid for
+ # this property.
+ #
+ def default(resource=nil, name=nil)
+ return @default if !resource && !name
+
+ if defined?(@default)
+ coerce(resource, name, @default)
+ elsif name_property? && name != :name
+ resource.name
+ else
+ nil
+ end
+ end
+
+ #
+ # Set the value of this property in the given resource.
+ #
+ # Non-lazy values are coerced and validated before being set. Coercion
+ # and validation of lazy values is delayed until they are first retrieved.
+ #
+ # @param resource [Chef::Resource] The resource to set this property in.
+ # @param name [Symbol] The name of the property to set.
+ # @param value The value to set.
+ #
+ # @return The value that was set, after coercion (if lazy, still returns
+ # the lazy value)
+ #
+ # @raise Chef::Exceptions::ValidationFailed If the value is invalid for
+ # this property.
+ #
+ def set(resource, name, value)
+ value = coerce(resource, name, value)
+ set_value(resource, name, value)
+ end
+
+ #
+ # Find out whether this property has been set.
+ #
+ # This will be true if:
+ # - The user explicitly set the value
+ # - The property is name_property and name has been set
+ # - The property has a default, and the value was retrieved.
+ #
+ # From this point of view, it is worth looking at this as "what does the
+ # user think this value should be." In order words, if the user grabbed
+ # the value, even if it was a default, they probably based calculations on
+ # it. If they based calculations on it and the value changes, the rest of
+ # the world gets inconsistent.
+ #
+ # @param resource [Chef::Resource] The resource to get the property from.
+ # @param name [Symbol] The name of the property to get.
+ #
+ # @return [Boolean]
+ #
+ def is_set?(resource, name)
+ value_is_set?(resource, name) ||
+ (name_property? && value_is_set?(resource, :name))
+ end
+
+ #
+ # Coerce an input value into canonical form for the property, validating
+ # it in the process.
+ #
+ # After coercion, the value is suitable for storage in the resource.
+ #
+ # Does not coerce or validate lazy values.
+ #
+ # @param resource [Chef::Resource] The resource we're coercing against
+ # (to provide context for the coerce).
+ # @param name [Symbol] The name of the property we're coercing (to provide
+ # context for the coerce).
+ # @param value The value to coerce.
+ #
+ # @return The coerced value.
+ #
+ # @raise Chef::Exceptions::ValidationFailed If the value is invalid for
+ # this property.
+ #
+ def coerce(resource, name, value)
+ if !value.is_a?(DelayedEvaluator)
+ value = resource.instance_exec(value, &@coerce) if @coerce
+ errors = validate(resource, name, value)
+ raise Chef::Exceptions::ValidationFailed, errors.map { |e| "Property #{name}'s #{e}" }.join("\n") if errors
+ end
+ value
+ end
+
+ #
+ # Validate a value.
+ #
+ # Honors #is, #equal_to, #regex, #kind_of, #respond_to, #cannot_be, and
+ # #callbacks.
+ #
+ # @param resource [Chef::Resource] The resource we're validating against
+ # (to provide context for the validate).
+ # @param name [Symbol] The name of the property we're validating (to provide
+ # context for the validate).
+ # @param value The value to validate.
+ #
+ # @return [Array<String>,nil] A list of errors, or nil if there was no error.
+ #
+ def validate(resource, name, value)
+ errors = []
+
+ # "is": capture the first type match so we can use it as the supertype
+ # for this value.
+ error_unless_any_match(errors, value, is, "is not") do |v|
+ case v
+ when Proc
+ resource.instance_exec(value, &v)
+ when PropertyType
+ got_errors = v.validate(resource, name, value)
+ errors += got_errors if got_errors
+ true
+ else
+ v === value
+ end
+ end
+
+ # equal_to
+ error_unless_any_match(errors, value, equal_to, "does not equal") do |v|
+ v == value
+ end
+
+ # regex
+ error_unless_any_match(errors, value, regex, "does not match") do |v|
+ value.is_a?(String) && v.match(value)
+ end
+
+ # kind_of
+ error_unless_any_match(errors, value, kind_of, "is not of type") do |v|
+ value.kind_of?(v)
+ end
+
+ # respond_to
+ error_unless_all_match(errors, value, respond_to, "does not respond to") do |v|
+ value.respond_to?(v)
+ end
+
+ # cannot_be
+ error_unless_all_match(errors, value, cannot_be, "is") do |v|
+ !(value.respond_to?("#{v}?") && value.send("#{v}?"))
+ end
+
+ # callbacks
+ error_unless_callbacks_match(errors, value, callbacks)
+
+ errors.empty? ? nil : errors
+ end
+
+ #
+ # Find out whether this type accepts nil explicitly.
+ #
+ # A type accepts nil explicitly if it validates as nil, *and* is not simply
+ # an empty type.
+ #
+ # These examples accept nil explicitly:
+ # ```ruby
+ # property :a, [ String, nil ]
+ # property :a, is: [ String, nil ]
+ # property :a, equal_to: [ 1, 2, 3, nil ]
+ # property :a, kind_of: [ String, NilClass ]
+ # property :a, respond_to: [ ]
+ # ```
+ #
+ # These do not:
+ # ```ruby
+ # property :a, [ String, nil ], cannot_be: :nil
+ # property :a, callbacks: { x: }
+ # ```
+ #
+ # This does not either (accepts nil implicitly only):
+ # ```ruby
+ # property :a
+ # ```
+ #
+ # @param resource [Chef::Resource] The resource we're coercing against
+ # (to provide context for the coerce).
+ # @param name [Symbol] The name of the property we're coercing (to provide
+ # context for the coerce).
+ #
+ # @return [Boolean] Whether this value explicitly accepts nil.
+ #
+ # @api private
+ def explicitly_accepts_nil?(resource, name)
+ return false if !validates_values?
+
+ !validate(resource, name, nil)
+ end
+
+ #
+ # Specialize this PropertyType by adding or changing some options.
+ #
+ def specialize(**options)
+ options[:is] = [ self ] + (options[:is] || [])
+ options[:coerce] = @coerce if defined?(@coerce) && !options.has_key?(:coerce)
+ options[:required] = @required if defined?(@required) && !options.has_key?(:coerce)
+ options[:name_property] = @name_property if defined?(@name_property) && !options.has_key?(:name_property)
+ options[:default] = @default if defined?(@default) && !options.has_key?(:default)
+ options[:desired_state] = @desired_state if defined?(@desired_state) && !options.has_key?(:desired_state)
+ options[:identity] = @identity if defined?(@identity) && !options.has_key?(:identity)
+ self.class.new(options)
+ end
+
+ protected
+
+ def error_unless_all_match(errors, value, match_values, message, &matcher)
+ if match_values && !match_values.empty?
+ match_values.each do |v|
+ if !matcher.call(v)
+ errors << "value #{value.inspect} #{message} #{v.inspect}"
+ end
+ end
+ end
+ end
+
+ def error_unless_any_match(errors, value, match_values, message, &matcher)
+ if match_values && !match_values.empty?
+ if !match_values.any?(&matcher)
+ errors << "value #{value.inspect} #{message} #{english_join(match_values)}"
+ end
+ end
+ end
+
+ def error_unless_callbacks_match(errors, value, callbacks)
+ if callbacks && !callbacks.empty?
+ callbacks.each do |message, callback|
+ if !callback.call(value)
+ errors << "value #{value.inspect} #{message}"
+ end
+ end
+ end
+ end
+
+ def english_join(values)
+ return '<nothing>' if values.size == 0
+ return values[0].inspect if values.size == 1
+ "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}"
+ end
+
+ #
+ # Whether this resource actually validates values.
+ #
+ # Returns true if there are any validation options that depend on the
+ # actual value. Does not check for coerce, default, required or
+ # name_property.
+ #
+ # @return [Boolean] Whether this resource validates anything.
+ #
+ def validates_values?
+ # If any validation option exists and *isn't* an empty hash / array, we
+ # will indeed spend time validating values.
+ %w(is equal_to regex kind_of respond_to cannot_be callbacks).any? do |option|
+ send(option) && !send(option).empty?
+ end
+ end
+
+ def get_value(resource, name)
+ resource.instance_variable_get(:"@#{name}")
+ end
+ def set_value(resource, name, value)
+ resource.instance_variable_set(:"@#{name}", value)
+ end
+ def value_is_set?(resource, name)
+ resource.instance_variable_defined?(:"@#{name}")
+ end
+ end
+ end
+end
diff --git a/spec/integration/recipes/property_spec.rb b/spec/integration/recipes/property_spec.rb
new file mode 100644
index 0000000000..4e7e0e4362
--- /dev/null
+++ b/spec/integration/recipes/property_spec.rb
@@ -0,0 +1,16 @@
+require 'support/shared/integration/integration_helper'
+
+describe "Chef::Resource.property" do
+ include IntegrationSupport
+
+ # Basic properties
+ # Inheritance
+ # default
+ # name_attribute
+ # coerce
+ # lazy
+ # identity
+ # desired_state
+ # to hash, json
+ # TODO "is" and types: coercion, defaults, name_attribute, identity, lazy values, and validation
+end
diff --git a/spec/integration/recipes/property_validation_spec.rb b/spec/integration/recipes/property_validation_spec.rb
new file mode 100644
index 0000000000..c6811d43bc
--- /dev/null
+++ b/spec/integration/recipes/property_validation_spec.rb
@@ -0,0 +1,16 @@
+require 'support/shared/integration/integration_helper'
+
+describe "Chef::Resource::.property validation" do
+ include IntegrationSupport
+
+ # Bare types
+ # is
+ # - Class, Regex, Symbol, nil, PropertyType, RSpec::Matcher
+ # equal_to
+ # kind_of
+ # regex
+ # callbacks
+ # respond_to
+ # cannot_be
+ # required
+end
diff --git a/spec/unit/resource_property_spec.rb b/spec/unit/resource_property_spec.rb
new file mode 100644
index 0000000000..516ab533f2
--- /dev/null
+++ b/spec/unit/resource_property_spec.rb
@@ -0,0 +1,744 @@
+require 'support/shared/integration/integration_helper'
+
+describe "Chef::Resource.property" do
+ include IntegrationSupport
+
+ class Namer
+ @i = 0
+ def self.next_resource_name
+ "chef_resource_property_spec_#{@i += 1}"
+ end
+ def self.reset_index
+ @current_index = 0
+ end
+ def self.current_index
+ @current_index
+ end
+ def self.next_index
+ @current_index += 1
+ end
+ end
+
+ def lazy(&block)
+ Chef::DelayedEvaluator.new(&block)
+ end
+
+ before do
+ Namer.reset_index
+ end
+
+ def self.new_resource_name
+ Namer.next_resource_name
+ end
+
+ let(:resource_class) do
+ new_resource_name = self.class.new_resource_name
+ Class.new(Chef::Resource) do
+ resource_name new_resource_name
+ def next_index
+ Namer.next_index
+ end
+ end
+ end
+
+ let(:resource) do
+ resource_class.new("blah")
+ end
+
+ def self.english_join(values)
+ return '<nothing>' if values.size == 0
+ return values[0].inspect if values.size == 1
+ "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}"
+ end
+
+ def self.with_property(*properties, &block)
+ tags_index = properties.find_index { |p| !p.is_a?(String)}
+ if tags_index
+ properties, tags = properties[0..tags_index-1], properties[tags_index..-1]
+ else
+ tags = []
+ end
+ properties = properties.map { |property| "property #{property}" }
+ context "With properties #{english_join(properties)}", *tags do
+ before do
+ properties.each do |property_str|
+ resource_class.class_eval(property_str, __FILE__, __LINE__)
+ end
+ end
+ instance_eval(&block)
+ end
+ end
+
+ # Basic properties
+ with_property ":bare_property" do
+ it "can be set" do
+ expect(resource.bare_property 10).to eq 10
+ expect(resource.bare_property).to eq 10
+ end
+ it "emits a deprecation warning and does a get, if set to nil" do
+ expect(resource.bare_property 10).to eq 10
+ expect { resource.bare_property nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError
+ Chef::Config[:treat_deprecation_warnings_as_errors] = false
+ expect(resource.bare_property nil).to eq 10
+ expect(resource.bare_property).to eq 10
+ end
+ it "can be updated" do
+ expect(resource.bare_property 10).to eq 10
+ expect(resource.bare_property 20).to eq 20
+ expect(resource.bare_property).to eq 20
+ end
+ it "can be set with =" do
+ expect(resource.bare_property 10).to eq 10
+ expect(resource.bare_property).to eq 10
+ end
+ it "can be set to nil with =" do
+ expect(resource.bare_property 10).to eq 10
+ expect(resource.bare_property = nil).to be_nil
+ expect(resource.bare_property).to be_nil
+ end
+ it "can be updated with =" do
+ expect(resource.bare_property 10).to eq 10
+ expect(resource.bare_property = 20).to eq 20
+ expect(resource.bare_property).to eq 20
+ end
+ end
+
+ with_property ":x, Integer" do
+ context "and subclass" do
+ let(:subresource_class) do
+ new_resource_name = self.class.new_resource_name
+ Class.new(resource_class) do
+ resource_name new_resource_name
+ end
+ end
+ let(:subresource) do
+ subresource_class.new('blah')
+ end
+
+ it "x is inherited" do
+ expect(subresource.x 10).to eq 10
+ expect(subresource.x).to eq 10
+ expect(subresource.x = 20).to eq 20
+ expect(subresource.x).to eq 20
+ expect(subresource_class.properties[:x]).not_to be_nil
+ end
+
+ it "x's validation is inherited" do
+ expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+
+ context "with property :y on the subclass" do
+ before do
+ subresource_class.class_eval do
+ property :y
+ end
+ end
+
+ it "x is still there" do
+ expect(subresource.x 10).to eq 10
+ expect(subresource.x).to eq 10
+ expect(subresource.x = 20).to eq 20
+ expect(subresource.x).to eq 20
+ expect(subresource_class.properties[:x]).not_to be_nil
+ end
+ it "y is there" do
+ expect(subresource.y 10).to eq 10
+ expect(subresource.y).to eq 10
+ expect(subresource.y = 20).to eq 20
+ expect(subresource.y).to eq 20
+ expect(subresource_class.properties[:y]).not_to be_nil
+ end
+ it "y is not on the superclass" do
+ expect { resource_class.y 10 }.to raise_error
+ expect(resource_class.properties[:y]).to be_nil
+ end
+ end
+
+ context "with property :x on the subclass" do
+ before do
+ subresource_class.class_eval do
+ property :x
+ end
+ end
+
+ it "x is still there" do
+ expect(subresource.x 10).to eq 10
+ expect(subresource.x).to eq 10
+ expect(subresource.x = 20).to eq 20
+ expect(subresource.x).to eq 20
+ expect(subresource_class.properties[:x]).not_to be_nil
+ expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x]
+ end
+
+ it "x's validation is overwritten" do
+ expect(subresource.x 'ohno').to eq 'ohno'
+ expect(subresource.x).to eq 'ohno'
+ end
+
+ it "the superclass's validation for x is still there" do
+ expect { resource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ end
+
+ context "with property :x, String on the subclass" do
+ before do
+ subresource_class.class_eval do
+ property :x, String
+ end
+ end
+
+ it "x is still there" do
+ expect(subresource.x "10").to eq "10"
+ expect(subresource.x).to eq "10"
+ expect(subresource.x = "20").to eq "20"
+ expect(subresource.x).to eq "20"
+ expect(subresource_class.properties[:x]).not_to be_nil
+ expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x]
+ end
+
+ it "x's validation is overwritten" do
+ expect { subresource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed
+ expect(subresource.x 'ohno').to eq 'ohno'
+ expect(subresource.x).to eq 'ohno'
+ end
+
+ it "the superclass's validation for x is still there" do
+ expect { resource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed
+ expect(resource.x 10).to eq 10
+ expect(resource.x).to eq 10
+ end
+ end
+ end
+ end
+
+ context "Chef::Resource::PropertyType#property_is_set?" do
+ it "when a resource is newly created, property_is_set?(:name) is true" do
+ expect(resource.property_is_set?(:name)).to be_truthy
+ end
+
+ it "when referencing an undefined property, property_is_set?(:x) raises an error" do
+ expect { resource.property_is_set?(:x) }.to raise_error(ArgumentError)
+ end
+
+ with_property ":x" do
+ it "when the resource is newly created, property_is_set?(:x) is false" do
+ expect(resource.property_is_set?(:x)).to be_falsey
+ end
+ it "when x is set, property_is_set?(:x) is true" do
+ resource.x 10
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ it "when x is set with =, property_is_set?(:x) is true" do
+ resource.x = 10
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ it "when x is set to a lazy value, property_is_set?(:x) is true" do
+ resource.x lazy { 10 }
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ it "when x is retrieved, property_is_set?(:x) is false" do
+ resource.x
+ expect(resource.property_is_set?(:x)).to be_falsey
+ end
+ end
+
+ with_property ":x, default: 10" do
+ it "when the resource is newly created, property_is_set?(:x) is false" do
+ expect(resource.property_is_set?(:x)).to be_falsey
+ end
+ it "when x is set, property_is_set?(:x) is true" do
+ resource.x 10
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ it "when x is set with =, property_is_set?(:x) is true" do
+ resource.x = 10
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ it "when x is set to a lazy value, property_is_set?(:x) is true" do
+ resource.x lazy { 10 }
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ it "when x is retrieved, property_is_set?(:x) is true" do
+ resource.x
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ end
+
+ with_property ":x, default: nil" do
+ it "when the resource is newly created, property_is_set?(:x) is false" do
+ expect(resource.property_is_set?(:x)).to be_falsey
+ end
+ it "when x is set, property_is_set?(:x) is true" do
+ resource.x 10
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ it "when x is set with =, property_is_set?(:x) is true" do
+ resource.x = 10
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ it "when x is set to a lazy value, property_is_set?(:x) is true" do
+ resource.x lazy { 10 }
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ it "when x is retrieved, property_is_set?(:x) is true" do
+ resource.x
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ end
+
+ with_property ":x, default: lazy { 10 }" do
+ it "when the resource is newly created, property_is_set?(:x) is false" do
+ expect(resource.property_is_set?(:x)).to be_falsey
+ end
+ it "when x is set, property_is_set?(:x) is true" do
+ resource.x 10
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ it "when x is set with =, property_is_set?(:x) is true" do
+ resource.x = 10
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ it "when x is retrieved, property_is_set?(:x) is true" do
+ resource.x
+ expect(resource.property_is_set?(:x)).to be_truthy
+ end
+ end
+ end
+
+ context "Chef::Resource::PropertyType#default" do
+ with_property ":x, default: 10" do
+ it "when x is set, it returns its value" do
+ expect(resource.x 20).to eq 20
+ expect(resource.property_is_set?(:x)).to be_truthy
+ expect(resource.x).to eq 20
+ end
+ it "when x is not set, it returns 10" do
+ expect(resource.x).to eq 10
+ end
+ it "when x is not set, it is not included in state" do
+ expect(resource.state).to eq({})
+ end
+
+ context "With a subclass" do
+ let(:subresource_class) do
+ new_resource_name = self.class.new_resource_name
+ Class.new(resource_class) do
+ resource_name new_resource_name
+ end
+ end
+ let(:subresource) { subresource_class.new('blah') }
+ it "The default is inherited" do
+ expect(subresource.x).to eq 10
+ end
+ end
+ end
+
+ with_property ":x, default: 10, identity: true" do
+ it "when x is not set, it is not included in identity" do
+ expect(resource.state).to eq({})
+ end
+ end
+
+ with_property ":x, default: nil" do
+ it "when x is not set, it returns nil" do
+ expect(resource.x).to be_nil
+ end
+ end
+
+ with_property ":x" do
+ it "when x is not set, it returns nil" do
+ expect(resource.x).to be_nil
+ end
+ end
+
+ context "hash default" do
+ with_property ":x, default: {}" do
+ it "when x is not set, it returns {}" do
+ expect(resource.x).to eq({})
+ end
+ it "The same exact value is returned multiple times in a row" do
+ value = resource.x
+ expect(value).to eq({})
+ expect(resource.x.object_id).to eq(value.object_id)
+ end
+ it "Multiple instances of x receive the exact same value" do
+ # TODO this isn't really great behavior, but it's noted here so we find out
+ # if it changed.
+ expect(resource.x.object_id).to eq(resource_class.new('blah2').x.object_id)
+ end
+ end
+
+ with_property ":x, default: lazy { {} }" do
+ it "when x is not set, it returns {}" do
+ expect(resource.x).to eq({})
+ end
+ it "The same exact value is returned multiple times in a row" do
+ value = resource.x
+ expect(value).to eq({})
+ expect(resource.x.object_id).to eq(value.object_id)
+ end
+ it "Multiple instances of x receive different values" do
+ expect(resource.x.object_id).not_to eq(resource_class.new('blah2').x.object_id)
+ end
+ end
+ end
+
+ context "with a class with 'blah' as both class and instance methods" do
+ before do
+ resource_class.class_eval do
+ def self.blah
+ 'class'
+ end
+ def blah
+ "instance#{next_index}"
+ end
+ end
+ end
+ with_property ":x, default: lazy { blah }" do
+ it "x is run in context of the instance" do
+ expect(resource.x).to eq "instance1"
+ end
+ it "x is run in the context of each instance it is run in" do
+ expect(resource.x).to eq "instance1"
+ expect(resource_class.new('blah2').x).to eq "instance2"
+ expect(resource.x).to eq "instance1"
+ end
+ end
+ end
+
+ context "validation of defaults" do
+ with_property ":x, String, default: 10" do
+ it "when the resource is created, no error is raised" do
+ resource
+ end
+ it "when x is set, no error is raised" do
+ expect(resource.x 'hi').to eq 'hi'
+ expect(resource.x).to eq 'hi'
+ end
+ it "when x is retrieved, a validation error is raised" do
+ expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ end
+
+ with_property ":x, String, default: lazy { Namer.next_index }" do
+ it "when the resource is created, no error is raised" do
+ resource
+ end
+ it "when x is set, no error is raised" do
+ expect(resource.x 'hi').to eq 'hi'
+ expect(resource.x).to eq 'hi'
+ end
+ it "when x is retrieved, a validation error is raised" do
+ expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed
+ expect(Namer.current_index).to eq 1
+ end
+ end
+
+ with_property ":x, default: lazy { Namer.next_index }, is: proc { |v| Namer.next_index; true }" do
+ it "when x is retrieved, validation is run no more than once" do
+ expect(resource.x).to eq 1
+ expect(Namer.current_index).to eq 2
+ expect(resource.x).to eq 1
+ expect(Namer.current_index).to eq 2
+ end
+ end
+ end
+
+ context "coercion of defaults" do
+ with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do
+ it "when the resource is created, the proc is not yet run" do
+ resource
+ expect(Namer.current_index).to eq 0
+ end
+ it "when x is set, coercion is run" do
+ expect(resource.x 'hi').to eq 'hi1'
+ expect(resource.x).to eq 'hi1'
+ expect(Namer.current_index).to eq 1
+ end
+ it "when x is retrieved, coercion is run, no more than once" do
+ expect(resource.x).to eq '101'
+ expect(resource.x).to eq '101'
+ expect(Namer.current_index).to eq 1
+ end
+ end
+
+ with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do
+ it "when the resource is created, the proc is not yet run" do
+ resource
+ expect(Namer.current_index).to eq 0
+ end
+ it "when x is set, coercion is run" do
+ expect(resource.x 'hi').to eq 'hi1'
+ expect(resource.x).to eq 'hi1'
+ expect(Namer.current_index).to eq 1
+ end
+ end
+
+ with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }, is: proc { |v| Namer.next_index; true }' do
+ it "when x is retrieved, coercion is run, no more than once" do
+ expect(resource.x).to eq '101'
+ expect(Namer.current_index).to eq 2
+ expect(resource.x).to eq '101'
+ expect(Namer.current_index).to eq 2
+ end
+ end
+
+ context "validation and coercion of defaults" do
+ with_property ':x, String, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do
+ it "when x is retrieved, it is coerced before validating and passes" do
+ expect(resource.x).to eq '101'
+ end
+ end
+ with_property ':x, Integer, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do
+ it "when x is retrieved, it is coerced before validating and fails" do
+ expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ end
+ with_property ':x, String, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do
+ it "when x is retrieved, it is coerced before validating and passes" do
+ expect(resource.x).to eq '101'
+ end
+ end
+ with_property ':x, Integer, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do
+ it "when x is retrieved, it is coerced before validating and fails" do
+ expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ end
+ with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }, is: proc { |v| Namer.next_index; true }' do
+ it "when x is retrieved, coercion and validation is run exactly once per instance" do
+ expect(resource.x).to eq '101'
+ expect(Namer.current_index).to eq 2
+ expect(resource.x).to eq '101'
+ expect(Namer.current_index).to eq 2
+ end
+ end
+ end
+ end
+ end
+
+ context "Chef::Resource#lazy" do
+ with_property ':x' do
+ it "setting x to a lazy value does not run it immediately" do
+ resource.x lazy { Namer.next_index }
+ expect(Namer.current_index).to eq 0
+ end
+ it "you can set x to a lazy value in the instance" do
+ resource.instance_eval do
+ x lazy { Namer.next_index }
+ end
+ expect(resource.x).to eq 1
+ expect(resource.x).to eq 1
+ expect(Namer.current_index).to eq 1
+ end
+ it "retrieving a lazy value pops it open" do
+ resource.x lazy { Namer.next_index }
+ expect(resource.x).to eq 1
+ expect(Namer.current_index).to eq 1
+ end
+ it "retrieving a lazy value twice does not run it a second time" do
+ resource.x lazy { Namer.next_index }
+ expect(resource.x).to eq 1
+ expect(resource.x).to eq 1
+ expect(Namer.current_index).to eq 1
+ end
+ it "setting the same lazy value on two different instances will run it twice" do
+ resource2 = resource_class.new("blah2")
+ l = lazy { Namer.next_index }
+ resource.x l
+ resource2.x l
+ expect(resource2.x).to eq 1
+ expect(resource.x).to eq 2
+ expect(resource2.x).to eq 1
+ end
+
+ context "when the class has a class and instance method named blah" do
+ before do
+ resource_class.class_eval do
+ def self.blah
+ "class"
+ end
+ def blah
+ "instance#{Namer.next_index}"
+ end
+ end
+ end
+ it "retrieving lazy { blah } gets the instance variable" do
+ resource.x lazy { blah }
+ expect(resource.x).to eq "instance1"
+ end
+ it "retrieving lazy { blah } from two different instances gets two different instance variables" do
+ resource2 = resource_class.new("blah2")
+ l = lazy { blah }
+ resource2.x l
+ resource.x l
+ expect(resource2.x).to eq "instance1"
+ expect(resource.x).to eq "instance2"
+ expect(resource2.x).to eq "instance1"
+ end
+ end
+ end
+
+ with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do
+ it "lazy values are not coerced on set" do
+ resource.x lazy { Namer.next_index }
+ expect(Namer.current_index).to eq 0
+ end
+ it "lazy values are coerced on get" do
+ resource.x lazy { Namer.next_index }
+ expect(resource.x).to eq "12"
+ expect(Namer.current_index).to eq 2
+ end
+ it "lazy values are coerced exactly once" do
+ resource.x lazy { Namer.next_index }
+ expect(resource.x).to eq "12"
+ expect(Namer.current_index).to eq 2
+ expect(resource.x).to eq "12"
+ expect(Namer.current_index).to eq 2
+ end
+ end
+
+ with_property ':x, String' do
+ it "lazy values are not validated on set" do
+ resource.x lazy { Namer.next_index }
+ expect(Namer.current_index).to eq 0
+ end
+ it "lazy values are validated on get" do
+ resource.x lazy { Namer.next_index }
+ expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed
+ expect(Namer.current_index).to eq 1
+ end
+ end
+
+ with_property ':x, is: proc { |v| Namer.next_index; true }' do
+ it "lazy values are validated exactly once" do
+ resource.x lazy { Namer.next_index }
+ expect(resource.x).to eq 1
+ expect(Namer.current_index).to eq 2
+ expect(resource.x).to eq 1
+ expect(Namer.current_index).to eq 2
+ end
+ end
+
+ with_property ':x, Integer, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do
+ it "lazy values are not validated or coerced on set" do
+ resource.x lazy { Namer.next_index }
+ expect(Namer.current_index).to eq 0
+ end
+ it "lazy values are coerced before being validated, which fails" do
+ resource.x lazy { Namer.next_index }
+ expect(Namer.current_index).to eq 0
+ expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed
+ expect(Namer.current_index).to eq 2
+ end
+ end
+
+ with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }, is: proc { |v| Namer.next_index; true }' do
+ it "lazy values are coerced and validated exactly once" do
+ resource.x lazy { Namer.next_index }
+ expect(resource.x).to eq "12"
+ expect(Namer.current_index).to eq 3
+ expect(resource.x).to eq "12"
+ expect(Namer.current_index).to eq 3
+ end
+ end
+
+ with_property ':x, String, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do
+ it "lazy values are coerced before being validated, which succeeds" do
+ resource.x lazy { Namer.next_index }
+ expect(resource.x).to eq "12"
+ expect(Namer.current_index).to eq 2
+ end
+ end
+ end
+
+ context "Chef::Resource::PropertyType#coerce" do
+ with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do
+ it "coercion runs on set" do
+ expect(resource.x 10).to eq "101"
+ expect(Namer.current_index).to eq 1
+ end
+ it "coercion sets the value (and coercion does not run on get)" do
+ expect(resource.x 10).to eq "101"
+ expect(resource.x).to eq "101"
+ expect(Namer.current_index).to eq 1
+ end
+ it "coercion runs each time set happens" do
+ expect(resource.x 10).to eq "101"
+ expect(Namer.current_index).to eq 1
+ expect(resource.x 10).to eq "102"
+ expect(Namer.current_index).to eq 2
+ end
+ end
+ with_property ':x, coerce: proc { |x| puts "hi"; Namer.next_index; raise "hi" if x == 10; x }, is: proc { |x| Namer.next_index; x != 10 }' do
+ it "failed coercion fails to set the value" do
+ resource.x 20
+ expect(resource.x).to eq 20
+ expect(Namer.current_index).to eq 2
+ expect { resource.x 10 }.to raise_error 'hi'
+ expect(resource.x).to eq 20
+ expect(Namer.current_index).to eq 3
+ end
+ it "validation does not run if coercion fails" do
+ expect { resource.x 10 }.to raise_error 'hi'
+ expect(Namer.current_index).to eq 1
+ end
+ end
+ end
+
+ context "Chef::Resource::PropertyType validation" do
+ with_property ':x, is: [ proc { |v| Namer.next_index; v.is_a?(Integer) } ]' do
+ it "validation runs on set" do
+ expect(resource.x 10).to eq 10
+ expect(Namer.current_index).to eq 1
+ end
+ it "validation sets the value (and validation does not run on get)" do
+ expect(resource.x 10).to eq 10
+ expect(resource.x).to eq 10
+ expect(Namer.current_index).to eq 1
+ end
+ it "validation runs each time set happens" do
+ expect(resource.x 10).to eq 10
+ expect(Namer.current_index).to eq 1
+ expect(resource.x 10).to eq 10
+ expect(Namer.current_index).to eq 2
+ end
+ it "failed validation fails to set the value" do
+ expect(resource.x 10).to eq 10
+ expect(Namer.current_index).to eq 1
+ expect { resource.x 'blah' }.to raise_error Chef::Exceptions::ValidationFailed
+ expect(resource.x).to eq 10
+ expect(Namer.current_index).to eq 2
+ end
+ end
+ end
+
+ context "Chef::Resource::PropertyType#name_property" do
+ with_property ':x, name_property: true' do
+ it "defaults x to resource.name" do
+ expect(resource.x).to eq 'blah'
+ end
+ it "does not pick up resource.name if set" do
+ expect(resource.x 10).to eq 10
+ expect(resource.x).to eq 10
+ end
+ it "binds to the latest resource.name when run" do
+ resource.name = 'foo'
+ expect(resource.x).to eq 'foo'
+ end
+ it "does not pick up later instances of resource.name" do
+ # TODO honestly not sure this is right, but let's at least test what we
+ # currently do so that if it changes, we know about it.
+ expect(resource.x).to eq 'blah'
+ resource.name = 'foo'
+ expect(resource.x).to eq 'blah'
+ end
+ end
+ with_property ':x, name_property: true, default: 10' do
+ it "chooses default over name_property" do
+ expect(resource.x).to eq 10
+ end
+ end
+ end
+
+ # TODO "is" on a PropertyType: inheritance of coercion, defaults, name_property, identity, lazy values, and validation
+end
diff --git a/spec/unit/resource_property_state_spec.rb b/spec/unit/resource_property_state_spec.rb
new file mode 100644
index 0000000000..c5117c53e3
--- /dev/null
+++ b/spec/unit/resource_property_state_spec.rb
@@ -0,0 +1,492 @@
+require 'support/shared/integration/integration_helper'
+
+describe "Chef::Resource#identity and #state" do
+ include IntegrationSupport
+
+ class NewResourceNamer
+ @i = 0
+ def self.next
+ "chef_resource_property_spec_#{@i += 1}"
+ end
+ end
+
+ def self.new_resource_name
+ NewResourceNamer.next
+ end
+
+ let(:resource_class) do
+ new_resource_name = self.class.new_resource_name
+ Class.new(Chef::Resource) do
+ resource_name new_resource_name
+ end
+ end
+
+ let(:resource) do
+ resource_class.new("blah")
+ end
+
+ def self.english_join(values)
+ return '<nothing>' if values.size == 0
+ return values[0].inspect if values.size == 1
+ "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}"
+ end
+
+ def self.with_property(*properties, &block)
+ tags_index = properties.find_index { |p| !p.is_a?(String)}
+ if tags_index
+ properties, tags = properties[0..tags_index-1], properties[tags_index..-1]
+ else
+ tags = []
+ end
+ properties = properties.map { |property| "property #{property}" }
+ context "With properties #{english_join(properties)}", *tags do
+ before do
+ properties.each do |property_str|
+ resource_class.class_eval(property_str, __FILE__, __LINE__)
+ end
+ end
+ instance_eval(&block)
+ end
+ end
+
+ # identity
+ context "Chef::Resource#identity_attr" do
+ with_property ":x" do
+ it "name is the default identity" do
+ expect(resource_class.identity_attr).to eq :name
+ expect(resource_class.properties[:name].identity?).to be_falsey
+ expect(resource.name).to eq 'blah'
+ expect(resource.identity).to eq 'blah'
+ end
+
+ it "identity_attr :x changes the identity" do
+ expect(resource_class.identity_attr :x).to eq :x
+ expect(resource_class.identity_attr).to eq :x
+ expect(resource_class.properties[:name].identity?).to be_falsey
+ expect(resource_class.properties[:x].identity?).to be_truthy
+
+ expect(resource.x 'woo').to eq 'woo'
+ expect(resource.x).to eq 'woo'
+
+ expect(resource.name).to eq 'blah'
+ expect(resource.identity).to eq 'woo'
+ end
+
+ with_property ":y, identity: true" do
+ context "and identity_attr :x" do
+ before do
+ resource_class.class_eval do
+ identity_attr :x
+ end
+ end
+
+ it "only returns :x as identity" do
+ resource.x 'foo'
+ resource.y 'bar'
+ expect(resource_class.identity_attr).to eq :x
+ expect(resource.identity).to eq 'foo'
+ end
+ it "does not flip y.desired_state off" do
+ resource.x 'foo'
+ resource.y 'bar'
+ expect(resource_class.state_attrs).to eq [ :x, :y ]
+ expect(resource.state).to eq({ x: 'foo', y: 'bar' })
+ end
+ end
+ end
+
+ context "With a subclass" do
+ let(:subresource_class) do
+ new_resource_name = self.class.new_resource_name
+ Class.new(resource_class) do
+ resource_name new_resource_name
+ end
+ end
+ let(:subresource) do
+ subresource_class.new('sub')
+ end
+
+ it "name is the default identity on the subclass" do
+ expect(subresource_class.identity_attr).to eq :name
+ expect(subresource_class.properties[:name].identity?).to be_falsey
+ expect(subresource.name).to eq 'sub'
+ expect(subresource.identity).to eq 'sub'
+ end
+
+ context "With identity_attr :x on the superclass" do
+ before do
+ resource_class.class_eval do
+ identity_attr :x
+ end
+ end
+
+ it "The subclass inherits :x as identity" do
+ expect(subresource_class.identity_attr).to eq :x
+ expect(subresource_class.properties[:name].identity?).to be_falsey
+ expect(subresource_class.properties[:x].identity?).to be_truthy
+
+ subresource.x 'foo'
+ expect(subresource.identity).to eq 'foo'
+ end
+
+ context "With property :y, identity: true on the subclass" do
+ before do
+ subresource_class.class_eval do
+ property :y, identity: true
+ end
+ end
+ it "The subclass's identity includes both x and y" do
+ expect(subresource_class.identity_attr).to eq :x
+ subresource.x 'foo'
+ subresource.y 'bar'
+ expect(subresource.identity).to eq({ x: 'foo', y: 'bar' })
+ end
+ end
+
+ with_property ":y, String" do
+ context "With identity_attr :y on the subclass" do
+ before do
+ subresource_class.class_eval do
+ identity_attr :y
+ end
+ end
+ it "y is part of state" do
+ expect(subresource_class.state_attrs).to eq [ :x, :y ]
+ subresource.x 'foo'
+ subresource.y 'bar'
+ expect(subresource.state).to eq({ x: 'foo', y: 'bar' })
+ end
+ it "y is the identity" do
+ expect(subresource_class.identity_attr).to eq :y
+ subresource.x 'foo'
+ subresource.y 'bar'
+ expect(subresource.identity).to eq 'bar'
+ end
+ it "y still has validation" do
+ expect { subresource.y 12 }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ end
+ end
+ end
+ end
+ end
+
+ with_property ":string_only, String, identity: true", ":string_only2, String" do
+ it "identity_attr does not change validation" do
+ resource_class.identity_attr :string_only
+ expect { resource.string_only 12 }.to raise_error Chef::Exceptions::ValidationFailed
+ expect { resource.string_only2 12 }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ end
+
+ with_property ":x, desired_state: false" do
+ it "identity_attr does not flip on desired_state" do
+ resource_class.identity_attr :x
+ resource.x 'hi'
+ expect(resource.identity).to eq 'hi'
+ expect(resource_class.properties[:x].desired_state?).to be_falsey
+ expect(resource_class.state_attrs).to eq []
+ expect(resource.state).to eq({})
+ end
+ end
+
+ context "With custom property custom_property defined only as methods, using different variables for storage" do
+ before do
+ resource_class.class_eval do
+ def custom_property
+ @blarghle*3
+ end
+ def custom_property=(x)
+ @blarghle = x*2
+ end
+ end
+
+ context "And identity_attr :custom_property" do
+ before do
+ resource_class.class_eval do
+ identity_attr :custom_property
+ end
+ end
+
+ it "identity_attr comes back as :custom_property" do
+ expect(resource_class.properties[:custom_property].identity?).to be_truthy
+ expect(resource_class.identity_attr).to eq :custom_property
+ end
+ it "custom_property becomes part of desired_state" do
+ expect(resource_class.properties[:custom_property].desired_state?).to be_truthy
+ expect(resource_class.state_attrs).to eq [ :custom_property ]
+ end
+ it "identity_attr does not change custom_property's getter or setter" do
+ expect(resource.custom_property = 1).to eq 2
+ expect(resource.custom_property).to eq 6
+ end
+ it "custom_property is returned as the identity" do
+ expect(resource_class.identity_attr).to
+ expect(resource.identity).to be_nil
+ resource.custom_property = 1
+ expect(resource.identity).to eq 6
+ end
+ it "custom_property is part of desired state" do
+ resource.custom_property = 1
+ expect(resource.state).to eq({ custom_property: 6 })
+ end
+ it "property_is_set?(:custom_property) returns true even if it hasn't been set" do
+ expect(resource.property_is_set?(:custom_property)).to be_truthy
+ end
+ end
+ end
+ end
+ end
+
+ context "PropertyType#identity" do
+ with_property ":x, identity: true" do
+ it "name is only part of the identity if an identity attribute is defined" do
+ expect(resource_class.identity_attr).to eq :x
+ resource.x 'woo'
+ expect(resource.identity).to eq 'woo'
+ end
+ end
+
+ with_property ":x, identity: true, default: 'xxx'",
+ ":y, identity: true, default: 'yyy'",
+ ":z, identity: true, default: 'zzz'" do
+ it "identity_attr returns the first identity attribute if multiple are defined" do
+ expect(resource_class.identity_attr).to eq :x
+ end
+ it "identity returns all identity values in a hash if multiple are defined" do
+ resource.x 'foo'
+ resource.y 'bar'
+ resource.z 'baz'
+ expect(resource.identity).to eq({ x: 'foo', y: 'bar', z: 'baz' })
+ end
+ it "identity returns only identity values that are set, and does not include defaults" do
+ resource.x 'foo'
+ resource.z 'baz'
+ expect(resource.identity).to eq({ x: 'foo', z: 'baz' })
+ end
+ it "identity returns only set identity values in a hash, if there is only one set identity value" do
+ resource.x 'foo'
+ expect(resource.identity).to eq({ x: 'foo' })
+ end
+ it "identity returns an empty hash if no identity values are set" do
+ expect(resource.identity).to eq({})
+ end
+ it "identity_attr wipes out any other identity attributes if multiple are defined" do
+ resource_class.identity_attr :y
+ resource.x 'foo'
+ resource.y 'bar'
+ resource.z 'baz'
+ expect(resource.identity).to eq 'bar'
+ end
+ end
+
+ with_property ":x, identity: true, name_property: true" do
+ it "identity when x is not defined returns the value of x" do
+ expect(resource.identity).to eq 'blah'
+ end
+ it "state when x is not defined returns the value of x" do
+ expect(resource.state).to eq({ x: 'blah' })
+ end
+ end
+ end
+
+ # state_attrs
+ context "Chef::Resource#state_attrs" do
+ it "name is not part of state_attrs" do
+ expect(Chef::Resource.state_attrs).to eq []
+ expect(resource_class.state_attrs).to eq []
+ expect(resource.state).to eq({})
+ end
+ with_property ":x", ":y", ":z" do
+ it "x, y and z are state attributes" do
+ resource.x 1
+ resource.y 2
+ resource.z 3
+ expect(resource_class.state_attrs).to eq [ :x, :y, :z ]
+ expect(resource.state).to eq(x: 1, y: 2, z: 3)
+ end
+ it "values that are not set are not included in state" do
+ resource.x 1
+ expect(resource.state).to eq(x: 1)
+ end
+ it "when no values are set, nothing is included in state" do
+ end
+ end
+ with_property ":x", ":y, desired_state: false", ":z, desired_state: true" do
+ it "x and z are state attributes, and y is not" do
+ resource.x 1
+ resource.y 2
+ resource.z 3
+ expect(resource_class.state_attrs).to eq [ :x, :z ]
+ expect(resource.state).to eq(x: 1, z: 3)
+ end
+ end
+ with_property ":x, name_property: true" do
+ it "Unset values with name_property are included in state" do
+ expect(resource.state).to eq(x: 'blah')
+ end
+ it "Set values with name_property are included in state" do
+ resource.x 1
+ expect(resource.state).to eq(x: 1)
+ end
+ end
+ with_property ":x, default: 1" do
+ it "Unset values with defaults are not included in state" do
+ expect(resource.state).to eq({})
+ end
+ it "Set values with defaults are included in state" do
+ resource.x 1
+ expect(resource.state).to eq(x: 1)
+ end
+ end
+ context "With a class with a normal getter and setter" do
+ before do
+ resource_class.class_eval do
+ def x
+ @blah*3
+ end
+ def x=(value)
+ @blah = value*2
+ end
+ end
+ end
+ it "state_attrs(:x) causes the value to be included in properties" do
+ resource_class.state_attrs(:x)
+ resource.x = 1
+
+ expect(resource.x).to eq 6
+ expect(resource.state).to eq(x: 6)
+ end
+ end
+
+ with_property ":x, Integer, identity: true" do
+ it "state_attrs(:x) leaves the property in desired_state" do
+ resource_class.state_attrs(:x)
+ resource.x 10
+
+ expect(resource_class.properties[:x].desired_state?).to be_truthy
+ expect(resource_class.state_attrs).to eq [ :x ]
+ expect(resource.state).to eq(x: 10)
+ end
+ it "state_attrs(:x) does not turn off validation" do
+ resource_class.state_attrs(:x)
+ expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ it "state_attrs(:x) does not turn off identity" do
+ resource_class.state_attrs(:x)
+ resource.x 10
+
+ expect(resource_class.identity_attr).to eq :x
+ expect(resource_class.properties[:x].identity?).to be_truthy
+ expect(resource.identity).to eq 10
+ end
+ end
+
+ with_property ":x, Integer, identity: true, desired_state: false" do
+ before do
+ resource_class.class_eval do
+ def y
+ 20
+ end
+ end
+ end
+ it "state_attrs(:x) sets the property in desired_state" do
+ resource_class.state_attrs(:x)
+ resource.x 10
+
+ expect(resource_class.properties[:x].desired_state?).to be_truthy
+ expect(resource_class.state_attrs).to eq [ :x ]
+ expect(resource.state).to eq(x: 10)
+ end
+ it "state_attrs(:x) does not turn off validation" do
+ resource_class.state_attrs(:x)
+ expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ it "state_attrs(:x) does not turn off identity" do
+ resource_class.state_attrs(:x)
+ resource.x 10
+
+ expect(resource_class.identity_attr).to eq :x
+ expect(resource_class.properties[:x].identity?).to be_truthy
+ expect(resource.identity).to eq 10
+ end
+ it "state_attrs(:y) adds y and removes x from desired state" do
+ resource_class.state_attrs(:y)
+ resource.x 10
+
+ expect(resource_class.properties[:x].desired_state?).to be_falsey
+ expect(resource_class.properties[:y].desired_state?).to be_truthy
+ expect(resource_class.state_attrs).to eq [ :y ]
+ expect(resource.state).to eq(y: 20)
+ end
+ it "state_attrs(:y) does not turn off validation" do
+ resource_class.state_attrs(:y)
+
+ expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ it "state_attrs(:y) does not turn off identity" do
+ resource_class.state_attrs(:y)
+ resource.x 10
+
+ expect(resource_class.identity_attr).to eq :x
+ expect(resource_class.properties[:x].identity?).to be_truthy
+ expect(resource.identity).to eq 10
+ end
+
+ context "With a subclassed resource" do
+ let(:resource_subclass) do
+ new_resource_name = self.class.new_resource_name
+ Class.new(resource_class) do
+ resource_name new_resource_name
+ end
+ end
+ let(:subresource) do
+ resource_subclass.new('blah')
+ end
+ it "state_attrs(:x) sets the property in desired_state" do
+ resource_subclass.state_attrs(:x)
+ subresource.x 10
+
+ expect(resource_subclass.properties[:x].desired_state?).to be_truthy
+ expect(resource_subclass.state_attrs).to eq [ :x ]
+ expect(subresource.state).to eq(x: 10)
+ end
+ it "state_attrs(:x) does not turn off validation" do
+ resource_subclass.state_attrs(:x)
+ expect { subresource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ it "state_attrs(:x) does not turn off identity" do
+ resource_subclass.state_attrs(:x)
+ subresource.x 10
+
+ expect(resource_subclass.identity_attr).to eq :x
+ expect(resource_subclass.properties[:x].identity?).to be_truthy
+ expect(subresource.identity).to eq 10
+ end
+ it "state_attrs(:y) adds y and removes x from desired state" do
+ resource_subclass.state_attrs(:y)
+ subresource.x 10
+
+ expect(resource_subclass.properties[:x].desired_state?).to be_falsey
+ expect(resource_subclass.properties[:y].desired_state?).to be_truthy
+ expect(resource_subclass.state_attrs).to eq [ :y ]
+ expect(subresource.state).to eq(y: 20)
+ end
+ it "state_attrs(:y) does not turn off validation" do
+ resource_subclass.state_attrs(:y)
+
+ expect { subresource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ it "state_attrs(:y) does not turn off identity" do
+ resource_subclass.state_attrs(:y)
+ subresource.x 10
+
+ expect(resource_subclass.identity_attr).to eq :x
+ expect(resource_subclass.properties[:x].identity?).to be_truthy
+ expect(subresource.identity).to eq 10
+ end
+ end
+ end
+ end
+
+end
diff --git a/spec/unit/resource_property_validation_spec.rb b/spec/unit/resource_property_validation_spec.rb
new file mode 100644
index 0000000000..42df2ea003
--- /dev/null
+++ b/spec/unit/resource_property_validation_spec.rb
@@ -0,0 +1,333 @@
+require 'support/shared/integration/integration_helper'
+
+describe "Chef::Resource.property validation" do
+ include IntegrationSupport
+
+ class Namer
+ @i = 0
+ def self.next_resource_name
+ "chef_resource_property_spec_#{@i += 1}"
+ end
+ def self.reset_index
+ @current_index = 0
+ end
+ def self.current_index
+ @current_index
+ end
+ def self.next_index
+ @current_index += 1
+ end
+ end
+
+ def lazy(&block)
+ Chef::DelayedEvaluator.new(&block)
+ end
+
+ before do
+ Namer.reset_index
+ end
+
+ def self.new_resource_name
+ Namer.next_resource_name
+ end
+
+ let(:resource_class) do
+ new_resource_name = self.class.new_resource_name
+ Class.new(Chef::Resource) do
+ resource_name new_resource_name
+ def blah
+ Namer.next_index
+ end
+ def self.blah
+ "class#{Namer.next_index}"
+ end
+ end
+ end
+
+ let(:resource) do
+ resource_class.new("blah")
+ end
+
+ def self.english_join(values)
+ return '<nothing>' if values.size == 0
+ return values[0].inspect if values.size == 1
+ "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}"
+ end
+
+ def self.with_property(*properties, &block)
+ tags_index = properties.find_index { |p| !p.is_a?(String)}
+ if tags_index
+ properties, tags = properties[0..tags_index-1], properties[tags_index..-1]
+ else
+ tags = []
+ end
+ properties = properties.map { |property| "property #{property}" }
+ context "With properties #{english_join(properties)}", *tags do
+ before do
+ properties.each do |property_str|
+ resource_class.class_eval(property_str, __FILE__, __LINE__)
+ end
+ end
+ instance_eval(&block)
+ end
+ end
+
+ def self.validation_test(validation, success_values, failure_values)
+ with_property ":x, #{validation}" do
+ success_values.each do |v|
+ it "value #{v.inspect} is valid" do
+ expect(resource.x v).to eq v
+ end
+ end
+ failure_values.each do |v|
+ if v.nil?
+ it "setting value to #{v.inspect} does not change the value" do
+ Chef::Config[:treat_deprecation_warnings_as_errors] = false
+ resource.x success_values.first
+ expect(resource.x v).to eq success_values.first
+ expect(resource.x).to eq success_values.first
+ end
+ else
+ it "value #{v.inspect} is invalid" do
+ expect { resource.x v }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ end
+ end
+ end
+ end
+
+ # Bare types
+ context "bare types" do
+ validation_test 'String',
+ [ 'hi' ],
+ [ 10, nil ]
+
+ validation_test ':a',
+ [ :a ],
+ [ :b, nil ]
+
+ validation_test ':a, is: :b',
+ [ :a, :b ],
+ [ :c, nil ]
+
+ validation_test ':a, is: [ :b, :c ]',
+ [ :a, :b, :c ],
+ [ :d, nil ]
+
+ validation_test '[ :a, :b ], is: :c',
+ [ :a, :b, :c ],
+ [ :d, nil ]
+
+ validation_test '[ :a, :b ], is: [ :c, :d ]',
+ [ :a, :b, :c, :d ],
+ [ :e, nil ]
+
+ validation_test 'nil',
+ [ nil ],
+ [ :a ]
+
+ validation_test '[ nil ]',
+ [ nil ],
+ [ :a ]
+
+ validation_test '[]',
+ [ :a ],
+ []
+ end
+
+ # is
+ context "is" do
+ # Class
+ validation_test 'is: String',
+ [ 'a', '' ],
+ [ nil, :a, 1 ]
+
+ # Value
+ validation_test 'is: :a',
+ [ :a ],
+ [ :b, nil ]
+
+ validation_test 'is: [ :a, :b ]',
+ [ :a, :b ],
+ [ [ :a, :b ], nil ]
+
+ validation_test 'is: [ [ :a, :b ] ]',
+ [ [ :a, :b ] ],
+ [ :a, :b, nil ]
+
+ # Regex
+ validation_test 'is: /abc/',
+ [ 'abc', 'wowabcwow' ],
+ [ '', 'abac', nil ]
+
+ # PropertyType
+ validation_test 'is: PropertyType.new(is: :a)',
+ [ :a ],
+ [ :b, nil ]
+
+ # RSpec Matcher
+ class Globalses
+ extend RSpec::Matchers
+ end
+
+ validation_test "is: Globalses.eq(10)",
+ [ 10 ],
+ [ 1, nil ]
+
+ # Proc
+ validation_test 'is: proc { |x| x }',
+ [ true, 1 ],
+ [ false, nil ]
+
+ validation_test 'is: proc { |x| x > blah }',
+ [ 10 ],
+ [ -1 ]
+
+ validation_test 'is: nil',
+ [ nil ],
+ [ 'a' ]
+
+ validation_test 'is: [ String, nil ]',
+ [ 'a', nil ],
+ [ :b ]
+ end
+
+ # Combination
+ context "combination" do
+ validation_test 'is: String, equal_to: "a"',
+ [ 'a' ],
+ [ 'b', nil ]
+ end
+
+ # equal_to
+ context "equal_to" do
+ # Value
+ validation_test 'equal_to: :a',
+ [ :a ],
+ [ :b, nil ]
+
+ validation_test 'equal_to: [ :a, :b ]',
+ [ :a, :b ],
+ [ [ :a, :b ], nil ]
+
+ validation_test 'equal_to: [ [ :a, :b ] ]',
+ [ [ :a, :b ] ],
+ [ :a, :b, nil ]
+
+ validation_test 'equal_to: nil',
+ [ nil ],
+ [ 'a' ]
+
+ validation_test 'equal_to: [ "a", nil ]',
+ [ 'a', nil ],
+ [ 'b' ]
+
+ validation_test 'equal_to: [ nil, "a" ]',
+ [ 'a', nil ],
+ [ 'b' ]
+ end
+
+ # kind_of
+ context "kind_of" do
+ validation_test 'kind_of: String',
+ [ 'a' ],
+ [ :b, nil ]
+
+ validation_test 'kind_of: [ String, Symbol ]',
+ [ 'a', :b ],
+ [ 1, nil ]
+
+ validation_test 'kind_of: [ Symbol, String ]',
+ [ 'a', :b ],
+ [ 1, nil ]
+
+ validation_test 'kind_of: NilClass',
+ [ nil ],
+ [ 'a' ]
+
+ validation_test 'kind_of: [ NilClass, String ]',
+ [ nil, 'a' ],
+ [ :a ]
+ end
+
+ # regex
+ context "regex" do
+ validation_test 'regex: /abc/',
+ [ 'xabcy' ],
+ [ 'gbh', 123, nil ]
+
+ validation_test 'regex: [ /abc/, /z/ ]',
+ [ 'xabcy', 'aza' ],
+ [ 'gbh', 123, nil ]
+
+ validation_test 'regex: [ /z/, /abc/ ]',
+ [ 'xabcy', 'aza' ],
+ [ 'gbh', 123, nil ]
+ end
+
+ # callbacks
+ context "callbacks" do
+ validation_test 'callbacks: { "a" => proc { |x| x > 10 }, "b" => proc { |x| x%2 == 0 } }',
+ [ 12 ],
+ [ 11, 4 ]
+
+ validation_test 'callbacks: { "a" => proc { |x| x%2 == 0 }, "b" => proc { |x| x > 10 } }',
+ [ 12 ],
+ [ 11, 4 ]
+
+ validation_test 'callbacks: { "a" => proc { |x| x.nil? } }',
+ [ nil ],
+ [ 'a' ]
+ end
+
+ # respond_to
+ context "respond_to" do
+ validation_test 'respond_to: :split',
+ [ 'hi' ],
+ [ 1, nil ]
+
+ validation_test 'respond_to: "split"',
+ [ 'hi' ],
+ [ 1, nil ]
+
+ validation_test 'respond_to: [ :split, :to_s ]',
+ [ 'hi' ],
+ [ 1, nil ]
+
+ validation_test 'respond_to: %w(split to_s)',
+ [ 'hi' ],
+ [ 1, nil ]
+
+ validation_test 'respond_to: [ :to_s, :split ]',
+ [ 'hi' ],
+ [ 1, nil ]
+ end
+
+ context "cannot_be" do
+ validation_test 'cannot_be: :empty',
+ [ nil, 1, [1,2], { a: 10 } ],
+ [ [] ]
+
+ validation_test 'cannot_be: "empty"',
+ [ nil, 1, [1,2], { a: 10 } ],
+ [ [] ]
+
+ validation_test 'cannot_be: [ :empty, :nil ]',
+ [ 1, [1,2], { a: 10 } ],
+ [ [], nil ]
+
+ validation_test 'cannot_be: [ "empty", "nil" ]',
+ [ 1, [1,2], { a: 10 } ],
+ [ [], nil ]
+
+ validation_test 'cannot_be: [ :nil, :empty ]',
+ [ 1, [1,2], { a: 10 } ],
+ [ [], nil ]
+
+ validation_test 'cannot_be: [ :empty, :nil, :blahblah ]',
+ [ 1, [1,2], { a: 10 } ],
+ [ [], nil ]
+ end
+
+ # TODO required
+end
diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb
index fefe78fbda..10a45f13c4 100644
--- a/spec/unit/resource_spec.rb
+++ b/spec/unit/resource_spec.rb
@@ -59,8 +59,8 @@ describe Chef::Resource do
end
describe "when declaring the identity attribute" do
- it "has no identity attribute by default" do
- expect(Chef::Resource.identity_attr).to be_nil
+ it "identity attribute is name by default" do
+ expect(Chef::Resource.identity_attr).to eq :name
end
it "sets an identity attribute" do