Generic method to set attributes - ruby-on-rails

In my model I have attributes: is_a, is_b and is_c. By default all are null.
I need APIs to set them. These attributes can be set as strictly one or in group. If I am to write APIs, I will be doing following in my model:
def set_as_a # strictly a
self.update_attributes!(:is_a => true, :is_b => false, :is_c => false)
end
def set_as_b # strictly b
self.update_attributes!(:is_a => false, :is_b => true, :is_c => false)
end
... # strictly c
def set_as_a_and_b # a and b
self.update_attributes!(:is_a => true, :is_b => true, :is_c => false)
end
..... # so on
While this works, it does not look elegant. Also if in future if the set has more than 3 attributes, it will result more repetitive code. What is the correct elegant way to achieve this?

class SettableAsABC
ATTRS = [:a, :b, :c]
METHOD_RE = /^set_as_([[:alnum:]]+?(?:_and_[[:alnum:]]+?)*)$/
def method_missing(name, *args)
if name.to_s =~ METHOD_RE
trues = $1.split('_and_').map(&:to_sym)
attrs = Hash[ATTRS.map { |a| ["is_#{a}".to_sym, trues.include?(a)] }]
update_attributes(attrs)
else
super
end
end
def respond_to_missing?(name, include_private = false)
!!(name =~ METHOD_RE) || super
end
end
a = SettableAsABC.new
a.set_as_a_and_c
No defining 2^N methods, just plain Ruby metaprogramming.
EDIT: Good point, #Stefan.
EDIT2: My previous edit introduced a bug. Fixed now.
EDIT3: TIL about respond_to_missing?

I might be misunderstanding something, but why not just write a single method that takes params?:
def set_attributes(opts = {})
update_attributes!(opts) unless opts.none?
end
# usage
set_attributes(is_a: false, is_b: true)
EDIT
To dynamically create methods for combinations of your attributes here is what I came up with:
attributes = %w(a b c d)
(1..attributes.size).flat_map { |size| attributes.combination(size).to_a }.each do |methods|
define_method "set_as_#{methods.join('_and_')}" do
update_attributes!(Hash[methods.map { |v| ["is_#{v}", true] }])
end
end
It will generate the following menthods:
set_as_a
set_as_b
set_as_c
set_as_d
set_as_a_and_b
set_as_a_and_c
set_as_a_and_d
set_as_b_and_c
set_as_b_and_d
set_as_c_and_d
set_as_a_and_b_and_c
set_as_a_and_b_and_d
set_as_a_and_c_and_d
set_as_b_and_c_and_d
set_as_a_and_b_and_c_and_d

How about this?
def set_true(true_fields=[])
attr_hash = {}
true_fields.each { |field| attr_hash[field] = true }
update_attributes(hash)
end
Hope that helps!

Related

Is a ':methods' option in 'to_json' substitutable with an ':only' option?

The to_json option has options :only and :methods. The former is intended to accept attributes and the latter methods.
I have a model that has an attribute foo, which is overwritten:
class SomeModel < ActiveRecord::Base
...
def foo
# Overrides the original attribute `foo`
"the overwritten foo value"
end
end
The overwritten foo method seems to be called irrespective of which option I write the foo under.
SomeModel.first.to_json(only: [:foo])
# => "{..., \"foo\":\"the overwritten foo value\", ...}"
SomeModel.first.to_json(methods: [:foo])
# => "{..., \"foo\":\"the overwritten foo value\", ...}"
This seems to suggest it does not matter whether I use :only or :methods.
Is this the case? I feel something wrong with my thinking.
The source code leads to these:
File activemodel/lib/active_model/serialization.rb, line 124
def serializable_hash(options = nil)
options ||= {}
attribute_names = attributes.keys
if only = options[:only]
attribute_names &= Array(only).map(&:to_s)
elsif except = options[:except]
attribute_names -= Array(except).map(&:to_s)
end
hash = {}
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
Array(options[:methods]).each { |m| hash[m.to_s] = send(m) }
serializable_add_includes(options) do |association, records, opts|
hash[association.to_s] = if records.respond_to?(:to_ary)
records.to_ary.map { |a| a.serializable_hash(opts) }
else
records.serializable_hash(opts)
end
end
hash
end
File activeresource/lib/active_resource/base.rb, line 1394
def read_attribute_for_serialization(n)
attributes[n]
end
and it seems that an :only option calls attributes[n] and :methods option calls send(m). What is the difference?

