1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
|
# This is a module-class hybrid.
#
# A flexible key conversion system that supports both String and Symbol keys,
# with optional serialization capabilities.
#
# @example Basic usage with string keys
# class MyHash < Hashie::Mash
# include SnakyHash::Snake.new(key_type: :string)
# end
#
# @example Usage with symbol keys and serialization
# class MySerializableHash < Hashie::Mash
# include SnakyHash::Snake.new(key_type: :symbol, serializer: true)
# end
#
# 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).
# see: Hashie::Extensions::Mash::SymbolizeKeys
#
module SnakyHash
# Creates a module that provides key conversion functionality when included
#
# @note Unlike Hashie::Mash, this implementation allows for both String and Symbol key types
class Snake < Module
# Initialize a new Snake module
#
# @param key_type [Symbol] the type to convert keys to (:string or :symbol)
# @param serializer [Boolean] whether to include serialization capabilities
# @raise [ArgumentError] if key_type is not :string or :symbol
def initialize(key_type: :string, serializer: false)
super()
@key_type = key_type
@serializer = serializer
end
# Includes appropriate conversion methods into the base class
#
# @param base [Class] the class including this module
# @return [void]
def included(base)
conversions_module = SnakyModulizer.to_mod(@key_type)
base.include(conversions_module)
if @serializer
base.extend(SnakyHash::Serializer)
end
end
# Internal module factory for creating key conversion functionality
module SnakyModulizer
class << self
# Creates a new module with key conversion methods based on the specified key type
#
# @param key_type [Symbol] the type to convert keys to (:string or :symbol)
# @return [Module] a new module with conversion methods
# @raise [ArgumentError] if key_type is not supported
def to_mod(key_type)
Module.new do
case key_type
when :string then
# Converts a key to a string if it is symbolizable, after underscoring
#
# @note checks for to_sym instead of to_s, because nearly everything responds_to?(:to_s)
# so respond_to?(:to_s) isn't very useful as a test, and would result in symbolizing integers
# amd it also provides parity between the :symbol behavior, and the :string behavior,
# regarding which keys get converted for a given version of Ruby.
#
# @param key [Object] the key to convert
# @return [String, Object] the converted key or original if not convertible
define_method(:convert_key) { |key| key.respond_to?(:to_sym) ? underscore_string(key.to_s) : key }
when :symbol then
# Converts a key to a symbol if possible, after underscoring
#
# @param key [Object] the key to convert
# @return [Symbol, Object] the converted key or original if not convertible
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
# Converts hash values to the appropriate type when assigning
#
# @param val [Object] the value to convert
# @param duping [Boolean] whether the value is being duplicated
# @return [Object] the converted value
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 string to underscore case
#
# @param str [String, #to_s] the string to convert
# @return [String] the underscored string
# @example
# underscore_string("CamelCase") #=> "camel_case"
# underscore_string("API::V1") #=> "api/v1"
# @note This is the same as ActiveSupport's 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
end
|