Creating attribute accessors with optional defaults - ruby-on-rails

I have a Rails application with a recurring need for setting default attributes. Sometimes the user will supply values for the attributes which will be respected, but in other circumstances either the model or the user might desire that these attributes are overridden with default values disregarding original values.
I guessed that this problem called for a banged (!) and non-banged method for setting default values allowing the user and program to switch to the appropriate state. The non-banged setter will only set default values when they are not nil whilst the banged version will always overwrite the attributes with defaults. The difference is minor:
class BangDiBang
attr_accessor :value
def set_default
self.value ||= do_some_suff_to_determine_default_value
end
def set_default!
self.value = do_some_suff_to_determine_default_value
end
...
end
The issue with this code is that if I had a bunch of variables to set, I would end up repeating the same code twice for each variable.
My question is how to partial out this code? Saving the logic in one method and having two methods set_value and set_value! calling the central logic with the different assignment operators.
I have conjured one solution: write the central logic as text, replace the assignment operation from the setter methods and evaluate (but this does not feel right). How do I not repeat myself?

The way you're going about this isn't going to work for multiple params since you're calling set_default but not specifying which variable. You'll want to define behavior for each variable, ideally. There are libraries that handle this sort of thing, but you can roll your own pretty easily:
class Example
def self.default_param(name, default_value)
define_method("default_#{name}") { default_value }
define_method(name) do
instance_variable_get("##{name}") || default_value
end
attr_writer name
end
default_param :foo, 'foo default'
end
ex = Example.new
ex.foo #=> "foo default"
ex.foo = 'bar'
ex.foo #=> "bar"
ex.default_foo #=> "foo default"
I've renamed set_default and set_default! to be more clear: for each variable with a default value, three methods are created (example using foo as the variable name):
foo — returns the value of #foo if it is truthy, and default_foo otherwise
foo= — sets the value of #foo
default_foo — returns the specified default
You could compartmentalize and dry up some of the code above further, creating a default_params (plural) method to take a hash, extracting the class macro to a concern:
module DefaultParams
def default_param(name, default_value)
define_method("default_#{name}") { default_value }
define_method(name) do
instance_variable_get("##{name}") || default_value
end
attr_writer name
end
def default_params(params)
params.each { |default| default_param(*default) }
end
end
class Example
extend DefaultParams
default_params foo: 'default foo', bar: 'my favorite bar'
end

I have implemented a solution with great inspiration from coreyward. I did not realize at first that rather than having two methods for setting an array of default values, I needed the default values separated out into single methods. This gives a great flexibility to the design of the application. So thanks a lot for the answer.
For the rails setup I’ve added to application_record.rb
def set_attr_from_defaults
default_attrs.each do |atr|
eval("self.#{atr[0..atr.length-9]} ||= self.#{atr}")
end
end
def set_attr_from_defaults!
default_attrs.each do |atr|
eval("self.#{atr[0..atr.length-9]} = self.#{atr}")
end
end
def set_default_attr params
params.each { |key, value| self.define_singleton_method(key){value} }
end
def default_attrs
self.attributes.keys.map{|i| (i+'_default').to_sym} & self.methods
end
default_attrs yields a list of symbols off default attributes. set_default_attr defines singleton methods, with intended use of parsing in a hash like attrname_default: 'default_value' .... The set_attr_from_defaults will set attributes from default values, when an attribute has a pair like var: var_default. The number 9 is the length of _default + 1.

Related

How to convert an Object to array in ruby

