lib/configurable/delegate_hash.rb in configurable-0.1.0 vs lib/configurable/delegate_hash.rb in configurable-0.3.0
- old
+ new
@@ -25,141 +25,184 @@
# dhash[:not_delegated] # => 'value'
#
# dhash.store # => {:not_delegated => 'value'}
# dhash.to_hash # => {:key => 'another', :not_delegated => 'value'}
#
+ # ==== IndifferentAccess
+ #
+ # The delegates hash maps keys to Delegate objects. In cases where multiple
+ # keys need to map to the same delegate (for example when you want indifferent
+ # access for strings and symbols), simply extend the delegate hash so that the
+ # AGET ([]) method returns the correct delegate in all cases.
+ #
class DelegateHash
# The bound receiver
attr_reader :receiver
- # The underlying data store for non-delegate keys
+ # The underlying data store
attr_reader :store
# A hash of (key, Delegate) pairs identifying which
# keys to delegate to the receiver
attr_reader :delegates
# Initializes a new DelegateHash. Note that initialize simply sets the
- # receiver, it does NOT map stored values the same way bind does.
- # This allows quick, implicit binding where the bound store is set
- # up beforehand.
+ # receiver, it does NOT map stored values the same way bind does. This
+ # allows quick, implicit binding when the store is set up beforehand.
#
# For more standard binding use: DelegateHash.new.bind(receiver)
def initialize(delegates={}, store={}, receiver=nil)
- @receiver = nil
@store = store
@delegates = delegates
@receiver = receiver
end
- # Binds self to the specified receiver. Mapped keys are
- # removed from store and sent to their writer method on
- # receiver.
+ # Binds self to the specified receiver. Delegate values are removed from
+ # store and sent to their writer method on receiver. If the store has no
+ # value for a delegate key, the delegate default value will be used.
def bind(receiver)
raise ArgumentError, "receiver cannot be nil" if receiver == nil
- raise ArgumentError, "already bound to: #{@receiver}" if bound? && @receiver != receiver
-
- store.keys.each do |key|
- next unless delegate = delegates[key]
- receiver.send(delegate.writer, store.delete(key)) if delegate.writer
+
+ if bound?
+ if @receiver == receiver
+ return(self)
+ else
+ raise ArgumentError, "already bound to: #{@receiver}"
+ end
end
+
@receiver = receiver
-
+ map(store)
self
end
# Returns true if self is bound to a receiver
def bound?
receiver != nil
end
- # Unbinds self from the specified receiver. Mapped values
+ # Unbinds self from the specified receiver. Delegate values
# are stored in store. Returns the unbound receiver.
def unbind
- delegates.each_pair do |key, delegate|
- store[key] = receiver.send(delegate.reader) if delegate.reader
- end
- current_receiver = receiver
+ unmap(store)
@receiver = nil
-
- current_receiver
+ self
end
- # Retrieves the value corresponding to the key. If bound?
- # and the key is a delegates key, then the value is
- # obtained from the delegate.reader method on the receiver.
+ # Retrieves the value corresponding to the key. When bound, delegates with
+ # readers pull values from the receiver; otherwise the value in store will
+ # be returned. When unbound, if the store has no value for a delegate, the
+ # delgate default value will be returned.
def [](key)
- case
- when bound? && delegate = delegates[key]
- delegate.reader ? receiver.send(delegate.reader) : store[key]
- else store[key]
+ return store[key] unless delegate = delegates[key]
+
+ case
+ when bound? && delegate.reader
+ receiver.send(delegate.reader)
+ when store.has_key?(key)
+ store[key]
+ else
+ store[key] = delegate.default
end
end
- # Associates the value the key. If bound? and the key
- # is a delegates key, then the value will be forwarded
- # to the delegate.writer method on the receiver.
+ # Stores a value for the key. When bound, delegates with writers send the
+ # value to the receiver; otherwise values are stored in store.
def []=(key, value)
- case
- when bound? && delegate = delegates[key]
- delegate.writer ? receiver.send(delegate.writer, value) : store[key] = value
- else store[key] = value
+ if bound? && delegate = delegates[key]
+ if delegate.writer
+ receiver.send(delegate.writer, value)
+ return
+ end
end
+
+ store[key] = value
end
+
+ # Returns the union of delegate and store keys.
+ def keys
+ delegates.keys | store.keys
+ end
- # True if the key is assigned in self.
+ # True if the key is an assigned delegate or store key.
def has_key?(key)
- (bound? && delegates.has_key?(key)) || store.has_key?(key)
+ delegates.has_key?(key) || store.has_key?(key)
end
+
+ # Merges another with self.
+ def merge!(another)
+ if bound?
+ (delegates.keys | another.keys).each do |key|
+ self[key] = another[key] if another.has_key?(key)
+ end
+ else
+ # optimization for the common case of an
+ # unbound merge of another hash
+ store.merge!(another.to_hash)
+ end
+ end
# Calls block once for each key-value pair stored in self.
def each_pair # :yields: key, value
- delegates.each_pair do |key, delegate|
- yield(key, receiver.send(delegate.reader)) if delegate.reader
- end if bound?
-
- store.each_pair do |key, value|
- yield(key, value)
- end
+ keys.each {|key| yield(key, self[key]) }
end
- # Updates self to ensure that each delegates key
- # has a value in self; the delegate.default value is
- # set if a value does not already exist.
- #
- # Returns self.
- def update
- delegates.each_pair do |key, delegate|
- self[key] ||= delegate.default
- end
- self
- end
-
- # Duplicates self, returning an unbound DelegateHash.
- def dup
- duplicate = super()
- duplicate.instance_variable_set(:@receiver, nil)
- duplicate.instance_variable_set(:@store, @store.dup)
- duplicate
- end
-
# Equal if the to_hash values of self and another are equal.
def ==(another)
another.respond_to?(:to_hash) && to_hash == another.to_hash
end
- # Returns self as a hash.
+ # Returns self as a hash. Any DelegateHash values are recursively
+ # hashified, to account for nesting.
def to_hash
- hash = store.dup
- delegates.keys.each do |key|
- hash[key] = self[key]
- end if bound?
+ hash = {}
+ each_pair do |key, value|
+ hash[key] = value.kind_of?(DelegateHash) ? value.to_hash : value
+ end
hash
end
# Overrides default inspect to show the to_hash values.
def inspect
"#<#{self.class}:#{object_id} to_hash=#{to_hash.inspect}>"
+ end
+
+ # Ensures duplicates are unbound and store the same values as the original.
+ def initialize_copy(orig)
+ super
+
+ @receiver = nil
+ @store = @store.dup
+ orig.unmap(@store) if orig.bound?
+ end
+
+ protected
+
+ # helper to map delegate values from source to the receiver
+ def map(source) # :nodoc:
+ delegates.each_pair do |key, delegate|
+ next unless writer = delegate.writer
+
+ # map the value; if no value is set in the source then use the
+ # delegate default. if map_default is false, then simply skip...
+ # this ensures each config is initialized to a value when bound
+ # UNLESS map_default is set (indicating manual initialization)
+ value = case
+ when source.has_key?(key) then source.delete(key)
+ when delegate[:map_default, true] then delegate.default
+ else next
+ end
+
+ receiver.send(writer, value)
+ end
+ end
+
+ # helper to unmap delegates from the receiver to a target hash
+ def unmap(target) # :nodoc:
+ delegates.each_pair do |key, delegate|
+ next unless reader = delegate.reader
+ target[key] = receiver.send(reader)
+ end
end
end
end
\ No newline at end of file