From 24dc69a9a97e82a6e4207de68d6dcc664178249b Mon Sep 17 00:00:00 2001 From: Seth Chisamore Date: Tue, 30 Oct 2012 10:39:35 -0400 Subject: [OC-3564] move core Chef to the repo root \o/ \m/ The opscode/chef repository now only contains the core Chef library code used by chef-client, knife and chef-solo! --- lib/chef/node/immutable_collections.rb | 387 +++++++++++++++++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 lib/chef/node/immutable_collections.rb (limited to 'lib/chef/node/immutable_collections.rb') diff --git a/lib/chef/node/immutable_collections.rb b/lib/chef/node/immutable_collections.rb new file mode 100644 index 0000000000..33f722cd4a --- /dev/null +++ b/lib/chef/node/immutable_collections.rb @@ -0,0 +1,387 @@ + +class Chef + class Node + + module Immutablize + def immutablize(root, value) + case value + when Hash + ImmutableMash.new(root, value) + when Array + ImmutableArray.new(root, value) + else + value + end + end + end + + # == ImmutableArray + # ImmutableArray is used to implement Array collections when reading node + # attributes. + # + # ImmutableArray acts like an ordinary Array, except: + # * Methods that mutate the array are overridden to raise an error, making + # the collection more or less immutable. + # * Since this class stores values computed from a parent + # Chef::Node::Attribute's values, it overrides all reader methods to + # detect staleness and raise an error if accessed when stale. + class ImmutableArray < Array + include Immutablize + + attr_reader :root + + alias :internal_push :<< + private :internal_push + + # A list of methods that mutate Array. Each of these is overridden to + # raise an error, making this instances of this class more or less + # immutable. + DISALLOWED_MUTATOR_METHODS = [ + :<<, + :[]=, + :clear, + :collect!, + :compact!, + :default=, + :default_proc=, + :delete, + :delete_at, + :delete_if, + :fill, + :flatten!, + :insert, + :keep_if, + :map!, + :merge!, + :pop, + :push, + :update, + :reject!, + :reverse!, + :replace, + :select!, + :shift, + :slice!, + :sort!, + :sort_by!, + :uniq!, + :unshift + ] + + # A list of methods that read values from the Array. Each of these is + # overridden to verify that the Chef::Node::Attribute object that this + # object belongs to has not been modified since the value was computed. + READER_METHODS = + [ + :&, + :*, + :+, + :-, + :[], + :all?, + :any?, + :assoc, + :at, + :chunk, + :collect, + :collect_concat, + :combination, + :compact, + :concat, + :count, + :cycle, + :detect, + :drop, + :drop_while, + :each, + :each_cons, + :each_entry, + :each_index, + :each_slice, + :each_with_index, + :each_with_object, + :empty?, + :entries, + :fetch, + :find, + :find_all, + :find_index, + :first, + :flat_map, + :flatten, + :grep, + :group_by, + :include?, + :index, + :inject, + :join, + :last, + :length, + :map, + :max, + :max_by, + :member?, + :min, + :min_by, + :minmax, + :minmax_by, + :none?, + :one?, + :pack, + :partition, + :permutation, + :product, + :rassoc, + :reduce, + :reject, + :repeated_combination, + :repeated_permutation, + :reverse, + :reverse_each, + :rindex, + :rotate, + :sample, + :select, + :shelljoin, + :shuffle, + :size, + :slice, + :slice_before, + :sort, + :sort_by, + :take, + :take_while, + :to_a, + :to_ary, + :to_set, + :transpose, + :uniq, + :values_at, + :zip, + :| + ] + + def initialize(root, array_data) + @root = root + @serial_number = root.serial_number + array_data.each do |value| + internal_push(immutablize(root, value)) + end + end + + # Redefine all of the methods that mutate a Hash to raise an error when called. + # This is the magic that makes this object "Immutable" + DISALLOWED_MUTATOR_METHODS.each do |mutator_method_name| + # Ruby 1.8 blocks can't have block arguments, so we must use string eval: + class_eval(<<-METHOD_DEFN) + def #{mutator_method_name}(*args, &block) + msg = "Node attributes are read-only when you do not specify which precedence level to set. " + + %Q(To set an attribute use code like `node.default["key"] = "value"') + raise Exceptions::ImmutableAttributeModification, msg + end + METHOD_DEFN + end + + READER_METHODS.each do |reader| + class_eval(<<-METHOD_DEFN) + def #{reader}(*args, &block) + if root.stale_subtree?(@serial_number) + raise Exceptions::StaleAttributeRead, + "Node attributes have been modified since this value was read. Get an updated value by reading from node, e.g., `node[:key]`" + end + super + end + METHOD_DEFN + end + + def dup + Array.new(self) + end + end + + # == ImmutableMash + # ImmutableMash implements Hash/Dict behavior for reading values from node + # attributes. + # + # ImmutableMash acts like a Mash (Hash that is indifferent to String or + # Symbol keys), with some important exceptions: + # * Methods that mutate state are overridden to raise an error instead. + # * Methods that read from the collection are overriden so that they check + # if the Chef::Node::Attribute has been modified since an instance of + # this class was generated. An error is raised if the object detects that + # it is stale. + # * Values can be accessed in attr_reader-like fashion via method_missing. + class ImmutableMash < Mash + + include Immutablize + + attr_reader :root + + alias :internal_set :[]= + private :internal_set + + DISALLOWED_MUTATOR_METHODS = [ + :[]=, + :clear, + :collect!, + :default=, + :default_proc=, + :delete, + :delete_if, + :keep_if, + :map!, + :merge!, + :update, + :reject!, + :replace, + :select!, + :shift + ] + + READER_METHODS = [ + :[], + :all?, + :any?, + :assoc, + :chunk, + :collect, + :collect_concat, + :count, + :cycle, + :detect, + :drop, + :drop_while, + :each, + :each_cons, + :each_entry, + :each_key, + :each_pair, + :each_slice, + :each_value, + :each_with_index, + :each_with_object, + :empty?, + :entries, + :except, + :fetch, + :find, + :find_all, + :find_index, + :first, + :flat_map, + :flatten, + :grep, + :group_by, + :has_key?, + :has_value?, + :include?, + :index, + :inject, + :invert, + :key, + :key?, + :keys, + :length, + :map, + :max, + :max_by, + :member?, + :merge, + :min, + :min_by, + :minmax, + :minmax_by, + :none?, + :one?, + :partition, + :rassoc, + :reduce, + :reject, + :reverse_each, + :select, + :size, + :slice_before, + :sort, + :sort_by, + :store, + :symbolize_keys, + :take, + :take_while, + :to_a, + :to_hash, + :to_set, + :value?, + :values, + :values_at, + :zip + ] + + def initialize(root, mash_data) + @serial_number = root.serial_number + @root = root + mash_data.each do |key, value| + internal_set(key, immutablize(root, value)) + end + end + + alias :attribute? :has_key? + + # Redefine all of the methods that mutate a Hash to raise an error when called. + # This is the magic that makes this object "Immutable" + DISALLOWED_MUTATOR_METHODS.each do |mutator_method_name| + # Ruby 1.8 blocks can't have block arguments, so we must use string eval: + class_eval(<<-METHOD_DEFN) + def #{mutator_method_name}(*args, &block) + msg = "Node attributes are read-only when you do not specify which precedence level to set. " + + %Q(To set an attribute use code like `node.default["key"] = "value"') + raise Exceptions::ImmutableAttributeModification, msg + end + METHOD_DEFN + end + + READER_METHODS.each do |reader_method| + class_eval(<<-METHOD_DEFN) + def #{reader_method}(*args, &block) + if root.stale_subtree?(@serial_number) + raise Exceptions::StaleAttributeRead, + "Node attributes have been modified since this value was read. Get an updated value by reading from node, e.g., `node[:key]`" + end + super + end + METHOD_DEFN + end + + def method_missing(symbol, *args) + if args.empty? + if key?(symbol) + self[symbol] + else + raise NoMethodError, "Undefined method or attribute `#{symbol}' on `node'" + end + # This will raise a ImmutableAttributeModification error: + elsif symbol.to_s =~ /=$/ + key_to_set = symbol.to_s[/^(.+)=$/, 1] + self[key_to_set] = (args.length == 1 ? args[0] : args) + else + raise NoMethodError, "Undefined node attribute or method `#{symbol}' on `node'" + end + end + + # Mash uses #convert_value to mashify values on input. + # Since we're handling this ourselves, override it to be a no-op + def convert_value(value) + value + end + + # NOTE: #default and #default= are likely to be pretty confusing. For a + # regular ruby Hash, they control what value is returned for, e.g., + # hash[:no_such_key] #=> hash.default + # Of course, 'default' has a specific meaning in Chef-land + + def dup + Mash.new(self) + end + end + + end +end -- cgit v1.2.1