将Hash递归转换为OpenStruct

h4cxqtbf  于 2022-11-04  发布在  Ruby
关注(0)|答案(9)|浏览(150)

假设我有这个哈希值:

h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }

我转换为OpenStruct:

o = OpenStruct.new(h)
 => #<OpenStruct a="a", b="b", c={:d=>"d", :e=>"e"}> 
o.a
 => "a" 
o.b
 => "b" 
o.c
 => {:d=>"d", :e=>"e"} 
2.1.2 :006 > o.c.d
NoMethodError: undefined method `d' for {:d=>"d", :e=>"e"}:Hash

我希望所有的嵌套键都是方法,所以我可以这样访问d

o.c.d
=> "d"

我如何才能做到这一点?

zbwhf8kr

zbwhf8kr1#

您可以对Hash类进行monkey-patch

class Hash
  def to_o
    JSON.parse to_json, object_class: OpenStruct
  end
end

那么你可以说

h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
o = h.to_o
o.c.d # => 'd'

请参阅Convert a complex nested hash to an object

bksxznpy

bksxznpy2#

我想出了这个办法:

h = { a: 'a', b: 'b', c: { d: 'd', e: 'e'} }
json = h.to_json
=> "{\"a\":\"a\",\"b\":\"b\",\"c\":{\"d\":\"d\",\"e\":\"e\"}}" 
object = JSON.parse(json, object_class:OpenStruct)
object.c.d
 => "d"

所以为了让它起作用,我必须做一个额外的步骤:将其转换为json。

j9per5c4

j9per5c43#

我个人使用recursive-open-struct gem -它就像RecursiveOpenStruct.new(<nested_hash>)一样简单
但为了便于递归练习,我将向您展示一个新的解决方案:

require 'ostruct'

def to_recursive_ostruct(hash)
  result = hash.each_with_object({}) do |(key, val), memo|
    memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
  end
  OpenStruct.new(result)
end

puts to_recursive_ostruct(a: { b: 1}).a.b

# => 1
  • 编辑 *

卫行健在这里表现出了轻微的改善https://stackoverflow.com/a/69311716/2981429

def to_recursive_ostruct(hash)
  hash.each_with_object(OpenStruct.new) do |(key, val), memo|
    memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
  end
end

另请参见https://stackoverflow.com/a/63264908/2981429,它显示了如何处理数组

  • 注意事项 *

这比基于JSON的解决方案更好的原因是,当你转换成JSON时,你可能会丢失一些数据。2例如,如果你把一个Time对象转换成JSON,然后解析它,它将是一个字符串。3还有很多其他的例子:

class Foo; end
JSON.parse({obj: Foo.new}.to_json)["obj"]

# => "#<Foo:0x00007fc8720198b0>"

是的......不是很有用。你已经完全失去了对实际示例的引用。

brccelvz

brccelvz4#

下面是一个递归的解决方案,它可以避免将hash转换为json:

def to_o(obj)
  if obj.is_a?(Hash)
    return OpenStruct.new(obj.map{ |key, val| [ key, to_o(val) ] }.to_h)
  elsif obj.is_a?(Array)
    return obj.map{ |o| to_o(o) }
  else # Assumed to be a primitive value
    return obj
  end
end
uwopmtnx

uwopmtnx5#

我的解决方案基于max pleaner's answer,与Xavi's answer类似:

require 'ostruct'

def initialize_open_struct_deeply(value)
  case value
  when Hash
    OpenStruct.new(value.transform_values { |hash_value| send __method__, hash_value })
  when Array
    value.map { |element| send __method__, element }
  else
    value
  end
end
qeeaahzv

qeeaahzv6#

我的解决方案比@max-pleaner的更干净、更快。
我不知道为什么,但我没有示例化额外的Hash对象:

def dot_access(hash)
  hash.each_with_object(OpenStruct.new) do |(key, value), struct|
    struct[key] = value.is_a?(Hash) ? dot_access(value) : value
  end
end

以下是供您参考的基准:
第一个

qcuzuvrc

qcuzuvrc7#

这里有一种重写初始化器的方法,这样你就可以执行OpenStruct.new({ a: "b", c: { d: "e", f: ["g", "h", "i"] }})
此外,当您使用require 'json'时,这个类也包含在其中,因此请确保在使用require后执行此修补程序。

class OpenStruct
  def initialize(hash = nil)
    @table = {}
    if hash
      hash.each_pair do |k, v|
        self[k] = v.is_a?(Hash) ? OpenStruct.new(v) : v
      end
    end
  end

  def keys
    @table.keys.map{|k| k.to_s}
  end
end
xtfmy6hx

xtfmy6hx8#

Basing a conversion on OpenStruct works fine until it doesn't. For instance, none of the other answers here properly handle these simple hashes:

people = { person1: { display: { first: 'John' } } }
creds = { oauth: { trust: true }, basic: { trust: false } }

The method below works with those hashes, modifying the input hash rather than returning a new object.

def add_indifferent_access!(hash)
  hash.each_pair do |k, v|
    hash.instance_variable_set("@#{k}", v.tap { |v| send(__method__, v) if v.is_a?(Hash) } )
    hash.define_singleton_method(k, proc { hash.instance_variable_get("@#{k}") } )
  end
end

then

add_indifferent_access!(people)
people.person1.display.first # => 'John'

Or if your context calls for a more inline call structure:

creds.yield_self(&method(:add_indifferent_access!)).oauth.trust # => true

Alternatively, you could mix it in:

module HashExtension
  def very_indifferent_access!
    each_pair do |k, v|
      instance_variable_set("@#{k}", v.tap { |v| v.extend(HashExtension) && v.send(__method__) if v.is_a?(Hash) } )
      define_singleton_method(k, proc { self.instance_variable_get("@#{k}") } )
    end
  end
end

and apply to individual hashes:

favs = { song1: { title: 'John and Marsha', author: 'Stan Freberg' } }
favs.extend(HashExtension).very_indifferent_access!
favs.song1.title

Here is a variation for monkey-patching Hash, should you opt to do so:

class Hash
  def with_very_indifferent_access!
    each_pair do |k, v|
      instance_variable_set("@#{k}", v.tap { |v| v.send(__method__) if v.is_a?(Hash) } )
      define_singleton_method(k, proc { instance_variable_get("@#{k}") } )
    end
  end
end

# Note the omission of "v.extend(HashExtension)" vs. the mix-in variation.

Comments to other answers expressed a desire to retain class types. This solution accommodates that.

people = { person1: { created_at: Time.now } }
people.with_very_indifferent_access!
people.person1.created_at.class # => Time

Whatever solution you choose, I recommend testing with this hash:
people = { person1: { display: { first: 'John' } }, person2: { display: { last: 'Jingleheimer' } } }

gtlvzcf8

gtlvzcf89#

如果您可以对Hash类进行monkey-patching,那么您可以:

require 'ostruct'

module Structurizable
  def each_pair(&block)
    each do |k, v|
      v = OpenStruct.new(v) if v.is_a? Hash

      yield k, v
    end
  end
end

Hash.prepend Structurizable

people = { person1: { display: { first: 'John' } }, person2: { display: { last: 'Jingleheimer' } } }

puts OpenStruct.new(people).person1.display.first

理想情况下,我们应该能够使用Refinement,而不是假装这样,但是由于某种原因,我不明白它对each_pair方法不起作用(而且,不幸的是,Refinement仍然非常有限)

相关问题