summaryrefslogtreecommitdiff
path: root/lib/chef/node/immutable_collections.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/node/immutable_collections.rb')
-rw-r--r--lib/chef/node/immutable_collections.rb387
1 files changed, 387 insertions, 0 deletions
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