composed_of updates ActiveRecord attributes - ruby-on-rails

Example object:
class Foo < ApplicationRecord
composed_of :bar,
mapping: [%w[user_id user_id], %w[color color]],
converter: proc { |user_id, color| Bar.new(user_id, color) }
end
class Bar
include ActiveModel::Model
attr_accessor :user_id, :color
def initialize(user_id, color)
#user_id = user_id
#color = color
end
def change_to_blue
self.color = "blue"
end
end
Is there a way to make it so if I update the bar object's color it also updates the Foo color?
Example
foo = Foo.last
foo.color = "yellow"
foo.bar.change_to_blue
puts foo.color
Currently the above would result in "yellow" but was wondering if it was possible to create the linkage back to Foo so that result is "blue"? I want to be able to do Foo.save! and the color would be updated to "blue"

The Docs explain this concept fairly well.
Active Record implements aggregation through a macro-like class method called composed_of for representing attributes as value objects.
Value objects are immutable and interchangeable objects that represent a given value...It's also important to treat the value objects as immutable. Don't allow the [Bar] object to have its [color] changed after creation. Create a new [Bar] object with the new value instead.
Additionally I believe your converter is incorrect as the converter is used in assignment and thus only passes a single value.
So I think the below is actually what you are looking for to change the color.
class Foo < ApplicationRecord
composed_of :bar,
mapping: [%w[user_id user_id], %w[color color]],
constructor: proc { |user_id, color| Bar.new(user_id, color) }
end
class Bar
include ActiveModel::Model
attr_reader :user_id, :color
def initialize(user_id, color)
#user_id = user_id
#color = color
end
def change_to_blue
self.class.new(user_id,'blue')
end
end
and then
foo = Foo.last
foo.color = "yellow"
foo.bar = foo.bar.change_to_blue
puts foo.color

Related

Rails to_json(methods: => [...]) for different ActiveRecords

In Rails, i have an object called values that could be 1 of 20 kinds of ActiveRecord, and in only 1 of them there's a method(may be the wrong term, rails newbie) that can add a customized field in returned JSON object where the method name is the field name and method returned value is the field value. For example
class XXXController < ApplicationController
..
if a
values = A
elsif b
values = B
elseif c
values = C
..
end
render :json => values.to_json(:methods => :type_needed)
and you will see response like
{
..
"type_needed": true,
..
}
I only have type_needed defined in A which will return true in some cases. For others like B, C, D... which in total 19, i want them to all have type_needed returned as false, is there a way i can do that in one place instead of add type_needed method in the rest 19?
I will do it as follows:
json = values.to_json(:methods => :type_needed)
# => "[{\"id\":1,\"name\":\"Aaa\"},{\"id\":\"2\",\"name\":\"Bbb\"}]" # => Representational value only
ary = JSON.parse(json)
# => [{"id"=>1, "name"=>"Aaa"}, {"id"=>2, "name"=>"Bbb"}]
ary.map! { |hash| hash[:type_needed] = false unless hash.key?(:type_needed); hash }
# => [{"id"=>1, "name"=>"Aaa", :type_needed=>false}, {"id"=>2, "name"=>"Bbb", :type_needed=>false}]
ary.to_json
# => "[{\"id\":1,\"name\":\"Aaa\",\"type_needed\":false},{\"id\":\"2\",\"name\":\"Bbb\",\"type_needed\":false}]"
If I am understanding your question correctly then you want to define type_needed method once and have it included on all your 20 models. If yes, then you can define a concern and include it in all your 20 models.
app/models/concerns/my_model_concern.rb
module MyModelConcern
extend ActiveSupport::Concern
def type_needed?
self.respond_to?(:some_method)
end
end
app/models/a.rb
class A < ApplicationRecord
include MyModelConcern
def some_method
end
end
app/models/b.rb
class B < ApplicationRecord
include MyModelConcern
end
app/models/c.rb
class C < ApplicationRecord
include MyModelConcern
end
With the above
a = A.new
a.type_needed?
=> true
b = B.new
b.type_needed?
=> false
c = C.new
c.type_needed?
=> false
See if this helps.

Pointer on class object

In my Ruby model I want to apply default value on somes properties on my Recipe. So I added an before_save callback to apply it: This is my Recipe model:
class Recipe < ActiveRecord::Base
before_save :set_default_time
# other stuff
private
# set default time on t_baking, t_cooling, t_cooking, t_rest if not already set
def set_default_time
zero_time = Time.new 2000, 1 ,1,0,0,0
self.t_baking = zero_time unless self.t_baking.present?
self.t_cooling = zero_time unless self.t_cooling.present?
self.t_cooking = zero_time unless self.t_cooking.present?
self.t_rest = zero_time unless self.t_rest.present?
end
end
It's pretty work but I want to factorize it like this:
class Recipe < ActiveRecord::Base
before_save :set_default_time
# other stuff
private
# set default time on t_baking, t_cooling, t_cooking, t_rest if not already set
def set_default_time
zero_time = Time.new 2000, 1 ,1,0,0,0
[self.t_baking, self.t_cooling, self.t_cooking, self.t_rest].each{ |t_time|
t_time = zero_time unless t_time.present?
}
end
end
But it doesn't work. How can I loop on "pointer" on my object propertie?
it won't work because you refer strictly to value, thus your override doesn't work as expected. you may try this:
[:t_baking, :t_cooling, :t_cooking, :t_rest].each { |t_time|
self.send("#{t_time}=".to_sym, zero_time) unless self.send(t_time).present?
}

