summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Keiser <john@johnkeiser.com>2015-07-03 14:16:32 -0600
committerJohn Keiser <john@johnkeiser.com>2015-07-03 14:16:32 -0600
commitcfd2b1fa1b26e8b0aa92a5ba8bd294b96379b2fa (patch)
treed0715ed8eaed714460b168103ef39b748e85b8ba
parented2b0904361448173e5644dddacb48399fd8dc68 (diff)
parent78f72c82716acbd504f6974555cb45d61e895032 (diff)
downloadchef-cfd2b1fa1b26e8b0aa92a5ba8bd294b96379b2fa.tar.gz
Merge branch 'jk/property-state'
-rw-r--r--lib/chef/constants.rb27
-rw-r--r--lib/chef/delayed_evaluator.rb21
-rw-r--r--lib/chef/exceptions.rb1
-rw-r--r--lib/chef/mixin/params_validate.rb192
-rw-r--r--lib/chef/property.rb539
-rw-r--r--lib/chef/resource.rb371
-rw-r--r--spec/unit/mixin/params_validate_spec.rb6
-rw-r--r--spec/unit/property/state_spec.rb654
-rw-r--r--spec/unit/property/validation_spec.rb54
-rw-r--r--spec/unit/property_spec.rb258
-rw-r--r--spec/unit/resource_spec.rb4
11 files changed, 1542 insertions, 585 deletions
diff --git a/lib/chef/constants.rb b/lib/chef/constants.rb
new file mode 100644
index 0000000000..d39ce4c68d
--- /dev/null
+++ b/lib/chef/constants.rb
@@ -0,0 +1,27 @@
+#
+# Author:: John Keiser <jkeiser@chef.io>
+# Copyright:: Copyright (c) 2015 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
+ NOT_PASSED = Object.new
+ def NOT_PASSED.to_s
+ "NOT_PASSED"
+ end
+ def NOT_PASSED.inspect
+ to_s
+ end
+ NOT_PASSED.freeze
+end
diff --git a/lib/chef/delayed_evaluator.rb b/lib/chef/delayed_evaluator.rb
new file mode 100644
index 0000000000..9f18a53445
--- /dev/null
+++ b/lib/chef/delayed_evaluator.rb
@@ -0,0 +1,21 @@
+#
+# Author:: John Keiser <jkeiser@chef.io>
+# Copyright:: Copyright (c) 2015 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
+ class DelayedEvaluator < Proc
+ end
+end
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index e7da6fc110..d5c4c4832b 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -103,6 +103,7 @@ class Chef
class ProviderNotFound < RuntimeError; end
NoProviderAvailable = ProviderNotFound
class VerificationNotFound < RuntimeError; end
+ class MultipleIdentityError < RuntimeError; end
# Can't find a Resource of this type that is valid on this platform.
class NoSuchResourceType < NameError
diff --git a/lib/chef/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb
index 4ab016249f..322caea474 100644
--- a/lib/chef/mixin/params_validate.rb
+++ b/lib/chef/mixin/params_validate.rb
@@ -15,11 +15,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-class Chef
- NOT_PASSED = Object.new
+require 'chef/constants'
+require 'chef/property'
+require 'chef/delayed_evaluator'
- class DelayedEvaluator < Proc
- end
+class Chef
module Mixin
module ParamsValidate
@@ -34,20 +34,55 @@ class Chef
# Would raise an exception if the value of :one above is not a kind_of? string. Valid
# map options are:
#
- # :default:: Sets the default value for this parameter.
- # :callbacks:: Takes a hash of Procs, which should return true if the argument is valid.
- # The key will be inserted into the error message if the Proc does not return true:
- # "Option #{key}'s value #{value} #{message}!"
- # :kind_of:: Ensure that the value is a kind_of?(Whatever). If passed an array, it will ensure
- # that the value is one of those types.
- # :respond_to:: Ensure that the value has a given method. Takes one method name or an array of
- # method names.
- # :required:: Raise an exception if this parameter is missing. Valid values are true or false,
- # by default, options are not required.
- # :regex:: Match the value of the parameter against a regular expression.
- # :equal_to:: Match the value of the parameter with ==. An array means it can be equal to any
- # of the values.
+ # @param opts [Hash<Symbol,Object>] Validation opts.
+ # @option opts [Object,Array] :is An object, or list of
+ # objects, that must match the value using Ruby's `===` operator
+ # (`opts[:is].any? { |v| v === value }`). (See #_pv_is.)
+ # @option opts [Object,Array] :equal_to An object, or list
+ # of objects, that must be equal to the value using Ruby's `==`
+ # operator (`opts[:is].any? { |v| v == value }`) (See #_pv_equal_to.)
+ # @option opts [Regexp,Array<Regexp>] :regex An object, or
+ # list of objects, that must match the value with `regex.match(value)`.
+ # (See #_pv_regex)
+ # @option opts [Class,Array<Class>] :kind_of A class, or
+ # list of classes, that the value must be an instance of. (See
+ # #_pv_kind_of.)
+ # @option opts [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). (See
+ # #_pv_callbacks.)
+ # @option opts [Symbol,Array<Symbol>] :respond_to A method
+ # name, or list of method names, the value must respond to. (See
+ # #_pv_respond_to.)
+ # @option opts [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). (See #_pv_cannot_be.)
+ # @option opts [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).
+ # (See #_pv_coerce.) (See #_pv_coerce.)
+ # @option opts [Boolean] :required `true` if this property
+ # must be present and not `nil`; `false` otherwise. This is checked
+ # after the resource is fully initialized. (See #_pv_required.)
+ # @option opts [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. (See
+ # #_pv_name_property.)
+ # @option opts [Boolean] :name_attribute Same as `name_property`.
+ # @option opts [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). (See #_pv_default.)
+ #
def validate(opts, map)
+ map = map.validation_options if map.is_a?(Property)
+
#--
# validate works by taking the keys in the validation map, assuming it's a hash, and
# looking for _pv_:symbol as methods. Assuming it find them, it calls the right
@@ -84,91 +119,8 @@ class Chef
end
def set_or_return(symbol, value, validation)
- symbol = symbol.to_sym
- iv_symbol = :"@#{symbol}"
-
- # Steal default, coerce, name_property and required from validation
- # so that we can handle the order in which they are applied
- validation = validation.dup
- if validation.has_key?(:default)
- default = validation.delete(:default)
- elsif validation.has_key?('default')
- default = validation.delete('default')
- else
- default = NOT_PASSED
- end
- coerce = validation.delete(:coerce)
- coerce ||= validation.delete('coerce')
- name_property = validation.delete(:name_property)
- name_property ||= validation.delete('name_property')
- name_property ||= validation.delete(:name_attribute)
- name_property ||= validation.delete('name_attribute')
- required = validation.delete(:required)
- required ||= validation.delete('required')
-
- opts = {}
- # If the user passed NOT_PASSED, or passed nil, then this is a get.
- if value == NOT_PASSED || (value.nil? && !explicitly_allows_nil?(symbol, validation))
-
- # Get the value if there is one
- if self.instance_variable_defined?(iv_symbol)
- opts[symbol] = self.instance_variable_get(iv_symbol)
-
- # Handle lazy values
- if opts[symbol].is_a?(DelayedEvaluator)
- if opts[symbol].arity >= 1
- opts[symbol] = opts[symbol].call(self)
- else
- opts[symbol] = opts[symbol].call
- end
-
- # Coerce and validate the default value
- _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required
- _pv_coerce(opts, symbol, coerce) if coerce
- validate(opts, { symbol => validation })
- end
-
- # Get the default value
- else
- _pv_default(opts, symbol, default) unless default == NOT_PASSED
- _pv_name_property(opts, symbol, name_property)
- _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required
-
- if opts.has_key?(symbol)
- # Handle lazy defaults.
- if opts[symbol].is_a?(DelayedEvaluator)
- if opts[symbol].arity >= 1
- opts[symbol] = opts[symbol].call(self)
- else
- opts[symbol] = instance_eval(&opts[symbol])
- end
- end
-
- # Coerce and validate the default value
- _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required
- _pv_coerce(opts, symbol, coerce) if coerce
- # We presently do not validate defaults, for backwards compatibility.
-# validate(opts, { symbol => validation })
-
- # Defaults are presently "stickily" set on the instance
- self.instance_variable_set(iv_symbol, opts[symbol])
- end
- end
-
- # Set the value
- else
- opts[symbol] = value
- unless opts[symbol].is_a?(DelayedEvaluator)
- # Coerce and validate the value
- _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required
- _pv_coerce(opts, symbol, coerce) if coerce
- validate(opts, { symbol => validation })
- end
-
- self.instance_variable_set(iv_symbol, opts[symbol])
- end
-
- opts[symbol]
+ property = SetOrReturnProperty.new(name: symbol, **validation)
+ property.call(self, value)
end
private
@@ -193,8 +145,9 @@ class Chef
if is_required
return true if opts.has_key?(key.to_s) && (explicitly_allows_nil || !opts[key.to_s].nil?)
return true if opts.has_key?(key.to_sym) && (explicitly_allows_nil || !opts[key.to_sym].nil?)
- raise Exceptions::ValidationFailed, "Required argument #{key} is missing!"
+ raise Exceptions::ValidationFailed, "Required argument #{key.inspect} is missing!"
end
+ true
end
#
@@ -425,9 +378,9 @@ class Chef
# x 1 #=> invalid
# ```
#
- # @example PropertyType
+ # @example Property
# ```ruby
- # type = PropertyType.new(is: String)
+ # type = Property.new(is: String)
# property :x, type
# x 'foo' #=> valid
# x 1 #=> invalid
@@ -448,8 +401,12 @@ class Chef
value = _pv_opts_lookup(opts, key)
to_be = [ to_be ].flatten(1)
to_be.each do |tb|
- if tb.is_a?(Proc)
+ case tb
+ when Proc
return true if instance_exec(value, &tb)
+ when Property
+ validate(opts, { key => tb.validation_options })
+ return true
else
return true if tb === value
end
@@ -484,6 +441,29 @@ class Chef
opts[key.to_sym] = instance_exec(opts[key], &coercer)
end
end
+
+ # Used by #set_or_return to avoid emitting a deprecation warning for
+ # "value nil" and to keep default stickiness working exactly the same
+ # @api private
+ class SetOrReturnProperty < Chef::Property
+ def get(resource)
+ value = super
+ # All values are sticky, frozen or not
+ if !is_set?(resource)
+ set_value(resource, value)
+ end
+ value
+ end
+
+ def call(resource, value=NOT_PASSED)
+ # setting to nil does a get
+ if value.nil? && !explicitly_accepts_nil?(resource)
+ get(resource)
+ else
+ super
+ end
+ end
+ end
end
end
end
diff --git a/lib/chef/property.rb b/lib/chef/property.rb
new file mode 100644
index 0000000000..408090d37b
--- /dev/null
+++ b/lib/chef/property.rb
@@ -0,0 +1,539 @@
+#
+# Author:: John Keiser <jkeiser@chef.io>
+# Copyright:: Copyright (c) 2015 John Keiser.
+# 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.
+#
+
+require 'chef/exceptions'
+require 'chef/delayed_evaluator'
+
+class Chef
+ #
+ # 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 full
+ # support for lazy values.
+ #
+ # @see Chef::Resource.property
+ # @see Chef::DelayedEvaluator
+ #
+ class Property
+ #
+ # Create a reusable property type that can be used in multiple properties
+ # in different resources.
+ #
+ # @param options [Hash<Symbol,Object>] Validation options. See Chef::Resource.property for
+ # the list of options.
+ #
+ # @example
+ # Property.derive(default: 'hi')
+ #
+ def self.derive(**options)
+ new(**options)
+ end
+
+ #
+ # Create a new property.
+ #
+ # @param options [Hash<Symbol,Object>] Property options, including
+ # control options here, as well as validation options (see
+ # Chef::Mixin::ParamsValidate#validate for a description of validation
+ # options).
+ # @option options [Symbol] :name The name of this property.
+ # @option options [Class] :declared_in The class this property comes from.
+ # @option options [Symbol] :instance_variable_name The instance variable
+ # tied to this property. Must include a leading `@`. Defaults to `@<name>`.
+ # `nil` means the property is opaque and not tied to a specific instance
+ # variable.
+ # @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 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 [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) and cached. If not, the value will be frozen with Object#freeze
+ # to prevent users from modifying it in an instance.
+ # @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.
+ #
+ def initialize(**options)
+ options.each { |k,v| options[k.to_sym] = v if k.is_a?(String) }
+ options[:name_property] = options.delete(:name_attribute) unless options.has_key?(:name_property)
+ @options = options
+
+ if options.has_key?(:default)
+ options[:default] = options[:default].freeze
+ end
+ options[:name] = options[:name].to_sym if options[:name]
+ options[:instance_variable_name] = options[:instance_variable_name].to_sym if options[:instance_variable_name]
+ end
+
+ #
+ # The name of this property.
+ #
+ # @return [String]
+ #
+ def name
+ options[:name]
+ end
+
+ #
+ # The class this property was defined in.
+ #
+ # @return [Class]
+ #
+ def declared_in
+ options[:declared_in]
+ end
+
+ #
+ # The instance variable associated with this property.
+ #
+ # Defaults to `@<name>`
+ #
+ # @return [Symbol]
+ #
+ def instance_variable_name
+ if options.has_key?(:instance_variable_name)
+ options[:instance_variable_name]
+ elsif name
+ :"@#{name}"
+ end
+ end
+
+ #
+ # The raw default value for this resource.
+ #
+ # Does not coerce or validate the default. Does not evaluate lazy values.
+ #
+ # Defaults to `lazy { name }` if name_property is true; otherwise defaults to
+ # `nil`
+ #
+ def default
+ return options[:default] if options.has_key?(:default)
+ return Chef::DelayedEvaluator.new { name } if name_property?
+ nil
+ end
+
+ #
+ # Whether this is part of the resource's natural identity or not.
+ #
+ # @return [Boolean]
+ #
+ def identity?
+ options[:identity]
+ end
+
+ #
+ # Whether this is part of desired state or not.
+ #
+ # Defaults to true.
+ #
+ # @return [Boolean]
+ #
+ def desired_state?
+ return true if !options.has_key?(:desired_state)
+ options[:desired_state]
+ end
+
+ #
+ # Whether this is name_property or not.
+ #
+ # @return [Boolean]
+ #
+ def name_property?
+ options[:name_property]
+ end
+
+ #
+ # Whether this property has a default value.
+ #
+ # @return [Boolean]
+ #
+ def has_default?
+ options.has_key?(:default) || name_property?
+ end
+
+ #
+ # Whether this property is required or not.
+ #
+ # @return [Boolean]
+ #
+ def required?
+ options[:required]
+ end
+
+ #
+ # Validation options. (See Chef::Mixin::ParamsValidate#validate.)
+ #
+ # @return [Hash<Symbol,Object>]
+ #
+ def validation_options
+ @validation_options ||= options.reject { |k,v|
+ [:declared_in,:name,:instance_variable_name,:desired_state,:identity,:default,:name_property,:coerce,:required].include?(k)
+ }
+ end
+
+ #
+ # Handle the property being called.
+ #
+ # The base implementation does the property get-or-set:
+ #
+ # ```ruby
+ # resource.myprop # get
+ # resource.myprop value # set
+ # ```
+ #
+ # Subclasses may implement this with any arguments they want, as long as
+ # the corresponding DSL calls it correctly.
+ #
+ # @param resource [Chef::Resource] The resource to get the property from.
+ # @param value The value to set (or NOT_PASSED if it is a get).
+ #
+ # @return The current value of the property. If it is a `set`, lazy values
+ # will be returned without running, validating or coercing. If it is a
+ # `get`, the non-lazy, coerced, validated value will always be returned.
+ #
+ def call(resource, value=NOT_PASSED)
+ if value == NOT_PASSED
+ return get(resource)
+ end
+
+ # myprop nil is sometimes a get (backcompat)
+ if value.nil? && !explicitly_accepts_nil?(resource)
+ # 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.")
+ return get(resource)
+ end
+
+ # Anything else (myprop value) is a set
+ set(resource, value)
+ end
+
+ #
+ # Get the property value from the resource, handling lazy values,
+ # defaults, and validation.
+ #
+ # - If the property's value is lazy, it is evaluated, coerced and validated.
+ # - If the property has no value, and is required, raises ValidationFailed.
+ # - If the property has no value, but has a lazy default, it is evaluated,
+ # coerced and validated. If the evaluated value is frozen, the resulting
+ # - If the property has no value, but has a default, the default value
+ # will be returned and frozen. 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.
+ #
+ # @return The value of the property.
+ #
+ # @raise Chef::Exceptions::ValidationFailed If the value is invalid for
+ # this property, or if the value is required and not set.
+ #
+ def get(resource)
+ if is_set?(resource)
+ value = get_value(resource)
+ if value.is_a?(DelayedEvaluator)
+ value = exec_in_resource(resource, value)
+ value = coerce(resource, value)
+ validate(resource, value)
+ end
+ value
+
+ else
+ if has_default?
+ value = default
+ if value.is_a?(DelayedEvaluator)
+ value = exec_in_resource(resource, value)
+ end
+
+ value = coerce(resource, value)
+
+ # We don't validate defaults
+
+ # If the value is mutable (non-frozen), we set it on the instance
+ # so that people can mutate it. (All constant default values are
+ # frozen.)
+ if !value.frozen?
+ set_value(resource, value)
+ end
+
+ value
+
+ elsif required?
+ raise Chef::Exceptions::ValidationFailed, "#{name} is required"
+ end
+ 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 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, value)
+ unless value.is_a?(DelayedEvaluator)
+ value = coerce(resource, value)
+ validate(resource, value)
+ end
+ set_value(resource, value)
+ end
+
+ #
+ # Find out whether this property has been set.
+ #
+ # This will be true if:
+ # - The user explicitly set the value
+ # - 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.
+ #
+ # @return [Boolean]
+ #
+ def is_set?(resource)
+ value_is_set?(resource)
+ end
+
+ #
+ # Reset the value of this property so that is_set? will return false and the
+ # default will be returned in the future.
+ #
+ # @param resource [Chef::Resource] The resource to get the property from.
+ #
+ def reset(resource)
+ reset_value(resource)
+ end
+
+ #
+ # Coerce an input value into canonical form for the property.
+ #
+ # After coercion, the value is suitable for storage in the resource.
+ # You must validate values after coercion, however.
+ #
+ # Does no special handling for lazy values.
+ #
+ # @param resource [Chef::Resource] The resource we're coercing against
+ # (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, value)
+ if options.has_key?(:coerce)
+ value = exec_in_resource(resource, options[:coerce], value)
+ end
+ value
+ end
+
+ #
+ # Validate a value.
+ #
+ # Calls Chef::Mixin::ParamsValidate#validate with #validation_options as
+ # options.
+ #
+ # @param resource [Chef::Resource] The resource we're validating against
+ # (to provide context for the validate).
+ # @param value The value to validate.
+ #
+ # @raise Chef::Exceptions::ValidationFailed If the value is invalid for
+ # this property.
+ #
+ def validate(resource, value)
+ resource.validate({ name => value }, { name => validation_options })
+ end
+
+ #
+ # Derive a new Property that is just like this one, except with some added or
+ # changed options.
+ #
+ # @param options [Hash<Symbol,Object>] List of options that would be passed
+ # to #initialize.
+ #
+ # @return [Property] The new property type.
+ #
+ def derive(**modified_options)
+ Property.new(**options.merge(**modified_options))
+ end
+
+ #
+ # Emit the DSL for this property into the resource class (`declared_in`).
+ #
+ # Creates a getter and setter for the property.
+ #
+ def emit_dsl
+ # We don't create the getter/setter if it's a custom property; we will
+ # be using the existing getter/setter to manipulate it instead.
+ return if !instance_variable_name
+
+ # We prefer this form because the property name won't show up in the
+ # stack trace if you use `define_method`.
+ declared_in.class_eval <<-EOM, __FILE__, __LINE__+1
+ def #{name}(value=NOT_PASSED)
+ self.class.properties[#{name.inspect}].call(self, value)
+ end
+ def #{name}=(value)
+ self.class.properties[#{name.inspect}].set(self, value)
+ end
+ EOM
+ rescue SyntaxError
+ # If the name is not a valid ruby name, we use define_method.
+ resource_class.define_method(name) do |value=NOT_PASSED|
+ self.class.properties[name].call(self, value)
+ end
+ resource_class.define_method("#{name}=") do |value|
+ self.class.properties[name].set(self, value)
+ end
+ end
+
+ protected
+
+ #
+ # The options this Property will use for get/set behavior and validation.
+ #
+ # @see #initialize for a list of valid options.
+ #
+ attr_reader :options
+
+ #
+ # Find out whether this type accepts nil explicitly.
+ #
+ # A type accepts nil explicitly if "is" allows nil, it validates as nil, *and* is not simply
+ # an empty type.
+ #
+ # These examples accept nil explicitly:
+ # ```ruby
+ # property :a, [ String, nil ]
+ # property :a, [ String, NilClass ]
+ # property :a, [ String, proc { |v| v.nil? } ]
+ # ```
+ #
+ # This does not (because the "is" doesn't exist or doesn't have nil):
+ #
+ # ```ruby
+ # property :x, String
+ # ```
+ #
+ # These do not, even though nil would validate fine (because they do not
+ # have "is"):
+ #
+ # ```ruby
+ # property :a
+ # property :a, equal_to: [ 1, 2, 3, nil ]
+ # property :a, kind_of: [ String, NilClass ]
+ # property :a, respond_to: [ ]
+ # property :a, callbacks: { "a" => proc { |v| v.nil? } }
+ # ```
+ #
+ # @param resource [Chef::Resource] The resource we're coercing against
+ # (to provide context for the coerce).
+ #
+ # @return [Boolean] Whether this value explicitly accepts nil.
+ #
+ # @api private
+ def explicitly_accepts_nil?(resource)
+ options.has_key?(:is) && resource.send(:_pv_is, { name => nil }, name, options[:is], raise_error: false)
+ end
+
+ def get_value(resource)
+ if instance_variable_name
+ resource.instance_variable_get(instance_variable_name)
+ else
+ resource.send(name)
+ end
+ end
+
+ def set_value(resource, value)
+ if instance_variable_name
+ resource.instance_variable_set(instance_variable_name, value)
+ else
+ resource.send(name, value)
+ end
+ end
+
+ def value_is_set?(resource)
+ if instance_variable_name
+ resource.instance_variable_defined?(instance_variable_name)
+ else
+ true
+ end
+ end
+
+ def reset_value(resource)
+ if instance_variable_name
+ if value_is_set?(resource)
+ resource.remove_instance_variable(instance_variable_name)
+ end
+ else
+ raise ArgumentError, "Property #{name} has no instance variable defined and cannot be reset"
+ end
+ end
+
+ def exec_in_resource(resource, proc, *args)
+ if resource
+ if proc.arity > args.size
+ value = proc.call(resource, *args)
+ else
+ value = resource.instance_exec(*args, &proc)
+ end
+ else
+ value = proc.call
+ end
+
+ if value.is_a?(DelayedEvaluator)
+ value = coerce(resource, value)
+ validate(resource, value)
+ end
+ value
+ end
+ end
+end
diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb
index 696089fe3e..5e9ba42703 100644
--- a/lib/chef/resource.rb
+++ b/lib/chef/resource.rb
@@ -101,7 +101,7 @@ class Chef
# @param run_context The context of the Chef run. Corresponds to #run_context.
#
def initialize(name, run_context=nil)
- name(name)
+ name(name) unless name.nil?
@run_context = run_context
@noop = nil
@before = nil
@@ -130,37 +130,27 @@ class Chef
end
#
- # The name of this particular resource.
+ # The list of properties defined on this resource.
#
- # This special resource attribute is set automatically from the declaration
- # of the resource, e.g.
+ # Everything defined with `property` is in this list.
#
- # execute 'Vitruvius' do
- # command 'ls'
- # end
- #
- # Will set the name to "Vitruvius".
+ # @param include_superclass [Boolean] `true` to include properties defined
+ # on superclasses; `false` or `nil` to return the list of properties
+ # directly on this class.
#
- # This is also used in to_s to show the resource name, e.g. `execute[Vitruvius]`.
- #
- # This is also used for resource notifications and subscribes in the same manner.
- #
- # This will coerce any object into a string via #to_s. Arrays are a special case
- # so that `package ["foo", "bar"]` becomes package[foo, bar] instead of the more
- # awkward `package[["foo", "bar"]]` that #to_s would produce.
- #
- # @param name [Object] The name to set, typically a String or Array
- # @return [String] The name of this Resource.
+ # @return [Hash<Symbol,Property>] The list of property names and types.
#
- def name(name=nil)
- if !name.nil?
- if name.is_a?(Array)
- @name = name.join(', ')
+ def self.properties(include_superclass=true)
+ @properties ||= {}
+ if include_superclass
+ if superclass.respond_to?(:properties)
+ superclass.properties.merge(@properties)
else
- @name = name.to_s
+ @properties.dup
end
+ else
+ @properties
end
- @name
end
#
@@ -475,13 +465,21 @@ class Chef
#
# Get the value of the state attributes in this resource as a hash.
#
+ # Does not include properties that are not set (unless they are identity
+ # properties).
+ #
# @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 = {}
+ state_properties = self.class.state_properties
+ state_properties.each do |property|
+ if property.identity? || property.is_set?(self)
+ state[property.name] = send(property.name)
+ end
end
+ state
end
#
@@ -494,17 +492,22 @@ 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.
+ #
+ # @return [Object,Hash<Symbol,Object>] The identity of this resource.
#
def identity
- if identity_attr = self.class.identity_attr
- send(identity_attr)
- else
- name
+ result = {}
+ identity_properties = self.class.identity_properties
+ identity_properties.each do |property|
+ result[property.name] = send(property.name)
end
+ return result.values.first if identity_properties.size == 1
+ result
end
#
@@ -756,6 +759,10 @@ class Chef
# 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`.
#
# @example Bare property
# property :x
@@ -772,29 +779,100 @@ class Chef
def self.property(name, type=NOT_PASSED, **options)
name = name.to_sym
- if type != NOT_PASSED
+ options.merge!(name: name, declared_in: self)
+
+ if type == NOT_PASSED
+ # If a type is not passed, the property derives from the
+ # superclass property (if any)
+ if properties.has_key?(name)
+ property = properties[name].derive(**options)
+ else
+ property = property_type(**options)
+ end
+
+ # If a Property is specified, derive a new one from that.
+ elsif type.is_a?(Property) || (type.is_a?(Class) && type <= Property)
+ property = type.derive(**options)
+
+ # If a primitive type was passed, combine it with "is"
+ else
if options[:is]
options[:is] = ([ type ] + [ options[:is] ]).flatten(1)
else
options[:is] = type
end
+ property = property_type(**options)
end
- define_method(name) do |value=NOT_PASSED|
- set_or_return(name, value, options)
- end
- define_method("#{name}=") do |value|
- set_or_return(name, value, options)
- end
+ local_properties = properties(false)
+ local_properties[name] = property
+
+ property.emit_dsl
end
#
+ # Create a reusable property type that can be used in multiple properties
+ # in different resources.
+ #
+ # @param options [Hash<Symbol,Object>] Validation options. see #property for
+ # the list of options.
+ #
+ # @example
+ # property_type(default: 'hi')
+ #
+ def self.property_type(**options)
+ Property.derive(**options)
+ end
+
+ #
+ # The name of this particular resource.
+ #
+ # This special resource attribute is set automatically from the declaration
+ # of the resource, e.g.
+ #
+ # execute 'Vitruvius' do
+ # command 'ls'
+ # end
+ #
+ # Will set the name to "Vitruvius".
+ #
+ # This is also used in to_s to show the resource name, e.g. `execute[Vitruvius]`.
+ #
+ # This is also used for resource notifications and subscribes in the same manner.
+ #
+ # This will coerce any object into a string via #to_s. Arrays are a special case
+ # so that `package ["foo", "bar"]` becomes package[foo, bar] instead of the more
+ # awkward `package[["foo", "bar"]]` that #to_s would produce.
+ #
+ # @param name [Object] The name to set, typically a String or Array
+ # @return [String] The name of this Resource.
+ #
+ property :name, String, coerce: proc { |v| v.is_a?(Array) ? v.join(', ') : v.to_s }, desired_state: false
+
+ #
# Whether this property has been set (or whether it has a default that has
# been retrieved).
#
+ # @param name [Symbol] The name of the property.
+ # @return [Boolean] `true` if the property has been set.
+ #
def property_is_set?(name)
- name = name.to_sym
- instance_variable_defined?("@#{name}")
+ property = self.class.properties[name.to_sym]
+ raise ArgumentError, "Property #{name} is not defined in class #{self}" if !property
+ property.is_set?(self)
+ end
+
+ #
+ # Clear this property as if it had never been set. It will thereafter return
+ # the default.
+ # been retrieved).
+ #
+ # @param name [Symbol] The name of the property.
+ #
+ def reset_property(name)
+ property = self.class.properties[name.to_sym]
+ raise ArgumentError, "Property #{name} is not defined in class #{self}" if !property
+ property.reset(self)
end
#
@@ -808,46 +886,186 @@ class Chef
DelayedEvaluator.new(&block)
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
- # attributes are attributes that could be populated by examining the state
- # of the system (e.g., File.stat can tell you the permissions on an
- # existing file). Contrarily, attributes that are not "state attributes"
- # usually modify the way Chef itself behaves, for example by providing
- # additional options for a package manager to use when installing a
- # package.
+ #
+ # Get or set the list of desired state properties for this resource.
+ #
+ # State properties are properties that describe the desired state
+ # of the system, such as file permissions or ownership.
+ # In general, state properties are properties that could be populated by
+ # examining the state of the system (e.g., File.stat can tell you the
+ # permissions on an existing file). Contrarily, properties that are not
+ # "state properties" usually modify the way Chef itself behaves, for example
+ # by providing additional options for a package manager to use when
+ # installing a package.
#
# This list is used by the Chef client auditing system to extract
# information from resources to describe changes made to the system.
- def self.state_attrs(*attr_names)
- @state_attrs ||= []
- @state_attrs = attr_names unless attr_names.empty?
+ #
+ # This method is unnecessary when declaring properties with `property`;
+ # properties are added to state_properties by default, and can be turned off
+ # with `desired_state: false`.
+ #
+ # ```ruby
+ # property :x # part of desired state
+ # property :y, desired_state: false # not part of desired state
+ # ```
+ #
+ # @param names [Array<Symbol>] A list of property names to set as desired
+ # state.
+ #
+ # @return [Array<Property>] All properties in desired state.
+ #
+ def self.state_properties(*names)
+ if !names.empty?
+ names = names.map { |name| name.to_sym }.uniq
- # 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
+ local_properties = properties(false)
+ # Add new properties to the list.
+ names.each do |name|
+ property = properties[name]
+ if !property
+ self.property name, instance_variable_name: false, desired_state: true
+ elsif !property.desired_state?
+ self.property name, desired_state: true
+ end
+ end
+
+ # If state_attrs *excludes* something which is currently desired state,
+ # mark it as desired_state: false.
+ local_properties.each do |name,property|
+ if property.desired_state? && !names.include?(name)
+ self.property name, desired_state: false
+ end
+ end
end
+
+ properties.values.select { |property| property.desired_state? }
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
+ #
+ # Set or return the list of "state properties" implemented by the Resource
+ # subclass.
+ #
+ # Equivalent to calling #state_properties and getting `state_properties.keys`.
+ #
+ # @deprecated Use state_properties.keys instead. Note that when you declare
+ # properties with `property`: properties are added to state_properties by
+ # default, and can be turned off with `desired_state: false`
+ #
+ # ```ruby
+ # property :x # part of desired state
+ # property :y, desired_state: false # not part of desired state
+ # ```
+ #
+ # @param names [Array<Symbol>] A list of property names to set as desired
+ # state.
+ #
+ # @return [Array<Symbol>] All property names with desired state.
+ #
+ def self.state_attrs(*names)
+ state_properties(*names).map { |property| property.name }
+ 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 the identity of this resource to a particular set of properties.
+ #
+ # This drives #identity, which returns data that uniquely refers to a given
+ # resource on the given node (in such a way that it can be correlated
+ # across Chef runs).
+ #
+ # This method is unnecessary when declaring properties with `property`;
+ # properties can be added to identity during declaration with
+ # `identity: true`.
+ #
+ # ```ruby
+ # property :x, identity: true # part of identity
+ # property :y # not part of identity
+ # ```
+ #
+ # If no properties are marked as identity, "name" is considered the identity.
+ #
+ # @param names [Array<Symbol>] A list of property names to set as the identity.
+ #
+ # @return [Array<Property>] All identity properties.
+ #
+ def self.identity_properties(*names)
+ if !names.empty?
+ names = names.map { |name| name.to_sym }
+
+ # Add or change properties that are not part of the identity.
+ names.each do |name|
+ property = properties[name]
+ if !property
+ self.property name, instance_variable_name: false, identity: true
+ elsif !property.identity?
+ self.property name, identity: true
+ end
+ end
+
+ # If state_attrs *excludes* something which is currently part of the
+ # identity, mark it as identity: false.
+ properties.each do |name,property|
+ if property.identity? && !names.include?(name)
+ self.property name, identity: false
+ end
+ end
+ end
+
+ result = properties.values.select { |property| property.identity? }
+ result = [ properties[:name] ] if result.empty?
+ result
+ end
+
+ #
+ # Set the identity of this resource to a particular property.
+ #
+ # This drives #identity, which returns data that uniquely refers to a given
+ # resource on the given node (in such a way that it can be correlated
+ # across Chef runs).
+ #
+ # This method is unnecessary when declaring properties with `property`;
+ # properties can be added to identity during declaration with
+ # `identity: true`.
+ #
+ # ```ruby
+ # property :x, identity: true # part of identity
+ # property :y # not part of identity
+ # ```
+ #
+ # @param name [Symbol] A list of property names to set as the identity.
+ #
+ # @return [Symbol] The identity property if there is only one; or `nil` if
+ # there are more than one.
+ #
+ # @raise [ArgumentError] If no arguments are passed and the resource has
+ # more than one identity property.
+ #
+ def self.identity_property(name=nil)
+ result = identity_properties(*Array(name))
+ if result.size > 1
+ raise Chef::Exceptions::MultipleIdentityError, "identity_property cannot be called on an object with more than one identity property (#{result.map { |r| r.name }.join(", ")})."
end
+ result.first
+ end
+
+ #
+ # Set a property as the "identity attribute" for this resource.
+ #
+ # Identical to calling #identity_property.first.key.
+ #
+ # @param name [Symbol] The name of the property to set.
+ #
+ # @return [Symbol]
+ #
+ # @deprecated `identity_property` should be used instead.
+ #
+ # @raise [ArgumentError] If no arguments are passed and the resource has
+ # more than one identity property.
+ #
+ def self.identity_attr(name=nil)
+ property = identity_property(name)
+ return nil if !property
+ property.name
end
#
@@ -1199,9 +1417,6 @@ class Chef
define_method(:load_current_resource) {}
end
end
- def self.default_action=(action_name)
- default_action(action_name)
- end
#
# Internal Resource Interface (for Chef)
diff --git a/spec/unit/mixin/params_validate_spec.rb b/spec/unit/mixin/params_validate_spec.rb
index 85e1c1abab..3724bbf583 100644
--- a/spec/unit/mixin/params_validate_spec.rb
+++ b/spec/unit/mixin/params_validate_spec.rb
@@ -21,6 +21,8 @@ require 'spec_helper'
class TinyClass
include Chef::Mixin::ParamsValidate
+ attr_reader :name
+
def music(is_good=true)
is_good
end
@@ -331,11 +333,11 @@ describe Chef::Mixin::ParamsValidate do
it "asserts that a value returns false from a predicate method" do
expect do
@vo.validate({:not_blank => "should pass"},
- {:not_blank => {:cannot_be => :nil, :cannot_be => :empty}})
+ {:not_blank => {:cannot_be => [ :nil, :empty ]}})
end.not_to raise_error
expect do
@vo.validate({:not_blank => ""},
- {:not_blank => {:cannot_be => :nil, :cannot_be => :empty}})
+ {:not_blank => {:cannot_be => [ :nil, :empty ]}})
end.to raise_error(Chef::Exceptions::ValidationFailed)
end
diff --git a/spec/unit/property/state_spec.rb b/spec/unit/property/state_spec.rb
index 80ebe01a41..bf2de178c9 100644
--- a/spec/unit/property/state_spec.rb
+++ b/spec/unit/property/state_spec.rb
@@ -50,20 +50,20 @@ describe "Chef::Resource#identity and #state" do
end
# identity
- context "Chef::Resource#identity_attr" do
+ context "Chef::Resource#identity_properties" 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 "name is the default identity" do
+ expect(resource_class.identity_properties).to eq [ Chef::Resource.properties[:name] ]
+ expect(Chef::Resource.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
+ it "identity_properties :x changes the identity" do
+ expect(resource_class.identity_properties :x).to eq [ resource_class.properties[:x] ]
+ expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ]
+ expect(Chef::Resource.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'
@@ -72,28 +72,31 @@ describe "Chef::Resource#identity and #state" do
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
+ with_property ":y, identity: true" do
+ context "and identity_properties :x" do
+ before do
+ resource_class.class_eval do
+ identity_properties :x
+ end
+ end
+
+ it "only returns :x as identity" do
+ resource.x 'foo'
+ resource.y 'bar'
+ expect(resource_class.identity_properties).to eq [ resource_class.properties[: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_properties).to eq [
+ resource_class.properties[:x],
+ resource_class.properties[:y]
+ ]
+ expect(resource.state_for_resource_reporter).to eq(x: 'foo', y: 'bar')
+ end
+ end
+ end
context "With a subclass" do
let(:subresource_class) do
@@ -107,57 +110,63 @@ describe "Chef::Resource#identity and #state" do
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_class.identity_properties).to eq [ Chef::Resource.properties[:name] ]
+ expect(Chef::Resource.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
+ context "With identity_properties :x on the superclass" do
before do
resource_class.class_eval do
- identity_attr :x
+ identity_properties :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
+ expect(subresource_class.identity_properties).to eq [ subresource_class.properties[:x] ]
+ expect(Chef::Resource.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
+ 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_properties).to eq [
+ subresource_class.properties[:x],
+ subresource_class.properties[:y]
+ ]
+ 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
+ context "With identity_properties :y on the subclass" do
before do
subresource_class.class_eval do
- identity_attr :y
+ identity_properties :y
end
end
- # it "y is part of state" do
- # subresource.x 'foo'
- # subresource.y 'bar'
- # expect(subresource.state).to eq({ x: 'foo', y: 'bar' })
- # expect(subresource_class.state_attrs).to eq [ :x, :y ]
- # end
+ it "y is part of state" do
+ subresource.x 'foo'
+ subresource.y 'bar'
+ expect(subresource.state_for_resource_reporter).to eq(x: 'foo', y: 'bar')
+ expect(subresource_class.state_properties).to eq [
+ subresource_class.properties[:x],
+ subresource_class.properties[:y]
+ ]
+ end
it "y is the identity" do
- expect(subresource_class.identity_attr).to eq :y
+ expect(subresource_class.identity_properties).to eq [ subresource_class.properties[:y] ]
subresource.x 'foo'
subresource.y 'bar'
expect(subresource.identity).to eq 'bar'
@@ -171,24 +180,24 @@ describe "Chef::Resource#identity and #state" do
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
+ with_property ":string_only, String, identity: true", ":string_only2, String" do
+ it "identity_properties does not change validation" do
+ resource_class.identity_properties :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_properties does not change desired_state" do
+ resource_class.identity_properties :x
+ resource.x 'hi'
+ expect(resource.identity).to eq 'hi'
+ expect(resource_class.properties[:x].desired_state?).to be_falsey
+ expect(resource_class.state_properties).to eq []
+ expect(resource.state_for_resource_reporter).to eq({})
+ end
+ end
context "With custom property custom_property defined only as methods, using different variables for storage" do
before do
@@ -202,24 +211,26 @@ describe "Chef::Resource#identity and #state" do
end
end
- context "And identity_attr :custom_property" do
+ context "And identity_properties :custom_property" do
before do
resource_class.class_eval do
- identity_attr :custom_property
+ identity_properties :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
+ it "identity_properties comes back as :custom_property" do
+ expect(resource_class.properties[:custom_property].identity?).to be_truthy
+ expect(resource_class.identity_properties).to eq [ resource_class.properties[:custom_property] ]
+ end
+ it "custom_property becomes part of desired_state" do
+ resource.custom_property = 1
+ expect(resource.state_for_resource_reporter).to eq(custom_property: 6)
+ expect(resource_class.properties[:custom_property].desired_state?).to be_truthy
+ expect(resource_class.state_properties).to eq [
+ resource_class.properties[:custom_property]
+ ]
end
- # it "custom_property becomes part of desired_state" do
- # resource.custom_property = 1
- # expect(resource.state).to eq({ custom_property: 6 })
- # 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
+ it "identity_properties does not change custom_property's getter or setter" do
resource.custom_property = 1
expect(resource.custom_property).to eq 6
end
@@ -232,111 +243,111 @@ describe "Chef::Resource#identity and #state" do
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({})
+ context "Property#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_properties).to eq [ resource_class.properties[: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_property raises an error if multiple identity values are defined" do
+ expect { resource_class.identity_property }.to raise_error Chef::Exceptions::MultipleIdentityError
+ end
+ it "identity_attr raises an error if multiple identity values are defined" do
+ expect { resource_class.identity_attr }.to raise_error Chef::Exceptions::MultipleIdentityError
+ 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 all values whether any value is set or not" do
+ expect(resource.identity).to eq(x: 'xxx', y: 'yyy', z: 'zzz')
+ end
+ it "identity_properties wipes out any other identity attributes if multiple are defined" do
+ resource_class.identity_properties :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_for_resource_reporter).to eq(x: 'blah')
+ end
end
+ 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
+ # state_properties
+ context "Chef::Resource#state_properties" do
+ it "state_properties is empty by default" do
+ expect(Chef::Resource.state_properties).to eq []
+ expect(resource.state_for_resource_reporter).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_properties).to eq [
+ resource_class.properties[:x],
+ resource_class.properties[:y],
+ resource_class.properties[:z]
+ ]
+ expect(resource.state_for_resource_reporter).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_for_resource_reporter).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_properties).to eq [
+ resource_class.properties[:x],
+ resource_class.properties[:z]
+ ]
+ expect(resource.state_for_resource_reporter).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_for_resource_reporter).to eq({ x: 'blah' })
+ # end
+ it "Set values with name_property are included in state" do
+ resource.x 1
+ expect(resource.state_for_resource_reporter).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_for_resource_reporter).to eq({})
+ end
+ it "Set values with defaults are included in state" do
+ resource.x 1
+ expect(resource.state_for_resource_reporter).to eq(x: 1)
+ end
+ end
context "With a class with a normal getter and setter" do
before do
@@ -349,143 +360,132 @@ describe "Chef::Resource#identity and #state" do
end
end
end
- it "state_attrs(:x) causes the value to be included in properties" do
- resource_class.state_attrs(:x)
+ it "state_properties(:x) causes the value to be included in properties" do
+ resource_class.state_properties(:x)
resource.x = 1
expect(resource.x).to eq 6
- expect(resource.state).to eq(x: 6)
+ expect(resource.state_for_resource_reporter).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
+ with_property ":x, Integer, identity: true" do
+ it "state_properties(:x) leaves the property in desired_state" do
+ resource_class.state_properties(:x)
+ resource.x 10
+
+ expect(resource_class.properties[:x].desired_state?).to be_truthy
+ expect(resource_class.state_properties).to eq [
+ resource_class.properties[:x]
+ ]
+ expect(resource.state_for_resource_reporter).to eq(x: 10)
+ end
+ it "state_properties(:x) does not turn off validation" do
+ resource_class.state_properties(:x)
+ expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed
+ end
+ it "state_properties(:x) does not turn off identity" do
+ resource_class.state_properties(:x)
+ resource.x 10
+
+ expect(resource_class.identity_properties).to eq [ resource_class.properties[: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_properties(:x) leaves x identical" do
+ old_value = resource_class.properties[:y]
+ resource_class.state_properties(:x)
+ resource.x 10
+
+ expect(resource_class.properties[:y].object_id).to eq old_value.object_id
+
+ expect(resource_class.properties[:x].desired_state?).to be_truthy
+ expect(resource_class.properties[:x].identity?).to be_truthy
+ expect(resource_class.identity_properties).to eq [
+ resource_class.properties[:x]
+ ]
+ expect(resource.identity).to eq(10)
+ expect(resource_class.state_properties).to eq [
+ resource_class.properties[:x]
+ ]
+ expect(resource.state_for_resource_reporter).to eq(x: 10)
+ end
+
+ it "state_properties(:y) adds y to desired state" do
+ old_value = resource_class.properties[:x]
+ resource_class.state_properties(:y)
+ resource.x 10
+
+ expect(resource_class.properties[:x].object_id).to eq old_value.object_id
+ expect(resource_class.properties[:x].desired_state?).to be_falsey
+ expect(resource_class.properties[:y].desired_state?).to be_truthy
+ expect(resource_class.state_properties).to eq [
+ resource_class.properties[:y]
+ ]
+ expect(resource.state_for_resource_reporter).to eq(y: 20)
+ end
+
+ context "With a subclassed resource" 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 "state_properties(:x) adds x to desired state" do
+ old_value = resource_class.properties[:y]
+ subresource_class.state_properties(:x)
+ subresource.x 10
+
+ expect(subresource_class.properties[:y].object_id).to eq old_value.object_id
+
+ expect(subresource_class.properties[:x].desired_state?).to be_truthy
+ expect(subresource_class.properties[:x].identity?).to be_truthy
+ expect(subresource_class.identity_properties).to eq [
+ subresource_class.properties[:x]
+ ]
+ expect(subresource.identity).to eq(10)
+ expect(subresource_class.state_properties).to eq [
+ subresource_class.properties[:x]
+ ]
+ expect(subresource.state_for_resource_reporter).to eq(x: 10)
+ end
+
+ it "state_properties(:y) adds y to desired state" do
+ old_value = resource_class.properties[:x]
+ subresource_class.state_properties(:y)
+ subresource.x 10
+
+ expect(subresource_class.properties[:x].object_id).to eq old_value.object_id
+ expect(subresource_class.properties[:y].desired_state?).to be_truthy
+ expect(subresource_class.state_properties).to eq [
+ subresource_class.properties[:y]
+ ]
+ expect(subresource.state_for_resource_reporter).to eq(y: 20)
+
+ expect(subresource_class.properties[:x].identity?).to be_truthy
+ expect(subresource_class.identity_properties).to eq [
+ subresource_class.properties[:x]
+ ]
+ expect(subresource.identity).to eq(10)
+ end
+ end
+ end
end
end
diff --git a/spec/unit/property/validation_spec.rb b/spec/unit/property/validation_spec.rb
index 7ce6a45167..a3649d228c 100644
--- a/spec/unit/property/validation_spec.rb
+++ b/spec/unit/property/validation_spec.rb
@@ -111,10 +111,6 @@ describe "Chef::Resource.property validation" do
it "get succeeds" do
expect(resource.x).to eq 'default'
end
- it "set(nil) = get" do
- expect(resource.x nil).to eq 'default'
- expect(resource.x).to eq 'default'
- end
it "set to valid value succeeds" do
expect(resource.x 'str').to eq 'str'
expect(resource.x).to eq 'str'
@@ -122,15 +118,18 @@ describe "Chef::Resource.property validation" do
it "set to invalid value raises ValidationFailed" do
expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed
end
+ it "set to nil emits a deprecation warning and does a get" do
+ expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError
+ Chef::Config[:treat_deprecation_warnings_as_errors] = false
+ resource.x 'str'
+ expect(resource.x nil).to eq 'str'
+ expect(resource.x).to eq 'str'
+ end
end
context "when the variable does not have an initial value" do
it "get succeeds" do
expect(resource.x).to be_nil
end
- it "set(nil) = get" do
- expect(resource.x nil).to be_nil
- expect(resource.x).to be_nil
- end
it "set to valid value succeeds" do
expect(resource.x 'str').to eq 'str'
expect(resource.x).to eq 'str'
@@ -138,6 +137,13 @@ describe "Chef::Resource.property validation" do
it "set to invalid value raises ValidationFailed" do
expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed
end
+ it "set to nil emits a deprecation warning and does a get" do
+ expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError
+ Chef::Config[:treat_deprecation_warnings_as_errors] = false
+ resource.x 'str'
+ expect(resource.x nil).to eq 'str'
+ expect(resource.x).to eq 'str'
+ end
end
end
with_property ":x, [ String, nil ]" do
@@ -255,10 +261,10 @@ describe "Chef::Resource.property validation" do
[ '', 'abac' ],
[ nil ]
- # PropertyType
- # validation_test 'is: PropertyType.new(is: :a)',
- # [ :a ],
- # [ :b, nil ]
+ # Property
+ validation_test 'is: Chef::Property.new(is: :a)',
+ [ :a ],
+ [ :b, nil ]
# RSpec Matcher
class Globalses
@@ -523,10 +529,11 @@ describe "Chef::Resource.property validation" do
expect(resource.x 1).to eq 1
expect(resource.x).to eq 1
end
- it "value nil does a get" do
+ it "value nil emits a deprecation warning and does a get" do
+ expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError
Chef::Config[:treat_deprecation_warnings_as_errors] = false
resource.x 1
- resource.x nil
+ expect(resource.x nil).to eq 1
expect(resource.x).to eq 1
end
end
@@ -549,16 +556,18 @@ describe "Chef::Resource.property validation" do
end
with_property ':x, name_property: true, required: true' do
- it "if x is not specified, retrieval succeeds" do
+ it "if x is not specified, the name property is returned" do
expect(resource.x).to eq 'blah'
end
it "value 1 is valid" do
expect(resource.x 1).to eq 1
expect(resource.x).to eq 1
end
- it "value nil does a get" do
+ it "value nil emits a deprecation warning and does a get" do
+ expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError
+ Chef::Config[:treat_deprecation_warnings_as_errors] = false
resource.x 1
- resource.x nil
+ expect(resource.x nil).to eq 1
expect(resource.x).to eq 1
end
end
@@ -571,9 +580,11 @@ describe "Chef::Resource.property validation" do
expect(resource.x 1).to eq 1
expect(resource.x).to eq 1
end
- it "value nil does a get" do
+ it "value nil is invalid" do
+ expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError
+ Chef::Config[:treat_deprecation_warnings_as_errors] = false
resource.x 1
- resource.x nil
+ expect(resource.x nil).to eq 1
expect(resource.x).to eq 1
end
end
@@ -596,11 +607,6 @@ describe "Chef::Resource.property validation" do
end
end
- # it "getting the value causes a deprecation warning" do
- # Chef::Config[:treat_deprecation_warnings_as_errors] = true
- # expect { resource.x }.to raise_error Chef::Exceptions::DeprecatedFeatureError
- # end
-
it "value 1 is valid" do
expect(resource.x 1).to eq 1
expect(resource.x).to eq 1
diff --git a/spec/unit/property_spec.rb b/spec/unit/property_spec.rb
index ce0552c564..09f7e52329 100644
--- a/spec/unit/property_spec.rb
+++ b/spec/unit/property_spec.rb
@@ -58,11 +58,15 @@ describe "Chef::Resource.property" do
else
tags = []
end
- properties = properties.map { |property| "property #{property}" }
- context "With properties #{english_join(properties)}", *tags do
+ if properties.size == 1
+ description = "With property #{properties.first}"
+ else
+ description = "With properties #{english_join(properties.map { |property| "#{property.inspect}" })}"
+ end
+ context description, *tags do
before do
properties.each do |property_str|
- resource_class.class_eval(property_str, __FILE__, __LINE__)
+ resource_class.class_eval("property #{property_str}", __FILE__, __LINE__)
end
end
instance_eval(&block)
@@ -75,11 +79,10 @@ describe "Chef::Resource.property" 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
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 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
@@ -92,11 +95,11 @@ describe "Chef::Resource.property" 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 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
@@ -121,7 +124,7 @@ describe "Chef::Resource.property" do
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 be_nil
end
it "x's validation is inherited" do
@@ -140,18 +143,18 @@ describe "Chef::Resource.property" do
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 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
+ 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
+ expect(resource_class.properties[:y]).to be_nil
end
end
@@ -167,17 +170,37 @@ describe "Chef::Resource.property" do
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]
+ 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'
+ it "x's validation is inherited" do
+ expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed
end
+ end
- it "the superclass's validation for x is still there" do
- expect { resource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed
+ context "with property :x, default: 80 on the subclass" do
+ before do
+ subresource_class.class_eval do
+ property :x, default: 80
+ 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 defaults to 80" do
+ expect(subresource.x).to eq 80
+ end
+
+ it "x's validation is inherited" do
+ expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed
end
end
@@ -193,8 +216,8 @@ describe "Chef::Resource.property" do
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]
+ 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
@@ -212,14 +235,89 @@ describe "Chef::Resource.property" do
end
end
- context "Chef::Resource::PropertyType#property_is_set?" do
+ context "Chef::Resource::Property#reset_property" do
+ it "when a resource is newly created, reset_property(:name) sets property to nil" do
+ expect(resource.property_is_set?(:name)).to be_truthy
+ resource.reset_property(:name)
+ expect(resource.property_is_set?(:name)).to be_falsey
+ expect(resource.name).to be_nil
+ end
+
+ it "when referencing an undefined property, reset_property(:x) raises an error" do
+ expect { resource.reset_property(:x) }.to raise_error(ArgumentError)
+ end
+
+ with_property ':x' do
+ it "when the resource is newly created, reset_property(:x) does nothing" do
+ expect(resource.property_is_set?(:x)).to be_falsey
+ resource.reset_property(:x)
+ expect(resource.property_is_set?(:x)).to be_falsey
+ expect(resource.x).to be_nil
+ end
+ it "when x is set, reset_property resets it" do
+ resource.x 10
+ expect(resource.property_is_set?(:x)).to be_truthy
+ resource.reset_property(:x)
+ expect(resource.property_is_set?(:x)).to be_falsey
+ expect(resource.x).to be_nil
+ end
+ end
+
+ with_property ':x, Integer' do
+ it "when the resource is newly created, reset_property(:x) does nothing" do
+ expect(resource.property_is_set?(:x)).to be_falsey
+ resource.reset_property(:x)
+ expect(resource.property_is_set?(:x)).to be_falsey
+ expect(resource.x).to be_nil
+ end
+ it "when x is set, reset_property resets it even though `nil` is technically invalid" do
+ resource.x 10
+ expect(resource.property_is_set?(:x)).to be_truthy
+ resource.reset_property(:x)
+ expect(resource.property_is_set?(:x)).to be_falsey
+ expect(resource.x).to be_nil
+ end
+ end
+
+ with_property ':x, default: 10' do
+ it "when the resource is newly created, reset_property(:x) does nothing" do
+ expect(resource.property_is_set?(:x)).to be_falsey
+ resource.reset_property(:x)
+ expect(resource.property_is_set?(:x)).to be_falsey
+ expect(resource.x).to eq 10
+ end
+ it "when x is set, reset_property resets it and it returns the default" do
+ resource.x 20
+ resource.reset_property(:x)
+ expect(resource.property_is_set?(:x)).to be_falsey
+ expect(resource.x).to eq 10
+ end
+ end
+
+ with_property ':x, default: lazy { 10 }' do
+ it "when the resource is newly created, reset_property(:x) does nothing" do
+ expect(resource.property_is_set?(:x)).to be_falsey
+ resource.reset_property(:x)
+ expect(resource.property_is_set?(:x)).to be_falsey
+ expect(resource.x).to eq 10
+ end
+ it "when x is set, reset_property resets it and it returns the default" do
+ resource.x 20
+ resource.reset_property(:x)
+ expect(resource.property_is_set?(:x)).to be_falsey
+ expect(resource.x).to eq 10
+ end
+ end
+ end
+
+ context "Chef::Resource::Property#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
+ 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
@@ -259,9 +357,9 @@ describe "Chef::Resource.property" 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
+ it "when x is retrieved, property_is_set?(:x) is false" do
resource.x
- expect(resource.property_is_set?(:x)).to be_truthy
+ expect(resource.property_is_set?(:x)).to be_falsey
end
end
@@ -281,9 +379,9 @@ describe "Chef::Resource.property" 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
+ it "when x is retrieved, property_is_set?(:x) is false" do
resource.x
- expect(resource.property_is_set?(:x)).to be_truthy
+ expect(resource.property_is_set?(:x)).to be_falsey
end
end
@@ -299,14 +397,14 @@ describe "Chef::Resource.property" 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
+ it "when x is retrieved, property_is_set?(:x) is false" do
resource.x
- expect(resource.property_is_set?(:x)).to be_truthy
+ expect(resource.property_is_set?(:x)).to be_falsey
end
end
end
- context "Chef::Resource::PropertyType#default" do
+ context "Chef::Resource::Property#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
@@ -317,7 +415,7 @@ describe "Chef::Resource.property" 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({})
+ expect(resource.state_for_resource_reporter).to eq({})
end
it "when x is set to nil, it returns nil" do
resource.instance_eval { @x = nil }
@@ -339,8 +437,15 @@ describe "Chef::Resource.property" do
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({})
+ it "when x is not set, it is included in identity" do
+ expect(resource.identity).to eq(10)
+ end
+ end
+
+ with_property ':x, default: 1, identity: true', ':y, default: 2, identity: true' do
+ it "when x is not set, it is still included in identity" do
+ resource.y 20
+ expect(resource.identity).to eq(x: 1, y: 20)
end
end
@@ -462,21 +567,35 @@ describe "Chef::Resource.property" do
# end
end
- with_property ":x, default: lazy { Namer.next_index }, is: proc { |v| Namer.next_index; true }" do
+ with_property ":x, default: lazy { Namer.next_index.to_s }, is: proc { |v| Namer.next_index; true }" do
it "validation is not run at all on the default value" do
- expect(resource.x).to eq 1
+ expect(resource.x).to eq '1'
+ expect(Namer.current_index).to eq 1
+ end
+ # it "validation is run each time" 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
+
+ with_property ":x, default: lazy { Namer.next_index.to_s.freeze }, is: proc { |v| Namer.next_index; true }" do
+ it "validation is not run at all on the default value" do
+ expect(resource.x).to eq '1'
expect(Namer.current_index).to eq 1
end
# it "validation is only run the first time" do
- # expect(resource.x).to eq 1
+ # expect(resource.x).to eq '1'
# expect(Namer.current_index).to eq 2
- # expect(resource.x).to eq 1
+ # expect(resource.x).to eq '1'
# expect(Namer.current_index).to eq 2
# end
end
end
context "coercion of defaults" do
+ # Frozen default, non-frozen coerce
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
@@ -487,13 +606,32 @@ describe "Chef::Resource.property" do
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
+ it "when x is retrieved, coercion is run exactly once" do
expect(resource.x).to eq '101'
expect(resource.x).to eq '101'
expect(Namer.current_index).to eq 1
end
end
+ # Frozen default, frozen coerce
+ with_property ':x, coerce: proc { |v| "#{v}#{next_index}".freeze }, 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 each time" do
+ expect(resource.x).to eq '101'
+ expect(resource.x).to eq '102'
+ expect(Namer.current_index).to eq 2
+ end
+ end
+
+ # Frozen lazy default, non-frozen coerce
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
@@ -504,6 +642,29 @@ describe "Chef::Resource.property" do
expect(resource.x).to eq 'hi1'
expect(Namer.current_index).to eq 1
end
+ it "when x is retrieved, coercion is run exactly once" do
+ expect(resource.x).to eq '101'
+ expect(resource.x).to eq '101'
+ expect(Namer.current_index).to eq 1
+ end
+ end
+
+ # Non-frozen lazy default, frozen coerce
+ with_property ':x, coerce: proc { |v| "#{v}#{next_index}".freeze }, 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
+ it "when x is retrieved, coercion is run each time" do
+ expect(resource.x).to eq '101'
+ expect(resource.x).to eq '102'
+ expect(Namer.current_index).to eq 2
+ end
end
with_property ':x, proc { |v| Namer.next_index; true }, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do
@@ -706,7 +867,7 @@ describe "Chef::Resource.property" do
end
end
- context "Chef::Resource::PropertyType#coerce" do
+ context "Chef::Resource::Property#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"
@@ -740,7 +901,7 @@ describe "Chef::Resource.property" do
end
end
- context "Chef::Resource::PropertyType validation" do
+ context "Chef::Resource::Property 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
@@ -768,7 +929,7 @@ describe "Chef::Resource.property" do
end
[ 'name_attribute', 'name_property' ].each do |name|
- context "Chef::Resource::PropertyType##{name}" do
+ context "Chef::Resource::Property##{name}" do
with_property ":x, #{name}: true" do
it "defaults x to resource.name" do
expect(resource.x).to eq 'blah'
@@ -797,6 +958,11 @@ describe "Chef::Resource.property" do
expect(resource.x).to eq 10
end
end
+ with_property ":x, #{name}: true, required: true" do
+ it "defaults x to resource.name" do
+ expect(resource.x).to eq 'blah'
+ end
+ end
end
end
end
diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb
index 353c5ec129..1377950c99 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 "has :name as identity attribute by default" do
+ expect(Chef::Resource.identity_attr).to eq(:name)
end
it "sets an identity attribute" do