lib/snaky_hash/snake.rb



# This is a module-class hybrid.
#
# Hashie's standard SymbolizeKeys is similar to the functionality we want.
# ... but not quite.  We need to support both String (for oauth2) and Symbol keys (for oauth).
# include Hashie::Extensions::Mash::SymbolizeKeys
module SnakyHash
  class Snake < Module
    def initialize(key_type: :string)
      super()
      @key_type = key_type
    end

    def included(base)
      conversions_module = SnakyModulizer.to_mod(@key_type)
      base.include(conversions_module)
    end

    module SnakyModulizer
      def self.to_mod(key_type)
        Module.new do
          # Converts a key to a symbol, or a string, depending on key_type,
          #   but only if it is able to be converted to a symbol,
          #   and after underscoring it.
          #
          # @api private
          # @param [<K>] key the key to attempt convert to a symbol
          # @return [Symbol, K]

          case key_type
          when :string then
            define_method(:convert_key) { |key| key.respond_to?(:to_sym) ? underscore_string(key.to_s) : key }
          when :symbol then
            define_method(:convert_key) { |key| key.respond_to?(:to_sym) ? underscore_string(key.to_s).to_sym : key }
          else
            raise ArgumentError, "SnakyHash: Unhandled key_type: #{key_type}"
          end

          # Unlike its parent Mash, a SnakyHash::Snake will convert other
          #   Hashie::Hash values to a SnakyHash::Snake when assigning
          #   instead of respecting the existing subclass
          define_method :convert_value do |val, duping = false| #:nodoc:
            case val
            when self.class
              val.dup
            when ::Hash
              val = val.dup if duping
              self.class.new(val)
            when ::Array
              val.collect { |e| convert_value(e) }
            else
              val
            end
          end

          # converts a camel_cased string to a underscore string
          # subs spaces with underscores, strips whitespace
          # Same way ActiveSupport does string.underscore
          define_method :underscore_string do |str|
            str.to_s.strip
               .tr(" ", "_")
               .gsub(/::/, "/")
               .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
               .gsub(/([a-z\d])([A-Z])/, '\1_\2')
               .tr("-", "_")
               .squeeze("_")
               .downcase
          end
        end
      end
    end
  end
end