I have a class with (as example) 3 attributes ,I want to convert the class's attribute to an array ,so I can store them into my csv file.
class Human
attr_accessor :name,:Lname,:id
....
end
when I create :
human1=Human.new("nice","human",1)
I need a function that return ["nice","human",1].
Is there a predefined one that I didn't find or I have to redefine to_a so it does the job.
note: the class has more than 3 attribute
is there a function to go through the object attribute or not.
I need a function that return ["nice","human",1]
Creating such method is trivial. If it is specifically for CSV, I would name it accordingly, e.g.:
class Human
attr_accessor :name, :lname, :id
# ...
def to_csv
[name, lname, id]
end
end
To generate a CSV:
require 'csv'
human1 = Human.new("nice", "human", 1)
csv_string = CSV.generate do |csv|
csv << ['name', 'lname', 'id']
csv << human1.to_csv
end
puts csv_string
# name,lname,id
# nice,human,1
Note that I've renamed Lname to lname in the above example. Uppercase is reserved for constants.
is there a function to go through the object attribute or not?
No, there is no built in way to actually do what you want and you might be falling for a common beginner trap.
attr_accessor does not "define attributes" since Ruby doesn't actually have properties/attributes/members like other langauges do. It defines a setter and getter method for an instance variable. Ruby doesn't keep track of which properties an object is presumed to have - only the actual instance variables which have been set.
But Ruby does provide the basic building blocks to make any kind of attributes system you want. This is very simplefied (and quite rubbish) example:
class Human
# this is a class instance variable
#attributes = []
# a class method that we use for "defining attributes"
def self.attribute(name)
attr_accessor name
#attributes << name
end
attribute(:name)
attribute(:l_name)
attribute(:id)
def initialize(**kwargs)
kwargs.each {|k,v| send("#{k}=", v) }
end
# the attributes that are defined for this class
def self.attributes
#attributes
end
# cast a human to an array
def to_a
self.class.attributes.map{ |attr| send(attr) }
end
# cast a human to an hash
def to_h
self.class.attributes.each_with_object({}) do |attr, hash|
hash[attr] = send(attr)
end
end
end
jd = Human.new(
name: 'John',
l_name: 'Doe',
id: 1
)
jd.to_a # ['John', Doe, 1]
jd.to_h # {:name=>"John", :l_name=>"Doe", :id=>1}
Here we are creating a class method attribute that adds the names of the "attributes" to a class instance variable as they are declared. Thus the class "knows" what attributes it has. It then uses attr_accessor to create the setter and getter as usual.
When we are "extracting" the attributes (to_a and to_h) we use the list we have defined in the class to call each corresponding setter.
Usually this kind functionality would go into a module or a base class and not the actual classes that represent your buisness logic. For example Rails provides this kind of functionality through ActiveModel::Attributes and ActiveRecord::Attributes.

Is is possible to return virtual attributes automatically in Rails models?