How to construct where clause in ruby using if

I am finding something like below. Constructing a where clause using condition. Is it possible in ruby? or I need to separate it into two where clause?
Post
.where(tag: "A") if condition A
.where(tag: "B") if condition B
.where(user_id: 1)
.order(....)
Actually, my case is like this. Is there any way to handle?
def this_function
#questions = Question.joins(:comment_threads)
.tagged_with(tag_variable, wild: true, any: true) if tag_variable.present?
.where(index_where_clause)
.where("questions.created_at < ?", query_from_date_time)
.order(created_at: :desc).limit(5)
end
def index_where_clause
where_clause = {}
where_clause[:user_detail_id] = current_user_detail.id if params[:type] == "my_question"
where_clause[:comments] = {user_detail_id: current_user_detail.id} if params[:type] == "my_answer"
where_clause[:wine_question_score_id] = params[:wine_question_score_id] if params[:wine_question_score_id].present?
where_clause
end
The methods you're using return relations so you can say things like this:
#questions = Question.joins(:comment_threads)
#questions = #questions.where("questions.created_at < ?", query_from_date_time)
#questions = #questions.tagged_with(tag_variable, wild: true, any: true) if tag_variable.present?
#questions = #questions.where(:user_detail_id => current_user_detail.id) if params[:type] == "my_question"
#questions = #questions.where(:comments => { user_detail_id: current_user_detail.id}) if params[:type] == "my_answer"
#questions = #questions.where(:wine_question_score_id => params[:wine_question_score_id]) if params[:wine_question_score_id].present?
#questions = #questions.order(created_at: :desc).limit(5)
and build the query piece by piece depending on what you have in params.
I'd probably break it down a little more:
def whatever
#questions = Question.joins(:comment_threads)
#questions = #questions.where("questions.created_at < ?", query_from_date_time)
#questions = with_tag(#questions, tag_variable)
#...
#questions = #questions.order(created_at: :desc).limit(5)
end
private
def with_tag(q, tag)
if tag.present?
q.tagged_with(tag, wild: true, any: true)
else
q
end
end
#...
and bury all the noisy bits in little methods to make things cleaner and easier to read. If you're doing this more than once then you could use scopes to hide the noise in the model class and re-use it as needed.
#tap can be helpful for modifying an object in place to apply conditional logic, in this case the object would be your .where conditions:
Post
.where(
{ user_id: 1 }
.tap do |conditions|
conditions[:tag] = 'A' if condition A
conditions[:tag] = 'B' if condition B
end
)
.order(...)
Or, perhaps it's a little cleaner if you create a helper method:
def specific_conditions
{ user_id: 1 }.tap do |conditions|
conditions[:tag] = 'A' if condition A
conditions[:tag] = 'B' if condition B
end
end
Post.where(specific_conditions).order(...)
But as a side note, if there's a case where condition A and condition B can both be true, the second conditions[:tag] = ... line will override the first. If there is not a case where both can be true, you might try to use some kind of collection to look up the proper value for tag.
CONDITION_TAGS = {
a: 'A'.freeze,
b: 'B'.freeze,
}.freeze
def specific_conditions
{ user_id: 1 }
.tap do |conditions|
conditions[:tag] = CONDITION_TAGS[condition_value] if condition_value
end
end
Post.where(specific_conditions).order(...)
#in Question class
scope :with_user_detail, -> (user_detail_id, flag=true) do
where("user_detail_id = ?", user_detail_id) if flag
end
scope :with_user_detail_comments, -> (user_detail_id, flag=true) do
joins(:comment_threads).where("comments.user_detail_id = ?", user_detail_id) if flag
end
scope :with_wine_question_score, -> (wine_question_score_id) do
where("wine_question_score_id = ?", wine_question_score_id) if wine_question_score_id.present?
end
scope :tagged_with_condition, -> (tag_variable, wild, any) do
tagged_with(tag_variable, wild, any) if tag_variable.present?
end
def this_function
my_question_flag = params[:type] == "my_question"
my_answer_flag = params[:type] == "my_answer"
Question.with_user_detail(current_user_detail.id, my_question_flag)
.tagged_with_condition(tag_variable, wild: true, any: true)
.with_user_detail_comments(current_user_detail.id, my_answer_flag)
.with_wine_question_score(params[:wine_question_score_id])
.order(created_at: :desc).limit(5)
end
You can do the following:
condition = {:tag => "A"} if condition A
condition = {:tag => "B"} if condition B
Post
.where(condition)
.where(:user_id => 1)
.order(....)
you have to use scope :
scope :my_scope, -> (variable) { where(some: vatiable) if my_condition }

