2

I'm not sure if I'm even asking the right question. I may be approaching the problem incorrectly, but basically I have this situation here:

obj = get_user(params)
obj.profile => {:name => "John D", :age => 40, :sex => "male"} #Has to be of class Hash
obj.profile.name => "John D"
obj.profile[:name] => "John D"
obj.profile.job => nil

So basically, I have to satisfy all of these conditions and I'm not sure exactly how to even approach this (I just learned Ruby today).

Note the dot notation for accessing the inner variables, otherwise I would have just had profile be a hash of symbols. So I've tried two methods, which only sort of get me there

Method 1: Make profile an OpenStruct

So this allows me to access name, age and sex using the dot notation, and it automatically returns nil if a key doesn't exist, however obj.profile is of the type OpenStruct instead of Hash

Method 2: Make profile its own class

With this I set them as instance variables, and I can use method_missing to return nil if they don't exist. But, I again run into the issue of obj.profile not being the correct type/class

Is there something I'm missing? Is there a way to maybe differentiate between

obj.profile
obj.profile.name

in the getter function and return either a hash or otherwise?

Can I change what is returned by my custom class for profile, so it returns a Hash instead?

I've even tried checking the args and **kwargs in the get function for obj.profile and neither of them seem to help, or populate if I call obj.profile.something

user1561753
  • 357
  • 2
  • 3
  • 13
  • I would argue that `obj.profile => {:name => "John D", :age => 40, :sex => "male"}` only means that it has to return a representation that looks like the representation of a hash. That does not necessarily mean that it has to be an instance of Hash. Would it be wrong to interpret the expectations in that way? – spickermann Feb 20 '15 at 06:07
  • The prompt said "return a symbolized hash", and OpenStruct can be accessed in all the exact same ways. The only difference is that it's class isn't the same...I'll see if the way I interpreted it is possible. If not, then I'll use use OpenStruct – user1561753 Feb 20 '15 at 06:13

3 Answers3

1

If it absolutely has to be a Hash:

require 'pp'

module JSHash
  refine Hash do
    def method_missing(name, *args, &block)
      if !args.empty? || block
        super(name, *args, &block)
      else
        self[name]
      end
    end
  end
end

using JSHash

profile = {:name => "John D", :age => 40, :sex => "male"}

pp profile.name    # "John D"
pp profile[:name]  # "John D"
pp profile.job     # nil
pp profile.class   # Hash

But still better not to be a Hash, unless it absolutely needs to:

require 'pp'

class Profile < Hash
  def initialize(hash)
    self.merge!(hash)
  end
  def method_missing(name, *args, &block)
    if !args.empty? || block
      super(name, *args, &block)
    else
      self[name]
    end
  end
end

profile = Profile.new({:name => "John D", :age => 40, :sex => "male"})

pp profile.name
pp profile[:name]
pp profile.job
Amadan
  • 191,408
  • 23
  • 240
  • 301
  • [`merge!`](http://ruby-doc.org//core-1.9.3/Hash.html#method-i-merge-21) is the destructive `merge`: the latter returns the result without affecting the receiver (or the arguments), the former changes the receiver. In most cases, `a.merge!(b)` is equivalent to `a = a.merge(b)`, but this changes the identity of `a`; and obviously changing the identity of `self` is not going to work. – Amadan Feb 20 '15 at 06:30
1

For only a few hash keys, you can easily define singleton methods like so:

def define_getters(hash)
  hash.instance_eval do
    def name
      get_val(__method__)
    end

    def job
      get_val(__method__)
    end

    def get_val(key)
      self[key.to_sym]
    end
  end
end

profile = person.profile #=> {name: "John Doe", age: 40, gender: "M"}
define_getters(profile)

person.profile.name #=> "John Doe"
person.profile.job #=> nil

Reflects changed values as well (in case you were wondering):

person.profile[:name] = "Ralph Lauren"
person.profile.name #=> "Ralph Lauren"

With this approach, you won't have to override method_missing, create new classes inheriting from Hash, or monkey-patch the Hash class.

However, to be able to access unknown keys through method-calls and return nil instead of errors, you'll have to involve method_missing.

SHS
  • 7,651
  • 3
  • 18
  • 28
1

This Hash override will accomplish what you're trying to do. All you need to do is include it with one of your class files that you're already loading.

class Hash
  def method_missing(*args)
    if args.size == 1  
      self[args[0].to_sym]
    else
      self[args[0][0..-2].to_sym] = args[1] # last char is chopped because the equal sign is included in the string, print out args[0] to see for yourself
    end
  end
end

See the following IRB output to confirm:

1.9.3-p194 :001 > test_hash = {test: "testing"}
 => {:test=>"testing"} 
1.9.3-p194 :002 > test_hash.test
 => "testing" 
1.9.3-p194 :003 > test_hash[:test]
 => "testing" 
1.9.3-p194 :004 > test_hash.should_return_nil
 => nil 
1.9.3-p194 :005 > test_hash.test = "hello"
 => "hello" 
1.9.3-p194 :006 > test_hash[:test]
 => "hello" 
1.9.3-p194 :007 > test_hash[:test] = "success"
 => "success" 
1.9.3-p194 :008 > test_hash.test
 => "success" 
1.9.3-p194 :009 > test_hash.some_new_key = "some value"
 => "some value" 
1.9.3-p194 :011 > test_hash[:some_new_key]
 => "some value" 
Mike S
  • 11,329
  • 6
  • 41
  • 76