require 'nugrant/config' module Nugrant class Bag < Hash ## # Create a new Bag object which holds key/value pairs. # The Bag object inherits from the Hash object, the main # differences with a normal Hash are indifferent access # (symbol or string) and method access (via method call). # # Hash objects in the map are converted to Bag. This ensure # proper nesting of functionality. # # =| Arguments # * `elements` # The initial elements the bag should be built with it.' # Must be an object responding to `each` and accepting # a block with two arguments: `key, value`. Defaults to # the empty hash. # # * `config` # A Nugrant::Config object or hash passed to Nugrant::Config # constructor. Used for `key_error` handler. # def initialize(elements = {}, config = {}) super() @__config = Config::convert(config) (elements || {}).each do |key, value| self[key] = value end end def config=(config) @__config = Config::convert(config) end def method_missing(method, *args, &block) self[method] end ## ### Enumerable Overridden Methods (for string & symbol indifferent access) ## def count(item) super(__try_convert_item(item)) end def find_index(item = nil, &block) block_given? ? super(&block) : super(__try_convert_item(item)) end ## ### Hash Overridden Methods (for string & symbol indifferent access) ## def [](input) key = __convert_key(input) return @__config.key_error.call(key) if not key?(key) super(key) end def []=(input, value) super(__convert_key(input), __convert_value(value)) end def assoc(key) super(__convert_key(key)) end def delete(key) super(__convert_key(key)) end def fetch(key, default = nil) super(__convert_key(key), default) end def has_key?(key) super(__convert_key(key)) end def include?(item) super(__try_convert_item(item)) end def key?(key) super(__convert_key(key)) end def member?(item) super(__try_convert_item(item)) end def dup() self.class.new(self, @__config.dup()) end def merge(other, options = {}) result = dup() result.merge!(other) end def merge!(other, options = {}) other.each do |key, value| current = __get(key) case when current == nil self[key] = value when current.kind_of?(Hash) && value.kind_of?(Hash) current.merge!(value, options) when current.kind_of?(Array) && value.kind_of?(Array) strategy = options[:array_merge_strategy] if not Nugrant::Config.supported_array_merge_strategy(strategy) strategy = @__config.array_merge_strategy end self[key] = send("__#{strategy}_array_merge", current, value) when value != nil self[key] = value end end self end def store(key, value) self[key] = value end def to_hash(options = {}) return {} if empty?() use_string_key = options[:use_string_key] Hash[map do |key, value| key = use_string_key ? key.to_s() : key value = __convert_value_to_hash(value, options) [key, value] end] end def walk(path = [], &block) each do |key, value| nested_bag = value.kind_of?(Nugrant::Bag) value.walk(path + [key], &block) if nested_bag yield path + [key], key, value if not nested_bag end end alias_method :to_ary, :to_a ## ### Private Methods ## private def __convert_key(key) return key.to_s().to_sym() if !key.nil? && key.respond_to?(:to_s) raise ArgumentError, "Key cannot be nil" if key.nil? raise ArgumentError, "Key cannot be converted to symbol, current value [#{key}] (#{key.class.name})" end ## # This function change value convertible to Bag into actual Bag. # This trick enable deeply using all Bag functionalities and also # ensures at the same time a deeply preventive copy since a new # instance is created for this nested structure. # # In addition, it also transforms array elements of type Hash into # Bag instance also. This enable indifferent access inside arrays # also. # # @param value The value to convert to bag if necessary # @return The converted value # def __convert_value(value) case # Converts Hash to Bag instance when value.kind_of?(Hash) Bag.new(value, @__config) # Converts Array elements to Bag instances if needed when value.kind_of?(Array) value.map(&self.method(:__convert_value)) # Keeps as-is other elements else value end end ## # This function does the reversion of value conversion to Bag # instances. It transforms Bag instances to Hash (using to_hash) # and array Bag elements to Hash (using to_hash). # # The options parameters are pass to the to_hash function # when invoked. # # @param value The value to convert to hash # @param options The options passed to the to_hash function # @return The converted value # def __convert_value_to_hash(value, options = {}) case # Converts Bag instance to Hash when value.kind_of?(Bag) value.to_hash(options) # Converts Array elements to Hash instances if needed when value.kind_of?(Array) value.map { |value| __convert_value_to_hash(value, options) } # Keeps as-is other elements else value end end def __get(key) # Calls Hash method [__convert_key(key)], used internally to retrieve value without raising Undefined parameter self.class.superclass.instance_method(:[]).bind(self).call(__convert_key(key)) end ## # The concat order is the reversed compared to others # because we assume that new values have precedence # over current ones. Hence, the are prepended to # current values. This is also logical for parameters # because of the order in which bags are merged. # def __concat_array_merge(current_array, new_array) new_array + current_array end def __extend_array_merge(current_array, new_array) current_array | new_array end def __replace_array_merge(current_array, new_array) new_array end def __try_convert_item(args) return [__convert_key(args[0]), args[1]] if args.kind_of?(Array) __convert_key(args) rescue args end end end