module Eco module Data module Hashes module SnakeCamelIndifferentAccess include Eco::Data::Strings::SnakeCase include Eco::Data::Strings::CamelCase private # options are: `:snake_case` and `:camel_case` def hash_isca_preferred @hash_isca_preferred ||= :snake_case end # Sets `value` for `key`. The end result is that get on `key` gives `value`, # no matter the original case form of `key`. This might entail to resolve # the `key` if its in its multiple case forms. # @note if an internal merge resolve of the `key` happens, it's done in the # preferred case form. # @note if once resolved the internal merge, only the preferred case form of # the key remains, that key form will be used to do the `set` operation, # no matter the original case form of the `key` argument. # @param keep_case [Boolean] whether only the preferred case form of key # should be used or rather try to keep its case. def hash_isca_set(hash, key, value, prefer: nil, keep_case: false, residual: {}) # (pp hash) if key == 'barCamel' hash_isca_resolve_key!(hash, key, prefer: prefer, residual: residual) # (pp hash) if key == 'barCamel' target_key = hash_isca_to_correct_key(key, keep_case: keep_case, prefer: prefer) # puts "potential_target_key: '#{key}' => '#{target_key}'" if key == 'barCamel' if keep_case # Spot the existing target key ext_keys = hash_isca_existing_correct_keys(hash, key, prefer: prefer) target_key = ext_keys.first unless ext_keys.empty? || ext_keys.include?(target_key) end puts "final target_key: '#{key}' => '#{target_key}'" if key == 'barCamel' hash[target_key] = value hash_isca_resolve_key!(hash, key, prefer: prefer, residual: residual) pp hash if key == 'barCamel' puts "*" * 20 if key == 'barCamel' value end # Retrieves the value of key `key` # @note # 1. when both forms of the key exist, it uses the preferred case form !! # 2. when the key exists only in its form it uses it # 3. when only the other form of the key is present it uses it def hash_isca_get(value, key, prefer: nil) hash = value.to_h keys = hash_isca_double_key(key, prefer: prefer) keys = keys.find_all {|k| hash.key?(k)} return nil unless keys.any? hash[keys.first] end # Slice generates a hash with `keys`, regardless the original case form # of those keys in the source hash (value). # @note values are retrieved as per `hash_isca_get` rules. def hash_isca_slice(value, *keys, prefer: nil) hash = value.to_h values = keys.map do |key| hash_isca_get(hash, key, prefer: prefer) end keys.zip(values).to_h end # Delegates to `hash_isca_get` def hash_isca_values_at(value, *keys, prefer: nil) hash = value.to_h keys.map do |key| hash_isca_get(hash, key, prefer: prefer) end end # The values of the uniq keys, where among keys with multiple case form # the preferred form case is used to retrieve the value. # @note residual named arg was not added just to keep consistent its hash type. def hash_isca_values(value, prefer: nil) hash = value.to_h hash.values_at(*hash_isca_uniq_keys(hash, prefer: prefer)) end # Enumerator def hash_isca_each(value, prefer: nil, residual: {}, &block) return to_enum(:hash_isca_each, value, prefer: prefer, residual: residual) unless block_given? hash = value.to_h keys = hash_isca_uniq_keys(hash, prefer: prefer) residual.merge!(hash.slice(*(hash.keys - keys))) hash.slice(*keys).each(&block) end # @note due to simplicity and consistency, for matching merging keys # it resolves internal key conflicts on both (source and other). # This internal resolve is ONLY applied to common keys (between source and other). def hash_isca_merge!(hash_1, value_2, prefer: nil, residual: {}) hash_1.tap do hash_2 = hash_isca_internal_merge_resolve!(value_2.to_h.dup, residual: (res_2 = {})) hash_isca_each(hash_2, prefer: prefer, residual: residual) do |key, value| hash_isca_set(hash_1, key, value, prefer: prefer, keep_case: true, residual: residual) end residual.merge!(res_2) end end def hash_isca_merge(value_1, value_2, prefer: nil, residual: {}) hash_isca_merge!(value_1.to_h.dup, value_2, prefer: prefer, residual: residual) end # Keys may not necessarily come in its camel or snake case version. # This method ensures they are. # @note modus operandi: # 1. will preserve all the keys that were in a correct case form. # 2. where it can't preserve, the preferred form will be used instead. # 3. where merge conflicts exist, the value associatd with the key that # was originally in the correc case form will prevail. def hash_isca_correct_keys!(hash, prefer: nil, residual: {}) hash.dup.each do |key, _val| ckey = hash_isca_to_correct_key(key, keep_case: true, prefer: prefer) next if ckey == key residual[key] = hash.delete(key) if hash.key?(ckey) end hash end # Same as `hash_isca_correct_keys!` but only for one single key. def hash_isca_correct_key!(hash, key, prefer: nil, residual: {}) tkey = hash_isca_to_correct_key(key, keep_case: true, prefer: prefer) hash.dup.each do |k_c, _val| ckey = hash_isca_to_correct_key(k_c, keep_case: true, prefer: prefer) next unless ckey == tkey next if ckey == k_c residual[k_c] = hash.delete(k_c) if hash.key?(ckey) end hash end # internal merge def hash_isca_internal_merge_resolve!(hash, prefer: nil, residual: {}) hash_isca_correct_keys!(hash, prefer: prefer, residual: residual) keep_keys = hash_isca_uniq_keys(hash, prefer: prefer) hash.dup.each do |key, _v| residual[key] = hash.delete(key) unless keep_keys.include?(key) end hash end # internal merge def hash_isca_internal_merge_resolve(value, prefer: nil, residual: {}) hash_isca_internal_merge_resolve!(value.to_h.dup, prefer: prefer, residual: residual) end # it resolves a single key def hash_isca_resolve_key!(hash, key, prefer: nil, residual: {}) hash_isca_correct_key!(hash, key, prefer: prefer, residual: residual) return hash if hash_isca_uniq_key?(hash, key) _pkey, rkey = hash_isca_double_key(key, prefer: prefer) residual[rkey] = hash.delete(rkey) hash end # all keys to camel case def hash_isca_camelize_keys(value) hash_isca_with_preferred_keys(value, prefer: :camel_case) end def hash_isca_camelize_keys!(hash) hash_isca_with_preferred_keys!(hash, prefer: :camel_case) end # all keys to snake case def hash_isca_snakeize_keys(value) hash_isca_with_preferred_keys(value, prefer: :snake_case) end def hash_isca_snakeize_keys!(hash) hash_isca_with_preferred_keys!(hash, prefer: :snake_case) end # Conerts keys to the `prefer` case form # @note it first resolves with the default/general `hash_isca_preferred`, # as all operations have been done up to now this way. def hash_isca_with_preferred_keys!(hash, prefer: nil, residual: {}) hash_isca_internal_merge_resolve!(hash, residual: residual) hash.dup.each do |k, _v| key = hash_isca_to_preferred_key(k, prefer: prefer) hash[key] = hash.delete(k) end hash end def hash_isca_with_preferred_keys(value, prefer: nil) hash_isca_with_preferred_keys!(value.to_h.dup, prefer: prefer) end # @return [Array] the resolved keys def hash_isca_uniq_keys(value, prefer: nil) hash = value.to_h all_keys = hash.keys dup_keys = all_keys.each_with_object({}) do |key, res| keys = hash_isca_double_key(key, prefer: prefer) next unless keys.all? {|k| hash.key?(k)} next if res.key?(keys.first) res[keys.first] = keys.last end all_keys - dup_keys.values end # @note if among the raw existing keys there were multiple keys for a given # correct case version of `key`, they would be considered as one. # @return [Boolean] `true` if the key is only in one form or not present. # `false` if the key is in **multiple** forms def hash_isca_uniq_key?(value, key) hash_isca_existing_correct_keys(value, key).length <= 1 end # For a given `key` it finds out what keys exist in its correct case form. # @return [Array] the existing correct keys. def hash_isca_existing_correct_keys(value, key, prefer: nil) hash = value.to_h hash_isca_double_key(key, prefer: prefer).find_all do |k| hash.key?(k) end end def hash_isca_key?(value, key) hash = value.to_h hash_isca_double_key(key).any? {|k| hash.key?(k)} end def hash_isca_same_key?(k_1, k_2) hash_isca_double_key(k_1) == hash_isca_double_key(k_2) end # @return [String] the preferred case form of the key def hash_isca_to_preferred_key(key, prefer: nil) hash_isca_double_key(key, prefer: prefer).first end # Spot the correct key. Ensures keys are in camel or snake case. # @param keep_case [Boolean] whether only the preferred case form of key # should be used or rather try to keep its case. # @return [String] the target key def hash_isca_to_correct_key(key, keep_case: true, prefer: nil) pkey, okey = hash_isca_double_key(key, prefer: prefer) return pkey unless keep_case return pkey unless [pkey, okey].include?(key) key end # @return [Boolean] whether `key` is already in snake or camel case form def hash_isca_correct_key?(key) hash_isca_double_key(key).any? {|k| k == key} end def hash_isca_double_key(key, prefer: nil) return [key, key] unless key.is_a?(String) return [snake_case(key), camel_case(key)] if hash_isca_prefer(prefer) == :snake_case [camel_case(key), snake_case(key)] end def hash_isca_prefer(prefer = nil) return hash_isca_preferred || :snake_case if prefer.nil? return :camel_case unless prefer == :snake_case :snake_case end end end end end