Overriding attribute accessors on Ruby on Rails 4 - ruby-on-rails

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 :[], []=

Related

Ruby Mixin module not saving Active Record property

I have a eBook resource with a value property:
class EBook < ApplicationRecord
include Mixin
end
and a module:
module Mixin
extend ActiveSupport::Concern
included do
# validations
belongs_to :user
end
def change_value
#value = 200
end
end
I would like to be able to call EBook.change_value and have that instance's value set to 200. How can I do this? Is this an antipattern? I can't seem to find anything that will allow me to change instance state through a module.
Using the rails console I get this output:
EBook Load (0.3ms) SELECT `e_books`.* FROM `e_books` ORDER BY `e_books`.`id` ASC LIMIT 1 OFFSET 1
=> 200
but it doesn't update or save the model.
ActiveRecord does not use separate instance variables for attributes represented in database.
Change your method to
def change_value
self.value = 200
end
in order to use setter method generated by ActiveRecord for your model.
In order to clear it up a bit more, this is what your code did:
class Ebook < ApplicationRecord
attr_reader :value
def change_value
#value = 200
end
end
2.5.1 :001 > e = Ebook.new
=> #<Ebook id: nil, value: nil>
2.5.1 :002 > e.change_value # this sets your instance_variable
=> 200
2.5.1 :003 > e
=> #<Ebook id: nil, value: nil> # ActiveRecord's value remain nil
2.5.1 :004 > e.value # reads from instance variable as we've overwritten the method with attr_reader
=> 200
2.5.1 :005 > e.read_attribute(:value) # reads from ActiveRecord's attributes
=> nil
2.5.1 :006 > e.tap(&:save)
=> #<Ebook id: 3, value: nil> # as expected, nothing is saved

Ruby: Can you determine if an object is having one of its methods called?

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"

How to reuse symbols from a hash in another method in Ruby

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

Array to Hash with conditional logic in Ruby?

ruby 2.1.1
Is there a way to do the logic in this piece of code in one line or a more concise manner?
user = User.new
h = Hash.new
attrs = [:name, :foo, :bar]
attrs.each do |a|
h[a] = user[a] if user.has_attribute? a
end
return h
If you're using Rails and User is an ActiveRecord model (which it looks like given your use of has_attribute?) then this will do the same thing:
user = User.new
...
return user.attributes.slice("name", "foo", "bar")
Or, if you really want symbols:
return user.attributes.with_indifferent_access.slice(:name, :foo, :bar)
It seems you are on Rails. If so,then -
attrs = [:name, :foo, :bar]
# the result hash will be returned, if last line of the method.
user.attributes.extract!(*attrs)
Look these methods extract! and attributes.
Example :
arup#linux-wzza:~/Rails/app> rails c
Loading development environment (Rails 4.1.1)
2.0.0-p451 :001 > h = { a: 1, b: 2, c: 3, d: 4 }
=> {:a=>1, :b=>2, :c=>3, :d=>4}
2.0.0-p451 :002 > h.extract!(:a ,:b ,:x)
=> {:a=>1, :b=>2}
2.0.0-p451 :003 >
Answers above are correct in Rails scope, I'l just add generic solution:
# assuming user[a] returns nil, if user have no a attribute
[:name, :foo, :bar].
map{|a| [attr, user[a]]}.
reject{|k, v| v.nil?}.
to_h
# assuming user[a] can raise if not user.has_attribute?(a)
[:name, :foo, :bar].
map{|a| [attr, user.has_attribute?(a) && user[a]]}.
reject{|k, v| !v}.
to_h
I've formatted them as NOT one-liners, but they are still one-statements :)
Basically, the trick is "invent the right method chain to convert one sequence to other", and requires to know all Enumerable sequence-transforming methods (map/select/reduce/reject/...), as well as a method to transform array of key-value pairs into hash (#to_h is standard in Ruby 2.1.1)

ActiveRecord derived attribute persistence which depends on id value

How do you persist a derived attribute which depends on the value of id in rails? The snippet below seems to work-- Is there a better rails way?
class Model < ActiveRecord::Base
....
def save
super
#derived_attr column exists in DB
self.derived_attr = compute_attr(self.id)
super
end
end
Callbacks are provided so you should never have to override save. The before_save call in the following code is functionally equivalent to all the code in the question.
I've made set_virtual_attr public so that it can be calculated as needed.
class Model < ActiveRecord::Base
...
# this one line is functionally equivalent to the code in the OP.
before_save :set_virtual_attr
attr_reader :virtual_attr
def set_virtual_attr
self.virtual_attr = compute_attr(self.id)
end
private
def compute_attr
...
end
end
I think the more accepted way to do this would be to provide a custom setter for the virtual attribute and then provide an after_create hook to set the value after the record is created.
The following code should do what you want.
class Virt < ActiveRecord::Base
def after_create()
self.virtual_attr = nil # Set it to anything just to invoke the setter
save # Saving will not invoke this callback again as the record exists
# Do NOT try this in after_save or you will get a Stack Overflow
end
def virtual_attr=(value)
write_attribute(:virtual_attr, "ID: #{self.id} #{value}")
end
end
Running this in the console shows the following
v=Virt.new
=> #<Virt id: nil, virtual_attr: nil, created_at: nil, updated_at: nil>
>> v.save
=> true
>> v
=> #<Virt id: 8, virtual_attr: "ID: 8 ", created_at: "2009-12-23 09:25:17",
updated_at: "2009-12-23 09:25:17">
>> Virt.last
=> #<Virt id: 8, virtual_attr: "ID: 8 ", created_at: "2009-12-23 09:25:17",
updated_at: "2009-12-23 09:25:17">

Resources