diff options
-rw-r--r-- | lib/chef/constants.rb | 20 | ||||
-rw-r--r-- | lib/chef/delayed_evaluator.rb | 21 | ||||
-rw-r--r-- | lib/chef/mixin/params_validate.rb | 73 | ||||
-rw-r--r-- | lib/chef/property_type.rb | 36 | ||||
-rw-r--r-- | lib/chef/resource.rb | 49 | ||||
-rw-r--r-- | spec/unit/property_spec.rb | 26 |
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 |