Given:
class Foo
has_one :bar
def bar_name
bar.name
end
end
class Bar
belongs_to :foo
end
In the console or in a view, I can #foo.bar_name to get 'baz'.
I'm aware that I can #foo.as_json(methods: :bar_name) to get {"id"=>"abc123", "bar_name"=>"baz"}.
I could also denormalize the attribute and make it non-virtual, but I would rather not do that in this case.
Is it possible to automatically return the model with the virtual attribute included?
#<Foo id: "abc123", bar_name: "baz">
I want to do this because I am constructing a large object with nested collections of models, and the as_json call is abstracted away from me.
Not 100% sure I understand if your concern is related to as_json but if so this will work
class Foo
has_one :bar
def bar_name
bar.name
end
def as_json(options={})
super(options.merge!(methods: :bar_name))
end
end
Now a call to #foo.as_json will by default include the bar_name like your explicit example does.
Ugly would not recommend but you could change the inspection of foo e.g. #<Foo id: "abc123", bar_name: "baz"> as follows
class Foo
def inspect
base_string = "#<#{self.class.name}:#{self.object_id} "
fields = self.attributes.map {|k,v| "#{k}: #{v.inspect}"}
fields << "bar_name: #{self.bar_name.inspect}"
base_string << fields.join(", ") << ">"
end
end
Then the "inspection notation" would show that information although I am still unclear if this is your intention and if so why you would want this.
You could use attr_accessor - per the Rails docs:
Defines a named attribute for this module, where the name is symbol.id2name, creating an instance variable (#name) and a corresponding access method to read it. Also creates a method called name= to set the attribute.

Set dynamic values when generating setter methods using attr_accessor in ruby

Is there a better way to set values to setter methods when they are made dynamically using attr_accessor method? I need this for setting values for them from another model in rails. I'm trying to do something like below.
Model_class.all.each do |mdl|
attr_accessor(mdl.some_field)
end
Then I know that it creates a set of get and setter methods. What I want to do is, when these methods are get created, i want some value to be specified for setter method.Thanks in advance.
attr_accessor has no magic embedded. For each of params passed to it, it basically executes something like (the code is simplified and lacks necessary checks etc):
def attr_accessor(*vars)
vars.each do |var|
define_method var { instance_variable_get("##{var}") }
define_method "#{var}=" { |val| instance_variable_set("##{var}", val) }
end
end
That said, the attr_accessor :var1, :var2 DSL simply brings new 4 plain old good ruby methods. For what you are asking, one might take care about defining these methods (or some of them, or none,) themselves. For instance, for cumbersome setting with checks one might do:
attr_reader :variable # reader is reader, no magic
def variable=(val) do
raise ArgumentError, "You must be kidding" if val.nil?
#variable = val
end
The above is called as usual:
instance.variable = 42
#⇒ 42
instance.variable = nil
#⇒ ArgumentError: You must be kidding
Here is another possible implementation for this:
def attr_accessor(*args)
args.each do |attribute|
define_method(attribute.to_sym) { eval("##{attribute}") }
define_method((attribute.to_s + '=').to_sym) {|value| eval("##{attribute} = value") }
end
end

Variable available to class methods (within Concerns)

I created a Rails Concern for my ActiveModel called List
When I run Product.all from the Rails Console, I get:
NameError: undefined local variable or method `parameters' for
Product:Class
When parameters is changed to #parameters I get this error:
NoMethodError: undefined method `include?' for nil:NilClass
Possible Solutions
Which is better, using a constant PARAMETERS or ##Parameters? Pros and Cons?
Code
module List
extend ActiveSupport::Concern
require 'csv'
parameters = [
:visible,
:desc,
:value,
]
attr_accessor(*parameters)
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
module ClassMethods
def all
list = []
filename = File.join(Rails.root,"app/models/data/#{self.name.downcase}.csv")
CSV.foreach(filename, headers: true) do |row|
list << self.new(row.select{|key,_| parameters.include? key.to_sym })
end
return list
end
def visible
list = []
filename = File.join(Rails.root,"app/models/data/#{self.name.downcase}.csv")
CSV.foreach(filename, headers: true) do |row|
list << self.new(row.select{|key,_| parameters.include? key.to_sym }) if row['visible']=='1'
end
return list
end
end
end
For a quick solution, to make Product.all and Product.visible work with the least amount of modification to your existing code, you can define a parameters method inside module ClassMethods. For example:
def parameters
#parameters ||= [:visible, :desc, :value]
end
This method solution can also serve as a long-term solution if you plan to use the parameters outside of the concern, or if a subclass might want to define its own parameters.
However, if the parameters are only meant to be used inside this concern, and this data will never change, at least not through any application logic, then a constant would be the best solution because it conveys the proper meaning to the reader. I would also freeze it to prevent modification:
PARAMETERS = [:visible, :desc, :value].freeze
Another option, as Rich mentioned, is to define a class variable. Note that the constant will work whether you define it inside the List module, or inside the ClassMethods module. However, a class variable will only work inside the ClassMethods module if you want Product to be able to call it as parameters.
Also, note that self is implied in any method within ClassMethods, so you don't need to specify it. If you defined a parameters method, it would be considered a Product class method, and if you used parameters within the all method, it would refer to the class method, not an instance method as suggested by Rich.
Class variables are generally discouraged in Ruby because their side effects are often misunderstood. The Ruby Style Guide recommends avoiding them: https://github.com/bbatsov/ruby-style-guide#no-class-vars
As for speed, I compared the method and constant solutions, and it looks like the constant is faster:
require "benchmark/ips"
PARAMETERS = [:visible, :desc, :value].freeze
def parameters
#parameters ||= [:visible, :desc, :value]
end
def uses_constant
puts PARAMETERS
end
def uses_method
puts parameters
end
Benchmark.ips do |x|
x.report("constant") { uses_constant }
x.report("method") { uses_method }
x.compare!
end
The result:
Comparison:
constant: 45256.8 i/s
method: 44799.6 i/s - 1.01x slower
Set it as a class var:
module List
extend ActiveSupport::Concern
##parameters = [:visible, :desc, :value]
cattr_Accessor :parameters #-> List.parameters && List.new.parameters
The problem you have right now is you're calling an instance method from a class method:
module ClassMethods
self.new(row.select{|key,_| parameters.include? key.to_sym })
With the class variable code, you'd be able to run:
self.new(row.select{|key,_| self.parameters.include? key.to_sym })
A method created with def doesn't see local variables present when the method was defined, so your first attempt doesn't work.
With the instance variable, the object your are setting the variable on (your module) and the object trying to read it (the class that included your module) are different. Instance variables don't take part in inheritance at all, so that doesn't work.
What would work is a constant in your List module, ie
PARAMETERS = [:visible, :desc, :value]
Because your class methods module is inside the List module, code inside it will find constants set on List. Constant lookup first looks at lexical scope (see Module.nesting for this search path) and then inheritance.

Dynamically defining instance method within an instance method

I have a several classes, each of which define various statistics.
class MonthlyStat
attr_accessor :cost, :size_in_meters
end
class DailyStat
attr_accessor :cost, :weight
end
I want to create a decorator/presenter for a collection of these objects, that lets me easily access aggregate information about each collection, for example:
class YearDecorator
attr_accessor :objs
def self.[]= *objs
new objs
end
def initialize objs
self.objs = objs
define_helpers
end
def define_helpers
if o=objs.first # assume all objects are the same
o.instance_methods.each do |method_name|
# sums :cost, :size_in_meters, :weight etc
define_method "yearly_#{method_name}_sum" do
objs.inject(0){|o,sum| sum += o.send(method_name)}
end
end
end
end
end
YearDecorator[mstat1, mstat2].yearly_cost_sum
Unfortunately define method isn't available from within an instance method.
Replacing this with:
class << self
define_method "yearly_#{method_name}_sum" do
objs.inject(0){|o,sum| sum += o.send(method_name)}
end
end
...also fails because the variables method_name and objs which are defined in the instance are no longer available. Is there an idomatic was to accomplish this in ruby?
(EDITED: I get what you're trying to do now.)
Well, I tried the same approaches that you probably did, but ended up having to use eval
class Foo
METHOD_NAMES = [:foo]
def def_foo
METHOD_NAMES.each { |method_name|
eval <<-EOF
def self.#{method_name}
\"#{method_name}\".capitalize
end
EOF
}
end
end
foo=Foo.new
foo.def_foo
p foo.foo # => "Foo"
f2 = Foo.new
p f2.foo # => "undefined method 'foo'..."
I myself will admit it's not the most elegant solution (may not even be the most idiomatic) but I've run into similar situations in the past where the most blunt approach that worked was eval.
I'm curious what you're getting for o.instance_methods. This is a class-level method and isn't generally available on instances of objects, which from what I can tell, is what you're dealing with here.
Anyway, you probably are looking for method_missing, which will define the method dynamically the first time you call it, and will let you send :define_method to the object's class. You don't need to redefine the same instance methods every time you instantiate a new object, so method_missing will allow you to alter the class at runtime only if the called method hasn't already been defined.
Since you're expecting the name of a method from your other classes surrounded by some pattern (i.e., yearly_base_sum would correspond to a base method), I'd recommend writing a method that returns a matching pattern if it finds one. Note: this would NOT involve making a list of methods on the other class - you should still rely on the built-in NoMethodError for cases when one of your objects doesn't know how to respond to message you send it. This keeps your API a bit more flexible, and would be useful in cases where your stats classes might also be modified at runtime.
def method_missing(name, *args, &block)
method_name = matching_method_name(name)
if method_name
self.class.send :define_method, name do |*args|
objs.inject(0) {|obj, sum| sum + obj.send(method_name)}
end
send name, *args, &block
else
super(name, *args, &block)
end
end
def matching_method_name(name)
# ... this part's up to you
end

Resources