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