Rails: ActiveRecord interdependent attributes setters

In activerecord, attribute setters seems to be called in order of the param hash.
Therefore, in the following sample, "par_prio" will be empty in "par1" setter.
class MyModel < ActiveRecord::Base
def par1=(value)
Rails.logger.info("second param: #{self.par_prio}")
super(value)
end
end
MyModel.new({ :par1 => 'bla', :par_prio => 'bouh' })
Is there any way to simply define an order on attributes in the model ?
NOTE: I have a solution, but not "generic", by overriding the initialize method on "MyModel":
def initialize(attributes = {}, options = {})
if attributes[:par_prio]
value = attributes.delete(:par_prio)
attributes = { :par_prio => value }.merge(attributes)
end
super(attributes, options)
end
Moreover, it does not works if par_prio is another model that has a relation on, and is used to build MyModel:
class ParPrio < ActiveRecord::Base
has_many my_models
end
par_prio = ParPrio.create
par_prio.my_models.build(:par1 => 'blah')
The par_prio param will not be available in the initialize override.
Override assign_attributes on the specific model where you need the assignments to happen in a specific order:
attr_accessor :first_attr # Attr that needs to be assigned first
attr_accessor :second_attr # Attr that needs to be assigned second
def assign_attributes(new_attributes, options = {})
sorted_new_attributes = new_attributes.with_indifferent_access
if sorted_new_attributes.has_key?(:second_attr)
first_attr_val = sorted_new_attributes.delete :first_attr
raise ArgumentError.new('YourModel#assign_attributes :: second_attr assigned without first_attr') unless first_attr_val.present?
new_attributes = Hash[:first_attr, first_attr_val].merge(sorted_new_attributes)
end
super(new_attributes, options = {})
end

ActiveRecord: How to set the "changed" property of an model?

For every model in ActiveRecord, there seems to be a private property called "changed", which is an array listing all the fields that have changed since you retrieved the record from the database.
Example:
a = Article.find(1)
a.shares = 10
a.url = "TEST"
a.changed ["shares", "url"]
Is there anyway to set this "changed" property yourself? I know it sounds hacky/klunky, but I am doing something rather unordinary, which is using Redis to cache/retrieve objects.
ActiveModel::Dirty#changed returns keys of the #changed_attributes hash, which itself returns attribute names and their original values:
a = Article.find(1)
a.shares = 10
a.url = "TEST"
a.changed #=> ["shares", "url"]
a.changed_attributes #=> {"shares" => 9, "url" => "BEFORE_TEST"}
Since there is no setter method changed_attributes=, you can set the instance variable by force:
a.instance_variable_set(:#changed_attributes, {"foo" => "bar"})
a.changed #=> ["foo"]
See this example from: Dirty Attributes
class Person
include ActiveModel::Dirty
define_attribute_methods :name
def name
#name
end
def name=(val)
name_will_change! unless val == #name
#name = val
end
def save
#previously_changed = changes
#changed_attributes.clear
end
end
So, if you have a attribute foo and want to "change" that just call foo_will_change!.

How would I pass sort criteria into a method so that it can be used by sort_by

How would I pass an attribute name to method sort_by, and use it within? For example, say I wanted to create a FrequencyCounter with an array of singleton objects Foo. (It's important that they're singletons because it means that there will be a certain number of identical Foos).
class Foo
attr_accessor :arbitrary_sorter
def initialize arbitrary_sorter
#arbitrary_sorter = arbitrary_sorter
end
end
class FrequencyCounter
def initialize ary
#multiset will create a hash of frequency like {3=>obj, 2->obj, 2=>obj, etc}
#hash = Multiset.new(ary).hash
end
def sort_by params={}
Hash[#hash.sort_by {|k,v| [-1 * v, -1 * k]}]
end
end
And when I create Frequency object:
#fc = FrequencyCounter.new([Foo.get(5), Foo.get(4), Foo.get(5), Foo.get(1)])
I'd like to tell #fc what to order on:
#fc.sort_by(:arbitrary_sorter)
Is this possible?
on any enumerator, you can tell it what to sort by. So, let's take a simple example:
class Animal
attr_accessor :name, :leg_count
def initialize(name, leg_count)
#name = name
#leg_count = leg_count
end
def to_s
#name
end
end
animals = [Animal.new('human', 2), Animal.new('dog', 4), Animal.new('snake', 0)]
=> [human, dog, snake]
# sort by an attribute or method on the model
animals.sort_by{|a| a.leg_count}
=> [snake, human, dog]
Same as:
animals.sort_by(&:leg_count)
=> [snake, human, dog]
You can event do something like:
sorter_lambda = lambda {|animal| animal.name == "human" ? 0 : 999}
animals.sort_by(&sorter_lambda)
=> [human, dog, snake]
The lambda may work best for you with the complicated logic I saw.

Resources