So I got introduced to using PORO instead of AR object for abstraction and size reduction.
But I have so many AR tables didn't make sense to put so much time to build a PORO class for each and every. Would take like a hour or two!! So instead I spent many hours thinking about how can I make this simpler.
And this is what I ended up making:
class BasePORO
def initialize(obj, immutable = true)
self.class::ATTRIBUTES.each do |attr|
instance_variable_set("##{attr}".to_sym, obj.attributes[attr.to_s])
instance_eval("undef #{attr}=") if immutable
end
end
end
class UserPORO < BasePORO
# or plug your own attributes
ATTRIBUTES = User.new.attributes.keys.map(&:to_sym).freeze
attr_accessor(*ATTRIBUTES)
end
But I can't somehow move the attr_accessor into the Base class or even ATTRIBUTES when not given explicitly. Not sure if its possible even.
Can I somehow move attr_accessor and default ATTRIBUTES into the main BasePORO class?
Any pointers or feedback is welcome.
As suggested in the comments, OpenStruct can do most of the heavy lifting for you. One thing to note is that if you don't freeze it, then after it's initialization you'll be able to add more attributes to it throughout its lifetime, e.g.:
struct = OpenStruct.new(name: "Joe", age: 20)
struct.email = "joe#example.com" # this works
p struct.email # => "joe#example.com"
(so essentially it works like a Hash with object-like interface)
This behavior may be undesired. And if you do freeze the struct, it won't allow any more attributes definition, but then you'd also lose the ability to override existing values (which I think you want to do in cases when someone sets immutable to false).
For the immutable flag to work as I understand you to expect it, I'd create a class that uses OpenStruct under its hood, for example like this:
class BasePORO
def initialize(obj, immutable = true)
#immutable = immutable
#data = OpenStruct.new(obj.attributes)
obj.attributes.keys.each do |attr|
self.class.define_method(attr.to_sym) do
#data.send(attr.to_sym)
end
self.class.define_method("#{attr}=".to_sym) do |new_value|
if #immutable
raise StandardError.new("#{self} is immutable")
else
#data.send("#{attr}=".to_sym, new_value)
end
end
end
end
end
class UserPORO < BasePORO
end
BTW, if you insisted on having a solution similar to the one shown in the question, then you could achieve this with something like that:
class BasePORO
def initialize(obj, immutable = true)
#immutable = immutable
attributes.each do |attr|
instance_variable_set("##{attr}".to_sym, obj.attributes[attr.to_s])
self.class.define_method(attr.to_sym) do
instance_variable_get("##{attr}".to_sym)
end
self.class.define_method("#{attr}=".to_sym) do |new_value|
if #immutable
raise StandardError.new("#{self} is immutable")
else
instance_variable_set("##{attr}".to_sym, new_value)
end
end
end
end
private
# default attributes
def attributes
[:id]
end
end
class UserPORO < BasePORO
private
# overriding default attributes from BasePORO
def attributes
User.new.attributes.keys.map(&:to_sym).freeze
end
end
So this is what actually ended up with:
class BaseStruct < OpenStruct
def initialize(model, immutable: true, only: [], includes: [])
if only.empty?
hash = model.attributes
else
hash = model.attributes.slice(*only.map!(&:to_s))
end
includes.each do |i|
relation = model.public_send(i)
if relation.respond_to?(:each)
hash[i.to_s] = relation.map{|r| OpenStruct.new(r.attributes).freeze}
else
hash[i.to_s] = OpenStruct.new(relation.attributes).freeze
end
end
super(hash)
self.freeze if immutable
end
end
Feel free to critique or suggest improvements.
Related
I am trying to assign session values to model object as below.
# models/product.rb
attr_accessor :selected_currency_id, :selected_currency_rate, :selected_currency_icon
def initialize(obj = {})
selected_currency_id = obj[:currency_id]
selected_currency_rate = obj[:currency_rate]
selected_currency_icon = obj[:currency]
end
but this works only when I initialize new Product object
selected_currency = (session[:currency].present? ? session : Currency.first.attributes)
Product.new(selected_currency)
While, i need to set these setter methods on each product object automatically even if was fetched from Database.(active record object) ie. Product.all or Product.first
Earlier i was manually assigning values to each product object after retrieving it from db on controller side.
#products.each do |product|
product.selected_currency_id = session[:currency_id]
product.selected_currency_rate = session[:currency_rate]
product.selected_currency_icon = session[:currency]
end
But then i need to do it on every method where product details need to be displayed. Please suggest a better alternative to set these setter methods automatically on activerecord objects.
I don't think you really want to do this on the model layer at all. One thing you definitely don't want to do is override the initializer on your model and change its signature and not call super.
Your model should only really know about its own currency. Displaying the price in another currency should be the concern of another object such as a decorator or a helper method.
For example a really naive implementation would be:
class ProductDecorator < SimpleDelegator
attr_accessor :selected_currency
def initialize(product, **options)
# Dynamically sets the ivars if a setter exists
options.each do |k,v|
self.send "#{k}=", v if self.respond_to? "#{k}="
end
super(product) # sets up delegation
end
def price_in_selected_currency
"#{ price * selected_currency.rate } #{selected_currency.icon}"
end
end
class Product
def self.decorate(**options)
self.map { |product| product.decorate(options) }
end
def decorate(**options)
ProductDecorator.new(self, options)
end
end
You would then decorate the model instances in your controller:
class ProductsController
before_action :set_selected_currency
def index
#products = Product.all
.decorate(selected_currency: #selected_currency)
end
def show
#product = Product.find(params[:id])
.decorate(selected_currency: #selected_currency)
end
private
def set_selected_currency
#selected_currency = Currency.find(params[:selected_currency_id])
end
end
But you don't need to reinvent the wheel, there are numerous implementations of the decorator pattern like Draper and dealing with currency localization is complex and you really want to look at using a library like the money gem to handle the complexity.
I have method in my rails model which returns anonymous class:
def today_earnings
Class.new do
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.instance_eval do
def to_f
reduce(:+)
end
end
end
def used
all.map(&:amount_used).instance_eval do
def to_f
reduce(:+)
end
end
end
end.new(self)
end
I want to achieve possibility to chain result in that way user.today_earning.unused.to_f and I have some problems with instance eval because when I call to_f on result it's undefined method, I guess it is due to ruby copying returned value so the instance gets changed, is it true? And if I'm correct how can I change the code to make it work. Also I'm wondering if making new model can be better solution than anomyous class thus I need advice if anonymous class is elegant in that case and if so how can I add to_f method to returned values
Yes, Anonymous class makes the code much complex. I would suggest a seperate class. It will solve 2 problems here.
defining some anonymous class again and again when we call the today_earnings method.
Readability of the code.
Now coming to actual question, you can try something similar to hash_with_indifferent_access. The code looks as follows.
class NumericArray < Array
def to_f
reduce(:+)
end
end
Array.class_eval do
def with_numeric_operations
NumericArray.new(self)
end
end
Usage will be:
Class Earnings
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.with_numeric_operations
end
def used
all.map(&:amount_used).with_numeric_operations
end
end
This looks like a "clever" but ridiculously over-complicated way to do something that can be simply and efficiently done in the database.
User.joins(:store_credits)
.select(
'users.*',
'SUM(store_credits.amount_used) AS amount_used',
'SUM(store_credits.amount) - amount_used AS unused',
)
.where(store_credits: { created_at: Time.current.beginning_of_day..Time.current.end_of_day })
.group(:id)
Description
I've created a Report object that has different environments/groups (:live & :demo) which are simply arrays that will be populated with ReportItem objects which have the following attributes :currency, :gross, :net (And Gross & Net hold Ruby Money objects).
In the Report.rb class there are two methods called add_money_to_net and add_money_to_gross both have a variable I temporarily named #group
Question
(report = Report.new)
What I'm trying to do is is chain methods like this report.demo.add_money_to_gross(<params>) if this method is called i would like to have access to report.demo (in place of my #group variable). Alternatively calling report.live.add_money_to_gross(<params>) should let me access report.live. Is this possible?
I hope this makes sense. Please let me know if anything is unclear.
Report.rb
class Report
include ActiveModel::Model
attr_accessor :demo, :live
def initialize
#demo = []
#live = []
end
def demo
#demo
self
end
def live
#live
self
end
def add_money_to_net(money)
add_money(group: #group, money: money)
end
def add_money_to_gross(money)
add_money(group: #group, money: money)
end
private
def add_money(group:, money:)
item = group.find {|s| s.currency == money.currency }
if item
item.net += money
else
group << ReportItem.new(currency: money.currency.to_s, net: money)
end
end
end
ReportItem.rb
class ReportItem
include ActiveModel::Model
attr_accessor :currency, :gross, :net
def initialize(currency:, gross: nil, net: nil)
#currency = currency
#gross = gross ? gross : Money.new(0, currency)
#net = net ? net : Money.new(0, currency)
end
def gross=(value)
#gross = value if #gross.currency == #currency
end
def net=(value)
#net = value if #net.currency == #currency
end
end
I get what you're trying to do, almost like a command-line pipe operation. And, no, not possible with your approach (or, IMHO, recommended).
Consider the demo method:
def demo
#demo
self
end
The line #demo really is a no-op. You're not "returning" anything here, not assigning anything, it's just ... there. The only thing returned here is self.
Now, you could do something like:
def demo
#group = #demo
self
end
Then make your call
report.live.add_money_to_gross(money)
And now it should work as you intend. Here, live and demo act more as state managers, setting the state of #group prior to calling the action method.
That said, this is kind of brittle, and requires that you call things in a certain order, and that temporary variable seems like a pitfall waiting to happen. Why? Because calling this:
report.add_money_to_gross(money)
is a valid call, but shouldn't be unless live or demo has been called first. No error will be thrown, it will just add to whichever #group was set last.
Instead, it would make more sense to me to actually create a new object to encapsulate all of this:
class ReportItemArray < Array
def add_money_to_net money
item = find_item(money)
if item
item.net += money
else
self << ReportItem.new(currency: money.currency.to_s, net: money)
end
end
def add_money_to_gross money
item = find_item(money)
if item
item.gross += money
else
self << ReportItem.new(currency: money.currency.to_s, gross: money)
end
end
def find_item money
self.find {|s| s.currency == money.currency }
end
end
Then, in your report class:
class Report
def initialize
#demo = ReportItemArray.new
#live = ReportItemArray.new
end
def live
#live
end
def demo
#demo
end
end
Note, the two methods live and demo are now actually obsolete, since you use attr_accessor and they don't return anything special anymore, so you don't actually need to override them, just doing so to be explicit. The live and demo objects themselves are still arrays, so should be compatible with the rest of your code, but the add_money methods are also attached to these Array objects, so now you can just do:
report.live.add_money_to_gross(money)
And now by calling live, you get the Array, and since the methods are part of that Array object, you no longer need to pass the context, they inherently know it (group becomes self). So, calling the method acts on that given array.
Doing it this way removes the possibility of calling report.add_money_to_gross (an error will be raised if you do), which is good because the call here needs more context (the group) to complete. This also removes the order-of-operations conflict, as the state is not needed anymore.
A final unrelated note, you may have caught my fix for this above, but your add_money method always adds to the ReportItem's net even when the original call was made from add_money_to_gross.
I'm not 100% sure about why ActiveModel::Dirty has its name. I'm guessing it is because it is considered as dirty to use it.
But in some cases, it is not possible to avoid watching on specific fields.
Ex:
if self.name_changed?
self.slug = self.name.parameterize
end
Without ActiveModel::Dirty, the code would look like:
if old_name != self.name
self.slug = self.name.parameterize
end
which implies having stored old_name before, and it is not readable, so IMHO, it is dirtier than using ActiveModel::Dirty. It becomes even worse if old_number is a number and equals params[:user]['old_number'] as it needs to be correctly formated (parsed as int), whereas ActiveRecord does this automagically.
So I would find clean to define watchable fields at Model level:
class User < ActiveRecord::Base
include ActiveModel::Dirty
watchable_fields :name
before_save :generate_slug, if: name_changed?
def generate_slug
self.slug = self.name.parameterize
end
end
Or (even better?) at controller level, before assigning new values:
def update
#user = current_user
#user.watch_fields(:name)
#user.assign_attributes(params[:user])
#user.generate_slug if #user.name_changed?
#user.save # etc.
end
The good thing here is that it removes the memory overload produced by using ActiveModel::Dirty.
So my question is:
Can I do that using ActiveRecord pre-built tools, or should I write a custom library to this?
Thanks
If ActiveModel::Dirty solves your problem, feel free to use it. The name comes from the term "dirty objects" and is not meant to imply that it's a dirty/hackish module.
See this answer for more details on dirty objects: What is meant by the term "dirty object"?
Here's what I have ended up doing. I like it:
class User < ActiveRecord::Base
attr_accessor :watched
def field_watch(field_name)
self.watched ||= {}
self.watched[field_name] = self.send(field_name)
return self
end
def field_changed?(field_name)
self.send(field_name) != self.watched(field_name)
end
end
And in the controller
def update
#user = current_user.field_watch(:name)
#user.assign_attributes(params[:user])
#user.generate_slug if #user.field_changed?(:name)
#user.save
end
I'll report here if I take the time to wrap this code in a gem or something.
How do you define a method for an attribute of an instance in Ruby?
Let's say we've got a class called HtmlSnippet, which extends ActiveRecord::Base of Rails and has got an attribute content. And, I want to define a method replace_url_to_anchor_tag! for it and get it called in the following way;
html_snippet = HtmlSnippet.find(1)
html_snippet.content = "Link to http://stackoverflow.com"
html_snippet.content.replace_url_to_anchor_tag!
# => "Link to <a href='http://stackoverflow.com'>http://stackoverflow.com</a>"
# app/models/html_snippet.rb
class HtmlSnippet < ActiveRecord::Base
# I expected this bit to do what I want but not
class << #content
def replace_url_to_anchor_tag!
matching = self.match(/(https?:\/\/[\S]+)/)
"<a href='#{matching[0]}'/>#{matching[0]}</a>"
end
end
end
As content is an instance of String class, redefine String class is one option. But I don't feel like to going for it because it overwrites behaviour of all instances of String;
class HtmlSnippet < ActiveRecord::Base
class String
def replace_url_to_anchor_tag!
...
end
end
end
Any suggestions please?
The reason why your code is not working is simple - you are working with #content which is nil in the context of execution (the self is the class, not the instance). So you are basically modifying eigenclass of nil.
So you need to extend the instance of #content when it's set. There are few ways, there is one:
class HtmlSnippet < ActiveRecord::Base
# getter is overrided to extend behaviour of freshly loaded values
def content
value = read_attribute(:content)
decorate_it(value) unless value.respond_to?(:replace_url_to_anchor_tag)
value
end
def content=(value)
dup_value = value.dup
decorate_it(dup_value)
write_attribute(:content, dup_value)
end
private
def decorate_it(value)
class << value
def replace_url_to_anchor_tag
# ...
end
end
end
end
For the sake of simplicity I've ommited the "nil scenario" - you should handle nil values differently. But that's quite simple.
Another thing is that you might ask is why I use dup in the setter. If there is no dup in the code, the behaviour of the following code might be wrong (obviously it depends on your requirements):
x = "something"
s = HtmlSnippet.find(1)
s.content = x
s.content.replace_url_to_anchor_tag # that's ok
x.content.replace_url_to_anchor_tag # that's not ok
Wihtout dup you are extending not only x.content but also original string that you've assigned.