diff options
Diffstat (limited to 'lib/chef/node/immutable_collections.rb')
-rw-r--r-- | lib/chef/node/immutable_collections.rb | 183 |
1 files changed, 161 insertions, 22 deletions
diff --git a/lib/chef/node/immutable_collections.rb b/lib/chef/node/immutable_collections.rb index 57c2f8c021..0320e0fb8b 100644 --- a/lib/chef/node/immutable_collections.rb +++ b/lib/chef/node/immutable_collections.rb @@ -24,22 +24,16 @@ class Chef class Node module Immutablize - def convert_value(value) + def convert_value(value, path = nil) case value when Hash - ImmutableMash.new(value, __root__, __node__, __precedence__) + ImmutableMash.new({}, __root__, __node__, __precedence__, path) when Array - ImmutableArray.new(value, __root__, __node__, __precedence__) - when ImmutableMash, ImmutableArray - value + ImmutableArray.new([], __root__, __node__, __precedence__, path) else value end end - - def immutablize(value) - convert_value(value) - end end # == ImmutableArray @@ -53,17 +47,52 @@ class Chef # Chef::Node::Attribute's values, it overrides all reader methods to # detect staleness and raise an error if accessed when stale. class ImmutableArray < Array + alias_method :internal_clear, :clear + alias_method :internal_replace, :replace + alias_method :internal_push, :<< + alias_method :internal_to_a, :to_a + alias_method :internal_each, :each + private :internal_push, :internal_replace, :internal_clear, :internal_each + protected :internal_to_a + include Immutablize - alias :internal_push :<< - private :internal_push + methods = Array.instance_methods - Object.instance_methods + + [ :!, :!=, :<=>, :==, :===, :eql?, :to_s, :hash, :key, :has_key?, :inspect, :pretty_print, :pretty_print_inspect, :pretty_print_cycle, :pretty_print_instance_variables ] - def initialize(array_data = []) - array_data.each do |value| - internal_push(immutablize(value)) + methods.each do |method| + define_method method do |*args, &block| + ensure_generated_cache! + super(*args, &block) + end + end + + def each + ensure_generated_cache! + # aggressively pre generate the cache, works around ruby being too smart and fiddling with internals + internal_each { |i| i.ensure_generated_cache! if i.respond_to?(:ensure_generated_cache!) } + super + end + + # because sometimes ruby gives us back Arrays or ImmutableArrays out of objects from things like #uniq or array slices + def return_normal_array(array) + if array.respond_to?(:internal_to_a, true) + array.internal_to_a + else + puts array.class + array.to_a end end + def uniq + ensure_generated_cache! + return_normal_array(super) + end + + def initialize(array_data = []) + # Immutable collections no longer have initialized state + end + # For elements like Fixnums, true, nil... def safe_dup(e) e.dup @@ -91,7 +120,58 @@ class Chef a end - # for consistency's sake -- integers 'converted' to integers + alias_method :to_array, :to_a + + def [](*args) + ensure_generated_cache! + args.length > 1 ? return_normal_array(super) : super # correctly handle array slices + end + + def reset + @generated_cache = false + internal_clear # redundant? + end + + # @api private + def ensure_generated_cache! + generate_cache unless @generated_cache + @generated_cache = true + end + + private + + def combined_components(components) + combined_values = nil + components.each do |component| + values = __node__.attributes.instance_variable_get(component).read(*__path__) + next unless values.is_a?(Array) + combined_values ||= [] + combined_values += values + end + combined_values + end + + def get_array(component) + array = __node__.attributes.instance_variable_get(component).read(*__path__) + if array.is_a?(Array) + array + end # else nil + end + + def generate_cache + internal_clear + components = [] + components << combined_components(Attribute::DEFAULT_COMPONENTS) + components << get_array(:@normal) + components << combined_components(Attribute::OVERRIDE_COMPONENTS) + components << get_array(:@automatic) + highest = components.compact.last + if highest.is_a?(Array) + internal_replace( highest.each_with_index.map { |x, i| convert_value(x, __path__ + [ i ] ) } ) + end + end + + # needed for __path__ def convert_key(key) key end @@ -113,19 +193,30 @@ class Chef # it is stale. # * Values can be accessed in attr_reader-like fashion via method_missing. class ImmutableMash < Mash + alias_method :internal_clear, :clear + alias_method :internal_key?, :key? # FIXME: could bypass convert_key in Mash for perf + include Immutablize include CommonAPI + methods = Hash.instance_methods - Object.instance_methods + + [ :!, :!=, :<=>, :==, :===, :eql?, :to_s, :hash, :key, :has_key?, :inspect, :pretty_print, :pretty_print_inspect, :pretty_print_cycle, :pretty_print_instance_variables ] + + methods.each do |method| + define_method method do |*args, &block| + ensure_generated_cache! + super(*args, &block) + end + end + # this is for deep_merge usage, chef users must never touch this API # @api private def internal_set(key, value) - regular_writer(key, convert_value(value)) + regular_writer(key, convert_value(value, __path__ + [ key ])) end def initialize(mash_data = {}) - mash_data.each do |key, value| - internal_set(key, value) - end + # Immutable collections no longer have initialized state end alias :attribute? :has_key? @@ -134,8 +225,8 @@ class Chef if symbol == :to_ary super elsif args.empty? - if key?(symbol) - self[symbol] + if key?(symbol.to_s) + self[symbol.to_s] else raise NoMethodError, "Undefined method or attribute `#{symbol}' on `node'" end @@ -157,7 +248,7 @@ class Chef Mash.new(self) end - def to_hash + def to_h h = Hash.new each_pair do |k, v| h[k] = @@ -173,6 +264,54 @@ class Chef h end + def [](key) + ensure_generated_cache! + super +# unless @merged_lazy_hash.nil? +# @merged_lazy_hash[key] +# else +# Attribute::COMPONENTS.reverse.each do |component| +# value = __node__.attributes.instance_variable_get(component).read(*__path__, key) +# unless value.nil? +# return convert_value(value, __path__ + [ key ]) +# end +# end +# nil +# end + end + + alias_method :to_hash, :to_h + + def reset + @generated_cache = false + internal_clear # redundant? + end + + # @api private + def ensure_generated_cache! + generate_cache unless @generated_cache + @generated_cache = true + end + + private + + def generate_cache + internal_clear + Attribute::COMPONENTS.reverse.each do |component| + subhash = __node__.attributes.instance_variable_get(component).read(*__path__) + unless subhash.nil? # FIXME: nil is used for not present + if subhash.kind_of?(Hash) + subhash.keys.each do |key| + next if internal_key?(key) + internal_set(key, subhash[key]) + end + else + break + end + end + end + end + prepend Chef::Node::Mixin::StateTracking prepend Chef::Node::Mixin::ImmutablizeHash end |