无法使用点语法访问 Ruby 哈希表

62

我正在使用net/http从Yahoo Placemaker API获取一些JSON数据。收到响应后,我对响应执行JSON.parse操作。这会给我一个类似于哈希的东西:

{"processingTime"=>"0.001493", "version"=>"1.4.0.526 build 111113", "documentLength"=>"25", "document"=>{"administrativeScope"=>{"woeId"=>"2503863", "type"=>"Town", "name"=>"Tampa, FL, US", "centroid"=>{"latitude"=>"27.9465", "longitude"=>"-82.4593"}}, "geographicScope"=>{"woeId"=>"2503863", "type"=>"Town", "name"=>"Tampa, FL, US", "centroid"=>{"latitude"=>"27.9465", "longitude"=>"-82.4593"}}, "localScopes"=>{"localScope"=>{"woeId"=>"2503863", "type"=>"Town", "name"=>"Tampa, FL, US (Town)", "centroid"=>{"latitude"=>"27.9465", "longitude"=>"-82.4593"}, "southWest"=>{"latitude"=>"27.8132", "longitude"=>"-82.6489"}, "northEast"=>{"latitude"=>"28.1714", "longitude"=>"-82.2539"}, "ancestors"=>[{"ancestor"=>{"woeId"=>"12587831", "type"=>"County", "name"=>"Hillsborough"}}, {"ancestor"=>{"woeId"=>"2347568", "type"=>"State", "name"=>"Florida"}}, {"ancestor"=>{"woeId"=>"23424977", "type"=>"Country", "name"=>"United States"}}]}}, "extents"=>{"center"=>{"latitude"=>"27.9465", "longitude"=>"-82.4593"}, "southWest"=>{"latitude"=>"27.8132", "longitude"=>"-82.6489"}, "northEast"=>{"latitude"=>"28.1714", "longitude"=>"-82.2539"}}, "placeDetails"=>{"placeId"=>"1", "place"=>{"woeId"=>"2503863", "type"=>"Town", "name"=>"Tampa, FL, US", "centroid"=>{"latitude"=>"27.9465", "longitude"=>"-82.4593"}}, "placeReferenceIds"=>"1", "matchType"=>"0", "weight"=>"1", "confidence"=>"8"}, "referenceList"=>{"reference"=>{"woeIds"=>"2503863", "placeReferenceId"=>"1", "placeIds"=>"1", "start"=>"15", "end"=>"20", "isPlaintextMarker"=>"1", "text"=>"Tampa", "type"=>"plaintext", "xpath"=>""}}}}

我可以通过 jsonResponse ['version'] 等方式访问元素,但无法执行 jsonResponse.version。为什么会这样?


我认为 Ruby 不支持那个。 - Rishav Rastogi
10个回答

116

Hash 的键名不能使用点语法。而 OpenStruct 可以:

require 'ostruct'
hash = {:name => 'John'}
os = OpenStruct.new(hash)
p os.name #=> "John"

注意:不适用于嵌套哈希。


1
仅适用于一个点的级别,os.name.firstname似乎无法工作,os.name返回一个普通的哈希值。 - teknopaul
1
这对于嵌套的哈希和数组不起作用。请参见下面whodabudda的答案。 - B Seven

50

OpenStruct适用于纯哈希,但对于嵌套数组或其他哈希的哈希,点语法将会失败。我发现了这个解决方案,它可以很好地工作而不需要加载其他宝石: https://coderwall.com/p/74rajw/convert-a-complex-nested-hash-to-an-object 基本步骤如下:

data = YAML::load(File.open("your yaml file"))
json_data = data.to_json
mystr = JSON.parse(json_data,object_class: OpenStruct)

现在你可以使用点符号语法访问mystr中的所有对象。


3
因为JSON和OpenStruct都在Ruby标准库中,所以我们不需要第三方依赖。 - Eduardo Santana

17

