I have the following method that defines a hash with a number of keys (there are a lot, I just cut it down for this example).
def data
#data ||= {
name: "Some Name",
email: "my#email.com"
}
end
Now, each of those keys I want to use in another method within the same class like so:
[:name, :email].each { |key| define_method("get_#{key}") { data[key] } }
While this works as it should, it doesn't seem to be a very good idea to hardcode the keys - I'd would much rather make them dynamic and have them reused from the hash I created within the first method. Since I am calling upon an Instance of this Class from another Class I get the following error when using the obvious approach:
data.keys.each { |key| define_method("get_#{key}") { data[key] } }
# => undefined local variable or method `data' for #<Class:0x0000000dc55938>
Any ideas how this could be solved?
As both methods are in same class why not use data instead of #data
2.1.2 :001 > def data
2.1.2 :002?> #data ||= {
2.1.2 :003 > name: "Some Name",
2.1.2 :004 > email: "my#email.com"
2.1.2 :005?> }
2.1.2 :006?> end
=> :data
2.1.2 :007 > data.keys.each { |key| define_method("get_#{key}") { data[key] } }
=> [:name, :email]
2.1.2 :008 > get_name
=> "Some Name"
2.1.2 :009 >
You can define method like data_keys. Use this method outside of your class and get keys.
class YourClass
def self.data
...
end
def self.data_keys
#data_keys ||= data.keys
end
end
YourClass.data_keys.each { |key| define_method("get_#{key}") { YourClass.data[key] } }
you can try something like this:--
def data(*args)
options = args.extract_options!
options ||= {
name: "Some Name",
email: "my#email.com"
}
##save it to instance variable for further use
#data=options
##either pass to another method
other_method(options)
##or call any other method to call private methods as well using send
new_obj =OtherObject.new
new_obj.send(:method_name,options)
end
same solution ,,using class variable
def data
##data ||= {
name: "Some Name",
email: "my#email.com"
}
end
in this way..you can even access data in model
def get_data
## ##data is available
end
If i'm understanding your requirements correctly, you want to have methods on each instance of the class that map (with a get/set prefix) to the keys in the data hash?
Although I despise magical logic in classes, could you not just define #method_missing and handle getting the value from the data hash based on that?
def method_missing method_symbol, *args, &block
if method_symbol.to_s.match(/^get_(.+)$/) && data.keys.include?($1.to_sym)
# Using the matching key in the data hash
data[$1.to_sym]
else
# Cannot detect method for current class, bubble method_missing to super class
super
end
end
It should also be noted that when overrideing method missing, you should always override #respond_to? as well
Related
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
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
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.
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"
I am following instructions from:
http://cloudspace.com/blog/2013/10/18/extending-faker/#.VLdumx9sY8o
My /config/locales/faker.en.yml looks like:
en:
faker:
girls:
first_name: ["priyanka", "Tanya", "aditi", "Tanvi"]
last_name: ["Acharya", "Agarwal", "Agate", "Aggarwal"]
name:
- "#{first_name} #{last_name}"
And I have following: /lib/faker/girls.rb looks like:
module Faker
class Girl < Base
class << self
def first_name
parse('girls.first_name')
end
def last_name
parse('girls.last_name')
end
def name
fetch('girls.name')
end
end
end
end
Right after starting rails console I run: require Rails.root.join 'lib/faker/girls' to which a true is returned.
After that running following commands do not work as expected.
Output:
2.1.1 :004 > Faker::Girl.first_name => ""
2.1.1 :005 > Faker::Girl.last_name => ""
2.1.1 :006 > Faker::Girl.name => "\#{first_name} \#{last_name}"
Please help me find where I went wrong..
You mixed parse and fetch up: simple properties are to be fetched while composed are to be parsed. Another glitch is that your class name should correspond the yml (by convention):
# ⇓
class Girls < Base
class << self
def first_name
#⇓⇓⇓⇓⇓ it is a simple property
fetch('girls.first_name')
end
def last_name
#⇓⇓⇓⇓⇓ it is a simple property
fetch('girls.last_name')
end
def name
#⇓⇓⇓⇓⇓ it is a composed property
parse('girls.name')
end
...
Hope it helps.
I defined a class for User, which takes a hash as parameter,
class User
attr_accessor :name, :email
def initialize(attributes = {})
#name = attributes[:name]
#email = attributes[:email]
end
def formatted_email
"#{#name} <#{#email}>"
end
end
While initializing in console using parenthesis and empty space syntax for parameter, the behaviours are different, the second one didn't receive the attributes, but if I define a hash first and then initialize with the hash as the parameter, it works well now, why does this happen?
2.0.0-p451 :013 > example = User.new(name:'John', email:'john#example.com')
=> #<User:0x007fbcdb57e140 #name="John", #email="john#example.com">
2.0.0-p451 :014 > example = User.new {name:'John', email:'john#example.com'}
=> #<User:0x007fbcdb599080 #name=nil, #email=nil>
2.0.0-p451 :021 > hash = {name: 'John', email: 'john#example.com'}
=> {:name=>"John", :email=>"john#example.com"}
2.0.0-p451 :022 > User.new hash
=> #<User:0x007fbcdc4af8a8 #name="John", #email="john#example.com">
Look the code :
class User
def initialize(attributes = {})
p attributes # I put it to debug the hash **attributes** after each *new* call.
#name = attributes[:name]
#email = attributes[:email]
end
def formatted_email
"#{#name} <#{#email}>"
end
end
# here you did pass the method argument as a Hash.
User.new(name:'John', email:'john#example.com')
# >> {:name=>"John", :email=>"john#example.com"} # output of attributes hash
# you got #<User:0x007fbcdb57e140 #name="John", #email="john#example.com">, as the
# hash **attributes** is, {:name=>"John", :email=>"john#example.com"}. So inside the
# initialize method
# #name = "John" as , attributes[:name] is "John"
# #email = "john#example.com" as, attributes[:email] is "john#example.com"
# here you didn't pass the method argument as a Hash, rather a block.
User.new {name:'John', email:'john#example.com'}
# >> {} # output of attributes hash is empty hash.
# you got #<User:0x007fbcdb599080 #name=nil, #email=nil>, as *attributes* is empty.
# so inside the initialize method #name = nil happened, as attributes[:name] is nil.
# #email = nil, as attributes[:email] is nil.
# here you did pass the method argument as a Hash.
hash = {name: 'John', email: 'john#example.com'}
User.new hash # output of attributes hash
# >> {:name=>"John", :email=>"john#example.com"}
In Ruby, the method calling syntax is
meth_name(argument_list_of_object/objects) { #block }
meth_name(argument_list_of_object/objects)
meth_name argument_list_of_object/objects. This is valid when you are not passing block,otherwise error will be thorwn
It means in Ruby, { .. } after method call, parsed as if, you are sending a block to the method, not as if you are passing the hash as argument.
One more simple example to illustrate this theory :
def foo(arg = 10)
p arg
end
foo 12 # => 12
foo(12) # => 12
foo { 12 } # => 10 default value
foo "arg" { 12 }
# ~> -:8: syntax error, unexpected '{', expecting end-of-input
# ~> foo "arg" { 12 }
# ~>
Summarizing :
# you are passing a 2 key/value argument, which in runtime will create a hash
# {name:'John', email:'john#example.com'}, then will be assigned to
# local variable attributes of the method initialize.
User.new name:'John', email:'john#example.com'
# => #<User:0x90f5a24 #email="john#example.com", #name="John">
# same as above
User.new(name:'John', email:'john#example.com')
# => #<User:0x90f4a5c #email="john#example.com", #name="John">
# simply passing a block to the method.
User.new {name:'John', email:'john#example.com'}
# => #<User:0x90f7450 #email=nil, #name=nil>
Hope this helps.
This is due to the empty space, following should print correct, I have removed white space and added parentheses
example = User.new({name: 'John', email: 'john#example.com'})
I have a Restriction model that represent a formula. The field formula is a string that at runtime is evaluated. This field can't be accessed from outside for security reasons so I'am trying to override the accessors.
class Restriction < ActiveRecord::Base
belongs_to :indicator
RESTRICTION_TYPES = {
less_than: "IND<X",
greater_than: "X<IND",
between: "X<IND && IND<Y"
}
def evaluate(number)
f = formula.gsub("IND", "#{number}")
eval(f)
end
def create_formula(type_name, arg)
if(type_name == :between)
f = RESTRICTION_TYPES[:between].gsub("X", "#{arg[0]}").gsub("Y", "#{arg[1]}")
else
f = RESTRICTION_TYPES[type_name].gsub("X", "#{arg}")
end
formula = f
end
private
def formula= f
write_attribute(:formula, f)
end
def formula
read_attribute(:formula)
end
def [](value)
super[value]
end
def []=(key, value)
super[key] = value
end
end
In rails console:
Loading development environment (Rails 4.0.0)
2.0.0p247 :001 > r = Restriction.new
=> #<Restriction id: nil, formula: nil, indicator_id: nil, created_at: nil, updated_at: nil, state: nil>
2.0.0p247 :002 > r.create_formula(:between, [1,2])
=> "1<IND && IND<2"
2.0.0p247 :003 > r.evaluate 1.5
NoMethodError: undefined method 'gsub' for nil:NilClass
2.0.0p247 :004 > r
=> #<Restriction id: nil, formula: nil>
formula is not change the value. What am I doing wrong?
PS: You can see that I have also overriden [](value) and []=(key, value). This is due to my previous question
Rails internally relies on the hash like access methods for reading and writing attributes. By making them private you wanted to restrict the access of [] and []= to from within the object only. But you destroyed the method, because you are using super the wrong way. When calling super you are not getting the super object of this class, but the super method, the current method overrides. Thus change it like this and it should work:
def [](value)
super(value)
end
def []=(key, value)
super(key, value)
end
P.S.: Overriding a method for declaring it private is overkill in Ruby. You can simply declare it private with
private :[], []=
I would like to do something like this:
require 'json'
class Person
attr_accessor :fname, :lname
end
p = Person.new
p.fname = "Mike"
p.lname = "Smith"
p.to_json
Is it possible?
Yes, you can do it with to_json.
You may need to require 'json' if you're not running Rails.
To make your Ruby class JSON-friendly without touching Rails, you'd define two methods:
to_json, which returns a JSON object
as_json, which returns a hash representation of the object
When your object responds properly to both to_json and as_json, it can behave properly even when it is nested deep inside other standard classes like Array and/or Hash:
#!/usr/bin/env ruby
require 'json'
class Person
attr_accessor :fname, :lname
def as_json(options={})
{
fname: #fname,
lname: #lname
}
end
def to_json(*options)
as_json(*options).to_json(*options)
end
end
p = Person.new
p.fname = "Mike"
p.lname = "Smith"
# case 1
puts p.to_json # output: {"fname":"Mike","lname":"Smith"}
# case 2
puts [p].to_json # output: [{"fname":"Mike","lname":"Smith"}]
# case 3
h = {:some_key => p}
puts h.to_json # output: {"some_key":{"fname":"Mike","lname":"Smith"}}
puts JSON.pretty_generate(h) # output
# {
# "some_key": {
# "fname": "Mike",
# "lname": "Smith"
# }
# }
Also see "Using custom to_json method in nested objects".
Try it. If you're using Ruby on Rails (and the tags say you are), I think this exact code should work already, without requiring anything.
Rails supports JSON output from controllers, so it already pulls in all of the JSON serialization code that you will ever need. If you're planning to output this data through a controller, you might be able to save time by just writing
render :json => #person