symtern 0.1.0

Fast general-purpose interners for every use case
Documentation
# Helper class that enables easy use of default values with minimal fuss.  When a `ConfigHash`
# is created, it can be passed a Hash of default values to use when a requested value is not
# found.
class ConfigHash < Hash
  # @!attribute [r]
  #   Default values for this hash.
  #   @return [Hash]
  attr_reader :defaults

  # These methods have been made private since their semantics conflict with the purpose of
  # ConfigHash.
  private(:default, :default=)
  private(:default_proc=) if RUBY_VERSION >= '1.9.1'


  def clone()
    o = super()
    o.instance_variable_set(:@defaults, @defaults.clone)
    o
  end
  

  # Initialize a new `ConfigHash` instance.
  #
  # @param [ConfigHash,Hash,nil] defaults Hash containing default values to use when a given
  #     setting cannot be found, or `nil`.
  #
  # @param [Proc,#clone] default_default A proc to call or value to use as a
  #   default in the new ConfigHash when no default value is available in
  #   `defaults`.
  def initialize(_values = {}, _options = nil, &block)
    _defaults = {}

    default_proc = block if block_given?

    if _options.nil? and _values.has_key?(:defaults)
      _options = _values
      _values = {}
    end
    unless _options.nil?
      _defaults = _options[:defaults]
    end

    @defaults = ( _defaults.nil? ? {} : _defaults )
    @defaults.freeze
    update(_values)
  end

  def subhash_with_defaults(key, value = {})
    _defaults = @defaults[key]
    _klass = ConfigHash
    if _defaults.kind_of?(ConfigHash)
      # Allow for subclasses of ConfigHash in the defaults.
      _klass = _defaults.class
      _defaults = _defaults.defaults if _defaults.empty?
    end          

    _klass.new(value, defaults: _defaults)
  end

  # Setter method override.  This method allows setting values within a (sub) hash with
  # defaults, without losing access to the other default values.
  #
  # @param [Object] k Search key.
  # @param [Object] v Value.
  def []=(key, value)
    old_value = begin
                  fetch(key)
                rescue
                  nil
                end
    new_value =
      if value.kind_of?(Hash)
        if has_key?(key) and old_value.kind_of?(Hash)
          old_value.update(value)
        else
          store(key, subhash_with_defaults(key, value))
        end
      else
        store(key, value)
      end
    new_value
  end

  # Getter method override.  Attempts to use default values when a missing key is encountered,
  # and does some magic to enable assignment to sublevels of a (previously) entirely-default
  # ConfigHash tree.
  #
  # Assignment to sublevels is made possible by creation of an empty ConfigHash for each
  # non-existent key whose default value is a Hash instance.  (If instead the default value was
  # returned, any assignment would change the _default_ value!)
  #
  # @param [Object] s Search key.
  #
  # @return [Object] The value associated with `s`, or the default value if there is no such
  #    association in this hash.  If no default values have been set, returns `nil`.
  def [](key)
    if explicit_key?(key)
      super(key)
    else
      if (result = @defaults[key]).kind_of?(Hash)
        # We need to create the empty storage in case the client assigns an entry of the
        # returned hash -- otherwise, we'd get a modified _default_!
        store(key, subhash_with_defaults(key, result))
      else
        result
      end
    end
  end

  def fetch(key, *rest)
    if explicit_key?(key)
      super
    elsif @defaults.key?(key)
      @defaults[key]
    elsif ! rest.empty?
      rest.shift
    else
      raise KeyError.new('key not found: %s' % key.inspect)
    end
  end      
  # Recursively update from a source hash.  This allows e.g. missing values in the
  # `:directories' sub-hash of a Config.
  #
  # @param [Hash] from Source hash (to update with).
  #
  # @return [Hash] @c tgt
  def update(from)
    from.each_pair do |key, value|
      old_value = begin
                    fetch(key)
                  rescue
                    nil
                  end

      if value.kind_of?(Hash)
        if has_key?(key) and old_value.kind_of?(ConfigHash)
          old_value.update(value)
        else
          store(key, subhash_with_defaults(key, value))
        end
      else
        store(key, value)
      end
    end
    self
  end

  # # Proxy to allow dotted-object-style access to configuration values.
  def method_missing(sym)
    self[sym]
  end
  def respond_to_missing?(sym, *args)
    self.key?(sym) && args.empty?
  end

  alias_method(:explicit_key?, :key?)
  def key?(name)
    explicit_key?(name) or @defaults.key?(name)
  end
  

  def to_hash
    o = {}
    all_keys.each { |k|
      v = self[k]
      o[k] = v.kind_of?(Hash) ? v.to_hash : v
    }
    o
  end

  def ==(other)
    self === other or self.to_hash == other.to_hash
  end

  def ===(other)
    other.kind_of?(ConfigHash) and  @defaults === other.defaults
  end

  def inspect
    "#<#{self.class}:#{'%#x' % self.object_id.abs} #{super}>"
  end

  def all_keys
    (@defaults.respond_to?(:all_keys) ? @defaults.all_keys : @defaults.keys) | keys
  end
end