Instance Variables in a Rails Model

I have this variable opinions I want to store as an instance variable in my model... am I right in assuming I will need to add a column for it or else be re-calculating it constantly?
My other question is what is the syntax to store into a column variable instead of just a local one?
Thanks for the help, code below:
# == Schema Information
#
# Table name: simulations
#
# id :integer not null, primary key
# x_size :integer
# y_size :integer
# verdict :string
# arrangement :string
# user_id :integer
#
class Simulation < ActiveRecord::Base
belongs_to :user
serialize :arrangement, Array
validates :user_id, presence: true
validates :x_size, :y_size, presence: true, :numericality => {:only_integer => true}
validates_numericality_of :x_size, :y_size, :greater_than => 0
def self.keys
[:soft, :hard, :none]
end
def generate_arrangement
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#arrangement = Array.new(y_size) { Array.new(x_size) }
#arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
rand_opinion = Simulation.keys[rand(0..2)]
#arrangement[y_index][x_index] = rand_opinion
#opinions[rand_opinion] += 1
end
end
end
def verdict
if #opinions[:hard] > #opinions[:soft]
:hard
elsif #opinions[:soft] > #opinions[:hard]
:soft
else
:push
end
end
def state
#arrangement
end
def next
new_arrangement = Array.new(#arrangement.size) { |array| array = Array.new(#arrangement.first.size) }
#opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
#seating_arrangement.each_with_index do |array, y_index|
array.each_with_index do |opinion, x_index|
new_arrangement[y_index][x_index] = update_opinion_for x_index, y_index
#opinions[new_arrangement[y_index][x_index]] += 1
end
end
#arrangement = new_arrangement
end
private
def in_array_range?(x, y)
((x >= 0) and (y >= 0) and (x < #arrangement[0].size) and (y < #arrangement.size))
end
def update_opinion_for(x, y)
local_opinions = Hash[ Simulation.keys.map { |key| [key, 0] } ]
for y_pos in (y-1)..(y+1)
for x_pos in (x-1)..(x+1)
if in_array_range? x_pos, y_pos and not(x == x_pos and y == y_pos)
local_opinions[#arrangement[y_pos][x_pos]] += 1
end
end
end
opinion = #arrangement[y][x]
opinionated_neighbours_count = local_opinions[:hard] + local_opinions[:soft]
if (opinion != :none) and (opinionated_neighbours_count < 2 or opinionated_neighbours_count > 3)
opinion = :none
elsif opinion == :none and opinionated_neighbours_count == 3
if local_opinions[:hard] > local_opinions[:soft]
opinion = :hard
elsif local_opinions[:soft] > local_opinions[:hard]
opinion = :soft
end
end
opinion
end
end
ActiveRecord analyzes the database tables and creates setter and getter methods with metaprogramming.
So you would create a database column with a migration:
rails g migration AddOpinionToSimulation opinion:hash
Note that not all databases support storing a hash or a similar key/value data type in a column. Postgres does. If you need to use another database such MySQL you should consider using a relation instead (storing the data in another table).
Then when you access simulation.opinion it will automatically get the database column value (if the record is persisted).
Since ActiveRecord creates a setter and getter you can access your property from within the Model as:
class Simulation < ActiveRecord::Base
# ...
def an_example_method
self.opinions # getter method
# since self is the implied receiver you can simply do
opinions
opinions = {foo: "bar"} # setter method.
end
end
The same applies when using the plain ruby attr_accessor, attr_reader and attr_writer macros.
When you assign to an attribute backed by a database column ActiveRecord marks the attribute as dirty and will include it when you save the record.
ActiveRecord has a few methods to directly update attributes: update, update_attributes and update_attribute. There are differences in the call signature and how they handle callbacks.
you can add a method like
def opinions
#opinions ||= Hash[ Simulation.keys.map { |key| [key, 0] }
end
this will cache the operation into the variable #opinions
i would also add a method like
def arrangement
#arrangement ||= Array.new(y_size) { Array.new(x_size) }
end
def rand_opinion
Simulation.keys[rand(0..2)]
end
and then replace the variables with your methods
def generate_arrangement
arrangement.each_with_index do |row, y_index|
row.each_with_index do |current, x_index|
arrangement[y_index][x_index] = rand_opinion
opinions[rand_opinion] += 1
end
end
end
now your opinions and your arrangement will be cached and the code looks better. you didn't have to add a new column in you table
you now hat to replace the #opinions variable with your opinions method

Call a generic function with or without parameters

I had a code looking like this:
def my_function(obj)
if obj.type == 'a'
return [:something]
elsif obj.type == 'b'
return []
elsif obj.type == 'c'
return [obj]
elsif obj.type == 'd'
return [obj]*2
end
end
I want to separate all these if...elsif blocks into functions like this:
def my_function_with_a
return [:something]
end
def my_function_with_b
return []
end
def my_function_with_c(a_parameter)
return [a_parameter]
end
def my_function_with_d(a_parameter)
return [a_parameter] * 2
end
I call these functions with
def my_function(obj)
send(:"my_function_with_#{obj.type}", obj)
end
The problem is that some functions need parameters, others do not. I can easily define def my_function_with_a(nothing=nil), but I'm sure there is a better solution to do this.
#Dogbert had a great idea with arity. I have a solution like this:
def my_function(obj)
my_method = self.method("my_function_with_#{obj.type}")
return (method.arity.zero? ? method.call : method.call(obj))
end
Check how to call methods in Ruby, for that I will recommend you this two resources: wikibooks and enter link description here.
Take a special note on optional arguments where you can define a method like this:
def method(*args)
end
and then you call call it like this:
method
method(arg1)
method(arg1, arg2)
def foo(*args)
[ 'foo' ].push(*args)
end
>> foo
=> [ 'foo' ]
>> foo('bar')
=> [ 'foo', 'bar' ]
>> foo('bar', 'baz')
=> [ 'foo', 'bar', 'baz' ]
def my_function(obj)
method = method("my_function_with_#{obj.type}")
method.call(*[obj].first(method.arity))
end
Change your function to something like:
def my_function_with_foo(bar=nil)
if bar
return ['foo', bar]
else
return ['foo']
end
end
Now the following will both work:
send(:"my_function_with_#{foo_bar}")
=> ['foo']
send(:"my_function_with_#{foo_bar}", "bar")
=> ['foo', 'bar']
You can also write it like this if you don't want to use if/else and you're sure you'll never need nil in the array:
def my_function_with_foo(bar=nil)
return ['foo', bar].compact
end
You can use a default value
def fun(a_param = nil)
if a_param
return ['raboof',a_param]
else
return ['raboof']
end
end
or...
def fun(a_param : nil)
if a_param
return ['raboof',a_param]
else
return ['raboof']
end
end
The latter is useful if you have multiple parameters because now when you call it you can just pass in the ones that matter right now.
fun(a_param:"Hooray")

Dynamically defined setter methods using define_method?

I use a lot of iterations to define convenience methods in my models, stuff like:
PET_NAMES.each do |pn|
define_method(pn) do
...
...
end
but I've never been able to dynamically define setter methods, ie:
def pet_name=(name)
...
end
using define_method like so:
define_method("pet_name=(name)") do
...
end
Any ideas? Thanks in advance.
Here's a fairly full example of using define_method in a module that you use to extend your class:
module VerboseSetter
def make_verbose_setter(*names)
names.each do |name|
define_method("#{name}=") do |val|
puts "##{name} was set to #{val}"
instance_variable_set("##{name}", val)
end
end
end
end
class Foo
extend VerboseSetter
make_verbose_setter :bar, :quux
end
f = Foo.new
f.bar = 5
f.quux = 10
Output:
#bar was set to 5
#quux was set to 10
You were close, but you don't want to include the argument of the method inside the arguments of your call to define_method. The arguments go in the block you pass to define_method.
Shoertly if you need it inside one class/module:
I use hash but you can put there array of elements etc.
PETS = {
"cat" => "meyow",
"cow" => "moo",
"dog" => "ruff"
}
def do_smth1(v)
...
end
def do_smth(sound,v)
...
end
#getter
PETS.each{ |k,v| define_method(k){ do_smth1(v) } }
#setter
PETS.each{ |k,v| define_method("#{k}="){|sound| do_smth2(sound, v) }

Resources