原生的 Ruby 哈希表无法像这样工作,但是HashDot gem 可以胜任。

HashDot 允许在哈希表上使用点符号语法。它还可以用于已重新解析为 JSON.parse 的 JSON 字符串。

require 'hash_dot'

hash = {b: {c: {d: 1}}}.to_dot
hash.b.c.d => 1

json_hash = JSON.parse(hash.to_json)
json_hash.b.c.d => 1

不错的宝石!谢谢! - Richard

11

如果您不想安装任何宝石(gem),可以尝试使用Ruby的本地Struct类和一些Ruby技巧,例如splat运算符

# regular hashes
customer = { name: "Maria", age: 21, country: "Brazil" }
customer.name
# => NoMethodError: undefined method `name' for {:name=>"Maria", :age=>21, :country=>"Brazil"}:Hash

# converting a hash to a struct
customer_on_steroids = Struct.new(*customer.keys).new(*customer.values)
customer_on_steroids.name
#=> "Maria"
请注意,这个简单的解决方案仅适用于单层哈希。为了使其动态并完全适用于任何类型的Hash,您需要将其递归以在结构体内创建子结构体。
您还可以将结构体存储为类。
customer_1 = { name: "Maria", age: 21, country: "Brazil" }
customer_2 = { name: "João",  age: 32, country: "Brazil" }
customer_3 = { name: "José",  age: 43, country: "Brazil" }

Customer = Struct.new(*customer_1.keys)
customer_on_steroids_1 = Customer.new(*customer_1.values)
customer_on_steroids_2 = Customer.new(*customer_2.values) 
customer_on_steroids_3 = Customer.new(*customer_3.values)

了解有关Ruby Struct类的更多信息。


我正在寻找与C#匿名类型类似的东西,这个很好用。 - ZX9

9

为什么不呢?您可以通过元编程来实现。

module LookLikeJSON
  def method_missing(meth, *args, &block)
    if has_key?(meth.to_s)
      self[meth.to_s]
    else
      raise NoMethodError, 'undefined method #{meth} for #{self}' 
    end
  end
end

h = {"processingTime"=>"0.001493", "version"=>"1.4.0.526 build 111113", "documentLength"=>"25"}
h.extend(LookLikeJSON)
h.processingTime #=> "0.001493"

在单引号中插值字符串?: / 并使用 stringify_keys! 先规范化键。 - Yuanfei Zhu
1
也许这个更简短、单行的版本是:Hash.define_method(:method_missing) { |*args| m = args.first; fetch(m, nil) || fetch(m.to_s, nil) || super(*args) } - Henry Blyth

6
那是JavaScript的特性,而不是Ruby的特性。在Ruby中,要使用“点语法”,对象需要响应相应的方法。Ruby哈希使用#[](key) 方法来访问元素。

4

我会借鉴@whodabudda的一句话来回答这个问题:

# example hash
hash = { some: [ {very: :deep}, {very: :nested}, {very: :hash} ] }

#one-liner to convert deep open
deep_open = JSON.parse(hash.to_json, object_class: OpenStruct)

#now you can do this sorcery!
deep_open.some.map(&:very)
=> ["deep", "nested", "hash"]


0

如果是在 Rspec 中,桩也可以起作用。

let(:item) { stub(current: 1, total: 1) } 

0
如果往返JSON似乎太过繁琐:
# Constructor like OpenStruct, but not open; and deep, not shallow
# Example: hash = { a: 1, b: { c: 2, d: 3 } }
#          obj = DeepStruct.new(hash)
#          obj.a # read
#          obj.b.c += 1 # read and write 
#          obj.x # error
class DeepStruct
  def initialize(hash)
    hash.each do |k, v|
      self.class.send(:attr_accessor, k)
      instance_variable_set("@#{k}", v.is_a?(Hash) ? DeepStruct.new(v) : v)
    end
  end
end

-1

因为Hash没有version方法。


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