ハッシュキーをメソッド名(プロパティ)としてハッシュにアクセス(読出、そして代入も)するには

たしか

メタプログラミングRuby

メタプログラミングRuby

に載ってたよなー、ActiveRecordで使ってるよーとか書いてたよなー、でもどうだったか思い出せないなー。
ということで検索してみて、やっと見つかりました。

class Hash
  def method_missing(name)
    return self[name] if key? name
    self.each { |k,v| return v if k.to_s.to_sym == name }
    super.method_missing name
  end
end

http://www.goodercode.com/wp/convert-your-hash-keys-to-object-properties-in-ruby/

method_missingの引数は Symbol なので、ハッシュキーが Symbol のものを優先して返します。
ハッシュキーが Symbol じゃない場合には、要素全てをループしてキーを探していますね。
そういえば、このエントリのタイトルで「メソッド名」と書きましたが、JavaScript的に言えばプロパティですかね。

キーをString限定に

ですが、例えば JSON.parse で返されたハッシュだと、だいたいのキーは String だと思います。なので、

class Hash
  def method_missing(name)
    return self[name.to_s] if self.has_key?(name.to_s)
    super.method_missing(name)
  end
end

でも良いかと(ついでにkey?をself.has_key?にしたのは私の趣味)。
キーの種類を String に決め打ちしてますので、キーが Symbol だと使えません。(Symbol キー版はもう少し下をご覧ください)

代入も('14/3/25追記)

下にありますが
http://enterpriserails.rubyforge.org/hash_extension/
スクリプトでは代入も出来ます。のでそれに倣って、代入も出来るようにしました。
もちろん、キーは String 限定です。

class Hash
  def method_missing(name, *params)
    name_str = name.to_s
    # substitution
    if name_str[-1] == "="
      self[name_str[0..-2]] = params.first
    # self has name as a key?
    elsif self.has_key?(name_str)
      self[name_str] 
    # super
    else
      super.method_missing(name, *params)
    end
  end
end

Symbol 優先で代入も出来るように('14/3/25追記)

JSON相手だから String 優先で良いよな、と思っていたのですが、最近わたしがよく使っているTwitter gemだと、#to_h または #attrs(おなじものです)で返されてくるハッシュのキーが Symbol。なので Symbol キー優先版を書いてみました。上げておきます。
代入もキーが Symbol のものを使うようにしました。

class Hash
  def method_missing(name, *params)
    name_str = name.to_s
    # substitution
    if name_str[-1] == "="
      self[name_str[0..-2].to_sym] = params.first
    # self has name as a key?
    elsif self.has_key?(name) # name.class == Symbol
      self[name]
    elsif self.has_key?(name_str) # name.class == String
      self[name_str]
    # super
    else
      super.method_missing(name, *params)
    end
  end
end

おまけ:訳してみた

ついでに、この記事の興味ある部分を超訳*1してみました。

XMLJSONデータをたくさん使うソフトを書いてるんだけど、データを表示させるときに book[‘author’][‘first_name’] じゃなくて book.author.first_name と呼びたいんだよね。でググったらこういう記事を見つけたんだ。ここに書き写しとくね。

class ::Hash
  # add keys to hash
  def to_obj
    self.each do |k,v|
      if v.kind_of? Hash
        v.to_obj
      end
      k=k.gsub(/\.|\s|-|\/|\'/, '_').downcase.to_sym
      ## create and initialize an instance variable for this key/value pair
      self.instance_variable_set("@#{k}", v)
      ## create the getter that returns the instance variable
      self.class.send(:define_method, k, proc{self.instance_variable_get("@#{k}")})
      ## create the setter that sets the instance variable
      self.class.send(:define_method, "#{k}=", proc{|v| self.instance_variable_set("@#{k}", v)})
    end
    return self
  end
end

ちゃんと動くよ。キーをハッシュ呼び出しメソッドにしてくれてる。

でもさ、なんかイマイチなんだよね。何でって、キーの9割は多分ほとんど使わないよね。使わないメソッドのためにいちいちメソッド作るのって手間掛かるだけじゃないの?

ということで魔法のメソッド method_missing を使ってみた。

class ::Hash
  def method_missing(name)
    return self[name] if key? name
    self.each { |k,v| return v if k.to_s.to_sym == name }
    super.method_missing name
  end
end

僕の目的により合うメソッドになったね。シンプルだし、メソッド名のSymbolがハッシュキーの場合にはそのまま、ハッシュキーがStringとかその他の場合にはループでキーを探すんだよ。

  • #3 by Ajasja Ljubetič at May 5th, 2010

なんで OpenStruct 使わないの?

  • #4 by kerry at May 5th, 2010

知らなかったよw

  • #5 by kerry at May 5th, 2010

JSONフィードが元の、入れ子になったハッシュを扱うときに、OpenStructだと望み通りに行かないんだ。
例えば

my_hash = { :a => :b, :c => {:d => :e, :f => :g} }

ってハッシュをOpenStructで扱うと:eを呼ぶときに openstruct.c[:d] って書かなきゃいけない。僕はもっとシンプルに my_hash.c.d と書きたいのさ。

  • #10 by Dan at August 16th, 2011

hash_extensionも同じようなことをしてるよ。しかもセッターメソッドも付いてるよ。

*1:つまり、かなりてきとー