在Ruby中将嵌套哈希键从CamelCase转换为snake_case

39

我正在尝试构建一个API包装器gem,并且在将哈希键从API返回的JSON转换为更Ruby风格的格式方面遇到了问题。

JSON包含多层嵌套,既有哈希又有数组。我想要做的是递归地将所有键转换为snake_case以便于使用。

这是我到目前为止所拥有的:

def convert_hash_keys(value)
  return value if (not value.is_a?(Array) and not value.is_a?(Hash))
  result = value.inject({}) do |new, (key, value)|
    new[to_snake_case(key.to_s).to_sym] = convert_hash_keys(value)
    new
  end
  result
end

上面的代码调用了这个方法来将字符串转换为snake_case:

def to_snake_case(string)
  string.gsub(/::/, '/').
  gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
  gsub(/([a-z\d])([A-Z])/,'\1_\2').
  tr("-", "_").
  downcase
end

理想情况下,结果应该类似于以下内容:

hash = {:HashKey => {:NestedHashKey => [{:Key => "value"}]}}

convert_hash_keys(hash)
# => {:hash_key => {:nested_hash_key => [{:key => "value"}]}}

我在递归中出现了问题,我尝试过各种解决方案,但要么无法转换除第一级以外的符号,要么过度地尝试转换整个哈希表,包括值。

如果可能的话,我想在辅助类中解决所有这些问题,而不是修改实际的哈希表和字符串函数。

提前感谢您的帮助。


在进行任何其他操作之前,“if (not ... and not ...)”是使用De Morgan定律的完美场所。你应该将它写成“unless ... or ...”。 - sawa
7个回答

58

如果您使用 Rails

哈希表示例:将camelCase转换为snake_case

hash = { camelCase: 'value1', changeMe: 'value2' }

hash.transform_keys { |key| key.to_s.underscore }
# => { "camel_case" => "value1", "change_me" => "value2" }

来源: http://apidock.com/rails/v4.0.2/Hash/transform_keys

对于嵌套属性,请使用 deep_transform_keys 而不是 transform_keys,示例:

hash = { camelCase: 'value1', changeMe: { hereToo: { andMe: 'thanks' } } }

hash.deep_transform_keys { |key| key.to_s.underscore }
# => {"camel_case"=>"value1", "change_me"=>{"here_too"=>{"and_me"=>"thanks"}}}

来源:http://apidock.com/rails/v4.2.7/Hash/deep_transform_keys


这只转换哈希的第一层,嵌套哈希键仍保留为驼峰式。 - nayiaw
这对于高级别的操作是可以的,但对于嵌套哈希也需要使用deep_transform_keys。hash.deep_transform_keys { |key| key.to_s.underscore } - Shanaka Kuruwita

42

你需要分别处理数组和哈希表。如果你在使用Rails,可以使用underscore代替你自己编写的to_snake_case函数。首先,一个小助手来减少冗余代码:

def underscore_key(k)
  k.to_s.underscore.to_sym
  # Or, if you're not in Rails:
  # to_snake_case(k.to_s).to_sym
end
如果你的哈希表中的键不是符号或字符串,那么可以相应地修改underscore_key
如果你有一个数组,那么你只需要递归地将convert_hash_keys 应用到数组的每个元素上;如果你有一个哈希表,你想要使用underscore_key修复键,并将convert_hash_keys 应用于每个值;如果你有其他类型的数据,则可以原样传递。
def convert_hash_keys(value)
  case value
    when Array
      value.map { |v| convert_hash_keys(v) }
      # or `value.map(&method(:convert_hash_keys))`
    when Hash
      Hash[value.map { |k, v| [underscore_key(k), convert_hash_keys(v)] }]
    else
      value
   end
end

非常好用。非常感谢。我没有在Rails中做这个,但我相信我用于to_snake_case的代码来自Rails的underscore方法。 - Andrew Stewart
@Andrew:在 to_snake_case 中的 string.gsub(/::/, '/') 让我想到你对 to_snake_case 来源的看法是正确的。欢迎来到 Stack Overflow,祝您愉快。 - mu is too short
不必使用Rails,只需使用ActiveSupport即可,它允许我们挑选需要的程序。require 'active_support/core_ext/string/inflections'就可以了:'FooBar'.underscore => "foo_bar" - the Tin Man
1
问题在于我需要整个ActiveSupport gem,而实际上我只需要一个方法。 - Andrew Stewart

14

我使用这个简略形式:

hash.transform_keys(&:underscore)

而且,正如@Shanaka Kuruwita指出的那样,要深度转换所有嵌套的哈希:

hash.deep_transform_keys(&:underscore)

1
这个是最好的解决方案)旧答案在2012年是正确的)) - Legendary
是的,现在这个答案好多了。 - Lomefin

5

2

使用deep_transform_keys进行递归转换。

transform_keys仅在高层次上进行转换。

hash = { camelCase: 'value1', changeMe: {nestedMe: 'value2'} }

hash.transform_keys { |key| key.to_s.underscore }
# => { "camel_case" => "value1", "change_me" => {nestedMe: 'value2'} }

deep_transform_keys会深入到所有嵌套的哈希中进行转换。

hash = { camelCase: 'value1', changeMe: {nestedMe: 'value2'} }

hash.deep_transform_keys { |key| key.to_s.underscore }
# => { "camel_case" => "value1", "change_me" => {nested_me: 'value2'} }

0
如果您正在使用active_support库,可以像这样使用deep_transform_keys!:
hash.deep_transform_keys! do |key|
  k = key.to_s.snakecase rescue key
  k.to_sym rescue key
end

请注意,在Rails 6中,snakecase应该是underscore - Iain Bryson

0

这对于对象的深度嵌套键 camelCasesnake_case 都适用,非常适用于JSON API:

def camelize_keys(object)
  deep_transform_keys_in_object!(object) { |key| key.to_s.camelize(:lower) }
end

def snakecase_keys(object)
  deep_transform_keys_in_object!(object) { |key| key.to_s.underscore.to_sym }
end

def deep_transform_keys_in_object!(object, &block)
  case object
  when Hash
    object.keys.each do |key|
      value = object.delete(key)
      object[yield(key)] = deep_transform_keys_in_object!(value, &block)
    end
    object
  when Array
    object.map! { |e| deep_transform_keys_in_object!(e, &block) }
  else
    object
  end
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接