diff options
author | John Keiser <john@johnkeiser.com> | 2015-10-20 17:15:05 -0700 |
---|---|---|
committer | John Keiser <john@johnkeiser.com> | 2015-10-20 17:15:05 -0700 |
commit | a16d253bec53b8209a092d990c58bcb3e10bd3bd (patch) | |
tree | c0bfafacdb7ccabf85a635209b43aeb22db23bb5 | |
parent | 4d35de670c9d54ad3b672e668672ba3256e2aef8 (diff) | |
parent | 4d11c33afc5821d4b19bba4f8431c941c6c73d39 (diff) | |
download | chef-a16d253bec53b8209a092d990c58bcb3e10bd3bd.tar.gz |
Merge branch 'jk/property_mixin'
-rw-r--r-- | lib/chef/mixin/properties.rb | 302 | ||||
-rw-r--r-- | lib/chef/resource.rb | 339 | ||||
-rw-r--r-- | spec/unit/mixin/properties_spec.rb | 97 |
3 files changed, 428 insertions, 310 deletions
diff --git a/lib/chef/mixin/properties.rb b/lib/chef/mixin/properties.rb new file mode 100644 index 0000000000..85abe4427e --- /dev/null +++ b/lib/chef/mixin/properties.rb @@ -0,0 +1,302 @@ +require 'chef/delayed_evaluator' +require 'chef/mixin/params_validate' +require 'chef/property' + +class Chef + module Mixin + module Properties + module ClassMethods + # + # 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,Property>] The list of property names and types. + # + def properties(include_superclass=true) + if include_superclass + result = {} + ancestors.reverse_each { |c| result.merge!(c.properties(false)) if c.respond_to?(:properties) } + result + else + @properties ||= {} + end + end + + # + # Create a property on this resource class. + # + # If a superclass has this property, or if this property has already been + # defined by this resource, this will *override* the previous value. + # + # @param name [Symbol] The name of the property. + # @param type [Object,Array<Object>] The type(s) of this property. + # If present, this is prepended to the `is` validation option. + # @param options [Hash<Symbol,Object>] Validation options. + # @option options [Object,Array] :is An object, or list of + # objects, that must match the value using Ruby's `===` operator + # (`options[:is].any? { |v| v === value }`). + # @option options [Object,Array] :equal_to An object, or list + # of objects, that must be equal to the value using Ruby's `==` + # operator (`options[:is].any? { |v| v == value }`) + # @option options [Regexp,Array<Regexp>] :regex An object, or + # list of objects, that must match the value with `regex.match(value)`. + # @option options [Class,Array<Class>] :kind_of A class, or + # list of classes, that the value must be an instance of. + # @option options [Hash<String,Proc>] :callbacks A hash of + # messages -> procs, all of which match the value. The proc must + # return a truthy or falsey value (true means it matches). + # @option options [Symbol,Array<Symbol>] :respond_to A method + # name, or list of method names, the value must respond to. + # @option options [Symbol,Array<Symbol>] :cannot_be A property, + # or a list of properties, that the value cannot have (such as `:nil` or + # `:empty`). The method with a questionmark at the end is called on the + # value (e.g. `value.empty?`). If the value does not have this method, + # it is considered valid (i.e. if you don't respond to `empty?` we + # assume you are not empty). + # @option options [Proc] :coerce A proc which will be called to + # transform the user input to canonical form. The value is passed in, + # and the transformed value returned as output. Lazy values will *not* + # be passed to this method until after they are evaluated. Called in the + # context of the resource (meaning you can access other properties). + # @option options [Boolean] :required `true` if this property + # must be present; `false` otherwise. This is checked after the resource + # is fully initialized. + # @option options [Boolean] :name_property `true` if this + # property defaults to the same value as `name`. Equivalent to + # `default: lazy { name }`, except that #property_is_set? will + # return `true` if the property is set *or* if `name` is set. + # @option options [Boolean] :name_attribute Same as `name_property`. + # @option options [Object] :default The value this property + # will return if the user does not set one. If this is `lazy`, it will + # be run in the context of the instance (and able to access other + # properties). + # @option options [Boolean] :desired_state `true` if this property is + # part of desired state. Defaults to `true`. + # @option options [Boolean] :identity `true` if this property + # is part of object identity. Defaults to `false`. + # + # @example Bare property + # property :x + # + # @example With just a type + # property :x, String + # + # @example With just options + # property :x, default: 'hi' + # + # @example With type and options + # property :x, String, default: 'hi' + # + def property(name, type=NOT_PASSED, **options) + name = name.to_sym + + options.each { |k,v| options[k.to_sym] = v if k.is_a?(String) } + + options[:instance_variable_name] = :"@#{name}" if !options.has_key?(:instance_variable_name) + 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 + + 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 property_type(**options) + Property.derive(**options) + end + + # + # Create a lazy value for assignment to a default value. + # + # @param block The block to run when the value is retrieved. + # + # @return [Chef::DelayedEvaluator] The lazy value + # + def lazy(&block) + DelayedEvaluator.new(&block) + end + + # + # 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. + # + # 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 state_properties(*names) + if !names.empty? + names = names.map { |name| name.to_sym }.uniq + + 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 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 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 identity_properties *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 + + def included(other) + other.extend ClassMethods + end + end + + def self.included(other) + other.extend ClassMethods + end + + include Chef::Mixin::ParamsValidate + + # + # 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) + 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 + end + end +end diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 9e40fdccfd..90453bd00e 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -19,7 +19,6 @@ # require 'chef/exceptions' -require 'chef/mixin/params_validate' require 'chef/dsl/platform_introspection' require 'chef/dsl/data_query' require 'chef/dsl/registry_helper' @@ -40,6 +39,7 @@ require 'chef/resource_resolver' require 'set' require 'chef/mixin/deprecation' +require 'chef/mixin/properties' require 'chef/mixin/provides' require 'chef/mixin/shell_out' require 'chef/mixin/powershell_out' @@ -61,6 +61,34 @@ class Chef include Chef::Mixin::ShellOut include Chef::Mixin::PowershellOut + # Bring in `property` and `property_type` + include Chef::Mixin::Properties + + # + # 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 + # # The node the current Chef run is using. # @@ -133,30 +161,6 @@ class Chef 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,Property>] 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 - - # # The action or actions that will be taken when this resource is run. # # @param arg [Array[Symbol], Symbol] A list of actions (e.g. `:create`) @@ -681,7 +685,6 @@ class Chef # Resource Definition Interface (for resource developers) # - include Chef::Mixin::ParamsValidate include Chef::Mixin::Deprecation # @@ -715,240 +718,6 @@ class Chef end # - # Create a property on this resource class. - # - # If a superclass has this property, or if this property has already been - # defined by this resource, this will *override* the previous value. - # - # @param name [Symbol] The name of the property. - # @param type [Object,Array<Object>] The type(s) of this property. - # If present, this is prepended to the `is` validation option. - # @param options [Hash<Symbol,Object>] Validation options. - # @option options [Object,Array] :is An object, or list of - # objects, that must match the value using Ruby's `===` operator - # (`options[:is].any? { |v| v === value }`). - # @option options [Object,Array] :equal_to An object, or list - # of objects, that must be equal to the value using Ruby's `==` - # operator (`options[:is].any? { |v| v == value }`) - # @option options [Regexp,Array<Regexp>] :regex An object, or - # list of objects, that must match the value with `regex.match(value)`. - # @option options [Class,Array<Class>] :kind_of A class, or - # list of classes, that the value must be an instance of. - # @option options [Hash<String,Proc>] :callbacks A hash of - # messages -> procs, all of which match the value. The proc must - # return a truthy or falsey value (true means it matches). - # @option options [Symbol,Array<Symbol>] :respond_to A method - # name, or list of method names, the value must respond to. - # @option options [Symbol,Array<Symbol>] :cannot_be A property, - # or a list of properties, that the value cannot have (such as `:nil` or - # `:empty`). The method with a questionmark at the end is called on the - # value (e.g. `value.empty?`). If the value does not have this method, - # it is considered valid (i.e. if you don't respond to `empty?` we - # assume you are not empty). - # @option options [Proc] :coerce A proc which will be called to - # transform the user input to canonical form. The value is passed in, - # and the transformed value returned as output. Lazy values will *not* - # be passed to this method until after they are evaluated. Called in the - # context of the resource (meaning you can access other properties). - # @option options [Boolean] :required `true` if this property - # must be present; `false` otherwise. This is checked after the resource - # is fully initialized. - # @option options [Boolean] :name_property `true` if this - # property defaults to the same value as `name`. Equivalent to - # `default: lazy { name }`, except that #property_is_set? will - # return `true` if the property is set *or* if `name` is set. - # @option options [Boolean] :name_attribute Same as `name_property`. - # @option options [Object] :default The value this property - # will return if the user does not set one. If this is `lazy`, it will - # be run in the context of the instance (and able to access other - # properties). - # @option options [Boolean] :desired_state `true` if this property is - # part of desired state. Defaults to `true`. - # @option options [Boolean] :identity `true` if this property - # is part of object identity. Defaults to `false`. - # - # @example Bare property - # property :x - # - # @example With just a type - # property :x, String - # - # @example With just options - # property :x, default: 'hi' - # - # @example With type and options - # property :x, String, default: 'hi' - # - def self.property(name, type=NOT_PASSED, **options) - name = name.to_sym - - options.each { |k,v| options[k.to_sym] = v if k.is_a?(String) } - - options[:instance_variable_name] = :"@#{name}" if !options.has_key?(:instance_variable_name) - 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 - - 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) - 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 - - # - # Create a lazy value for assignment to a default value. - # - # @param block The block to run when the value is retrieved. - # - # @return [Chef::DelayedEvaluator] The lazy value - # - def self.lazy(&block) - DelayedEvaluator.new(&block) - end - - # - # 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. - # - # 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 - - 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 list of "state properties" implemented by the Resource # subclass. # @@ -973,56 +742,6 @@ class Chef end # - # 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 identity_properties *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 diff --git a/spec/unit/mixin/properties_spec.rb b/spec/unit/mixin/properties_spec.rb new file mode 100644 index 0000000000..18178619e4 --- /dev/null +++ b/spec/unit/mixin/properties_spec.rb @@ -0,0 +1,97 @@ +require 'support/shared/integration/integration_helper' +require 'chef/mixin/properties' + +module ChefMixinPropertiesSpec + describe "Chef::Resource.property" do + include IntegrationSupport + + context "with a base class A with properties a, ab, and ac" do + class A + include Chef::Mixin::Properties + property :a, 'a', default: 'a' + property :ab, ['a', 'b'], default: 'a' + property :ac, ['a', 'c'], default: 'a' + end + + context "and a module B with properties b, ab and bc" do + module B + include Chef::Mixin::Properties + property :b, 'b', default: 'b' + property :ab, default: 'b' + property :bc, ['b', 'c'], default: 'c' + end + + context "and a derived class C < A with properties c, ac and bc" do + class C < A + include B + property :c, 'c', default: 'c' + property :ac, default: 'c' + property :bc, default: 'c' + end + + it "A.properties has a, ab, and ac with types 'a', ['a', 'b'], and ['b', 'c']" do + expect(A.properties.keys).to eq [ :a, :ab, :ac ] + expect(A.properties[:a].validation_options[:is]).to eq 'a' + expect(A.properties[:ab].validation_options[:is]).to eq [ 'a', 'b' ] + expect(A.properties[:ac].validation_options[:is]).to eq [ 'a', 'c' ] + end + it "B.properties has b, ab, and bc with types 'b', nil and ['b', 'c']" do + expect(B.properties.keys).to eq [ :b, :ab, :bc ] + expect(B.properties[:b].validation_options[:is]).to eq 'b' + expect(B.properties[:ab].validation_options[:is]).to be_nil + expect(B.properties[:bc].validation_options[:is]).to eq [ 'b', 'c' ] + end + it "C.properties has a, b, c, ac and bc with merged types" do + expect(C.properties.keys).to eq [ :a, :ab, :ac, :b, :bc, :c ] + expect(C.properties[:a].validation_options[:is]).to eq 'a' + expect(C.properties[:b].validation_options[:is]).to eq 'b' + expect(C.properties[:c].validation_options[:is]).to eq 'c' + expect(C.properties[:ac].validation_options[:is]).to eq [ 'a', 'c' ] + expect(C.properties[:bc].validation_options[:is]).to eq [ 'b', 'c' ] + end + it "C.properties has ab with a non-merged type (from B)" do + expect(C.properties[:ab].validation_options[:is]).to be_nil + end + + context "and an instance of C" do + let(:c) { C.new } + + it "all properties can be retrieved and merged properties default to ab->b, ac->c, bc->c" do + expect(c.a).to eq('a') + expect(c.b).to eq('b') + expect(c.c).to eq('c') + expect(c.ab).to eq('b') + expect(c.ac).to eq('c') + expect(c.bc).to eq('c') + end + end + end + end + end + end + + context "with an Inner module" do + module Inner + include Chef::Mixin::Properties + property :inner + end + + context "and an Outer module including it" do + module Outer + include Inner + property :outer + end + + context "and an Outerest class including that" do + class Outerest + include Outer + property :outerest + end + + it "Outerest.properties.validation_options[:is] inner, outer, outerest" do + expect(Outerest.properties.keys).to eq [:inner, :outer, :outerest] + end + end + end + end +end |