summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Keiser <john@johnkeiser.com>2015-06-08 12:05:55 -0700
committerJohn Keiser <john@johnkeiser.com>2015-06-18 15:26:09 -0700
commit4375de0ecb57b24937caa5054a30bc1d4f54a4a4 (patch)
tree8892ea1966795fa869258c8f481873b878d3f33f
parentd4d4a7b5f5888dd01065e8b040a211ca6e8ace7e (diff)
downloadchef-4375de0ecb57b24937caa5054a30bc1d4f54a4a4.tar.gz
Add Resource.properties and PropertyType
-rw-r--r--lib/chef/constants.rb20
-rw-r--r--lib/chef/delayed_evaluator.rb21
-rw-r--r--lib/chef/mixin/params_validate.rb73
-rw-r--r--lib/chef/property_type.rb36
-rw-r--r--lib/chef/resource.rb49
-rw-r--r--spec/unit/property_spec.rb26
6 files changed, 192 insertions, 33 deletions
diff --git a/lib/chef/constants.rb b/lib/chef/constants.rb
new file mode 100644
index 0000000000..3112982c6c
--- /dev/null
+++ b/lib/chef/constants.rb
@@ -0,0 +1,20 @@
+#
+# 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
+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/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb
index 8b5df37186..5f1cda3311 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_type'
+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?(PropertyType)
+
#--
# 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
@@ -86,6 +121,7 @@ class Chef
def set_or_return(symbol, value, validation)
symbol = symbol.to_sym
iv_symbol = :"@#{symbol}"
+ validation = validation.validation_options if validation.is_a?(PropertyType)
# Steal default, coerce, name_property and required from validation
# so that we can handle the order in which they are applied
@@ -198,8 +234,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
#
diff --git a/lib/chef/property_type.rb b/lib/chef/property_type.rb
new file mode 100644
index 0000000000..decc909ca6
--- /dev/null
+++ b/lib/chef/property_type.rb
@@ -0,0 +1,36 @@
+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 PropertyType
+ #
+ # Create a new property type.
+ #
+ # @param validation_options [Hash<Symbol,Object>] Validation options. (See Chef::Mixin::ParamsValidate#validate)
+ #
+ def initialize(**validation_options)
+ @validation_options = validation_options
+ end
+
+ #
+ # Validation options. (See Chef::Mixin::ParamsValidate#validate.)
+ #
+ # @return [Hash<Symbol,Object>]
+ #
+ attr_reader :validation_options
+ end
+end
diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb
index 81506c5b53..eb0a79511b 100644
--- a/lib/chef/resource.rb
+++ b/lib/chef/resource.rb
@@ -778,11 +778,49 @@ class Chef
end
end
- define_method(name) do |value=NOT_PASSED|
- set_or_return(name, value, options)
+ properties(false)[name] = PropertyType.new(options)
+
+ begin
+ class_eval <<-EOM, __FILE__, __LINE__+1
+ def #{name}(value=NOT_PASSED)
+ set_or_return(#{name.inspect}, value, self.class.properties[#{name.inspect}])
+ end
+ def #{name}=(value)
+ set_or_return(#{name.inspect}, value, self.class.properties[#{name.inspect}])
+ end
+ EOM
+ rescue SyntaxError
+ puts ""
+ 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
end
- define_method("#{name}=") do |value|
- set_or_return(name, value, options)
+ end
+
+ #
+ # The list of properties defined on this resource.
+ #
+ # Everything defined with `property` is in this list.
+ #
+ # @param include_superclass [Boolean] `true` to include properties defined
+ # on superclasses; `false` or `nil` to return the list of properties
+ # directly on this class.
+ #
+ # @return [Hash<Symbol,PropertyType>] The list of property names and types.
+ #
+ def self.properties(include_superclass=true)
+ @properties ||= {}
+ if include_superclass
+ if superclass.respond_to?(:properties)
+ superclass.properties.merge(@properties)
+ else
+ @properties.dup
+ end
+ else
+ @properties
end
end
@@ -790,6 +828,9 @@ class Chef
# 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}")
diff --git a/spec/unit/property_spec.rb b/spec/unit/property_spec.rb
index 04a542ea2c..4a3b46d1e4 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)
@@ -121,7 +125,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 +144,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,8 +171,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
@@ -193,8 +197,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