In my model, I'm trying to dynamically expose objects that are inside an array as a top level attribute. Here's the code snippet:
class Widget < ActiveRecord::Base
# attr_accessor :name
end
class MyModel < ActiveRecord::Base
has_many :widgets
#attr_accessor :widgets
after_initialize :init_widgets
def init_widgets
widgets
widgets.each_with_index do |widget, index|
define_method(widget.name) do
widgets[index]
end
end
end
end
Is there any way for me to define the value of index into the new method I am creating so that it will be associated with the proper index?
I might create an accessor / assignment methods that overload the [] operator:
class BracketOperator
def initialize
#values = (1..100).to_a
end
def [](index)
#values[index]
end
def []=(index, value)
#values[index] = value
end
end
bo = BracketOperator.new
bo[3] # => 4
bo[3] = 17
bo[3] # => 17
So for reference, here's the answer I came up with. Apparently I don't understand scoping for ruby too well. The variable n somehow remains referenced to the n within the each loop. Hence for my original question, I can just use the index variable within the method and it will be mapped to what I am expecting it to be mapped to.
class Test
def setup
names = [ "foo","bar" ]
names.each do |n|
self.class.send :define_method, n do
puts "method is called #{n}!"
end
end
end
end
Related
I am facing a design decision I cannot solve. In the application a user will have the ability to create a campaign from a set of different campaign types available to them.
Originally, I implemented this by creating a Campaign and CampaignType model where a campaign has a campaign_type_id attribute to know which type of campaign it was.
I seeded the database with the possible CampaignType models. This allows me to fetch all CampaignType's and display them as options to users when creating a Campaign.
I was looking to refactor because in this solution I am stuck using switch or if/else blocks to check what type a campaign is before performing logic (no subclasses).
The alternative is to get rid of CampaignType table and use a simple type attribute on the Campaign model. This allows me to create Subclasses of Campaign and get rid of the switch and if/else blocks.
The problem with this approach is I still need to be able to list all available campaign types to my users. This means I need to iterate Campaign.subclasses to get the classes. This works except it also means I need to add a bunch of attributes to each subclass as methods for displaying in UI.
Original
CampaignType.create! :fa_icon => "fa-line-chart", :avatar=> "spend.png", :name => "Spend Based", :short_description => "Spend X Get Y"
In STI
class SpendBasedCampaign < Campaign
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
Neither way feels right to me. What is the best approach to this problem?
A not very performant solution using phantom methods. This technique only works with Ruby >= 2.0, because since 2.0, unbound methods from modules can be bound to any object, while in earlier versions, any unbound method can only be bound to the objects kind_of? the class defining that method.
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
enum :campaign_type => [:spend_based, ...]
def method_missing(name, *args, &block)
campaign_type_module.instance_method(name).bind(self).call
rescue NameError
super
end
def respond_to_missing?(name, include_private=false)
super || campaign_type_module.instance_methods(include_private).include?(name)
end
private
def campaign_type_module
Campaigns.const_get(campaign_type.camelize)
end
end
# app/models/campaigns/spend_based.rb
module Campaigns
module SpendBased
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
end
Update
Use class macros to improve performance, and keep your models as clean as possible by hiding nasty things to concerns and builder.
This is your model class:
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
include CampaignAttributes
enum :campaign_type => [:spend_based, ...]
campaign_attr :name, :fa_icon, :avatar, ...
end
And this is your campaign type definition:
# app/models/campaigns/spend_based.rb
Campaigns.build 'SpendBased' do
name 'Spend Based'
fa_icon 'fa-line-chart'
avatar 'spend.png'
end
A concern providing campaign_attr to your model class:
# app/models/concerns/campaign_attributes.rb
module CampaignAttributes
extend ActiveSupport::Concern
module ClassMethods
private
def campaign_attr(*names)
names.each do |name|
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{name}
Campaigns.const_get(campaign_type.camelize).instance_method(:#{name}).bind(self).call
end
EOS
end
end
end
end
And finally, the module builder:
# app/models/campaigns/builder.rb
module Campaigns
class Builder < BasicObject
def initialize
#mod = ::Module.new
end
def method_missing(name, *args)
value = args.shift
#mod.send(:define_method, name) { value }
end
def build(&block)
instance_eval &block
#mod
end
end
def self.build(module_name, &block)
const_set module_name, Builder.new.build(&block)
end
end
I need defaults (from a remote service) in ModelA to be set before the object is passed to the view from ModelAController#new. I have used after_initialize for this. However, in #create I have a problem. If I use model_b.create_model_a(some_attributes), the attributes are passed in during initialization and then overwritten by the after_initialize call:
class ModelA < ActiveRecord::Base
after_initialize :set_defaults, if: :new_record?
def set_defaults
self.c = "default"
#actually a remote call, not something can be set as database default
end
end
class ModelB < ActiveRecord::Base
belongs_to :model_a
end
class ModelAController < ApplicationController
#ModelA is nested under ModelB in routes.rb
#GET /model_bs/:model_b_id/model_as/new
def new
model_b = ModelB.find(params[:model_b_id])
#no problem
respond_with model_b.build_model_a
end
#POST /model_bs/:model_b_id/model_as
def create
model_b = ModelB.find(params[:id])
#problem:
respond_with model_b.create_model_a({c: "not default"})
#at this point the model_a instance still has attribute c set to "default"
end
...
end
I could separate the create steps:
model_b = ModelB.find(params[:id])
model_a = model_b.build_model_a #should fire after_initialize
model_a.update_attributes({c: "not default"}) #overwrite default c value
But I feel like this makes the lifecycle of ModelA a bit of a trap for other programmers. This looks like an obvious candidate for refactoring the last two lines into one, but that would create this problem again. Is there a neater solution?
Make a conditional assignment:
def set_defaults
self.c ||= "default"
end
Alternatively instead of after_initialize hook set a default in attribute reader. That way you only set the default when you actually need attribute value, so it saves you a remote call if you don't need it:
def c
super || self.c = 'default'
end
I have a STI in place for 10 models inheriting from one ActiveRecord::Base model.
class Listing::Numeric < ActiveRecord::Base
end
class Listing::AverageDuration < Listing::Numeric
end
class Listing::TotalViews < Listing::Numeric
end
There are 10 such models those inherit from Listing::Numeric
In rails console, when I try for .descendants or .subclasses it returns an empty array.
Listing::Numeric.descendants
=> []
Listing::Numeric.subclasses
=> []
Ideally this should work.
Any ideas why its not returning the expected subclasses ?
This will work only if all the inherited classes are referenced in some running code as rails will load the classes when required then only it will be added as descendants
For eg:
Listing::Numeric.descendants.count
=> 0
Listing::AverageDuration
Listing::TotalViews
Listing::Numeric.descendants.count
=> 2
Old Q, but for anyone else like me who get confused with empty lists for subclasses MyClass.subclasses => []
You need to explicitly set the dependency to your MySubclass class.
class MyClass < ApplicationRecord
end
require_dependency 'my_subclass'
$ MyClass.subclasses
=> ['MySubclass']
https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoloading-and-sti
Execute Rails.application.eager_load! before calling the .descendants method.
This helped me
config.autoload_paths += %W( #{config.root}/app/models/listings )
Taken from here - http://hakunin.com/rails3-load-paths
Here is a solution for Rails 7. Originally sourced from RailsGuides, I made modifications to include results even if no data is stored in the table:
module StiPreload
unless Rails.application.config.eager_load
extend ActiveSupport::Concern
included do
cattr_accessor :preloaded, instance_accessor: false
end
class_methods do
def descendants
preload_sti unless preloaded
super
end
def preload_sti
types_from_db = base_class
.unscoped
.select(inheritance_column)
.distinct
.pluck(inheritance_column)
.compact
(types_from_db.present? || types_from_file).each do |type|
type.constantize
end
self.preloaded = true
end
def types_from_file
Dir::each_child("#{Rails.root}/app/models").reduce([]) do |acc, filename|
if filename =~ /^(#{base_class.to_s.split(/(?=[A-Z])/).first.downcase})_(\w+)_datum.rb$/
acc << "#{$1.capitalize}#{$2.classify}"
end
acc
end
end
end
end
end
class YoursTruly < ApplicationRecord
include StiPreload
end
YoursTruly.descendants # => [...]
I've defined a couple of virtual attributes, defining both the setter and the getters methods:
class Post < ActiveRecord::Base
def shared_to_all
# evaluates some expression of the attribute privacy
end
def shared_to_friends
# evaluates some other expression of the attribute privacy
end
def shared_to_all=(bool)
# write_attribute( :privacy, ... )
end
def shared_to_friends=(bool)
# write_attribute( :privacy, ... )
end
end
So far so good, but I want also to make this virtual attributes available using symbols, so I can do something like #post= Post.first; #post[:shared_to_all]= true
[Edit:]
A Ruby approach would be to override [] and []= operators, like in:
def [](shared_to_all)
shared_to_all
end
def []=(shared_to_all, bool)
self.shared_to_all= (bool)
end
But this seems to break Rails relationship methods (those brought by has_one - has_many - belongs_to - has_and_belongs_to_many directives): e.g. now Post.first.author => nil and Author.first.posts => []
Seems like this should do it:
def [](attr)
attr
end
def []=(attr, value)
self.attr = value
end
Is there any built-in way to attach an instance variable to a record? For example, suppose I have a User class with a foo attr_accessor:
class User < ActiveRecord::Base
...
attr_accessor :foo
end
If I do
u = User.first
u.foo = "bar"
u.foo # -> "bar"
User.find(1).foo # => nil
This is the correct behavior and I understand why: the instance variable exists for the instance (in memory) and not for the record. What I want is something like a record variable, so that in the above example u.foo and User.find(1).foo return the same value within the same instance of my application. I don't want persistence: it's not appropriate for foo to be a column in the table, it's just appropriate for foo to return the same value for the same record during the life cycle of e.g., a controller action, console session, etc. Nor do I want a class variable via cattr_accessor, because there's no reason that User.find(1).foo should be the same as User.find(2).foo.
The best bet I can come up with is to fake it with a class variable array and instance methods to get/set the appropriate element of the array:
class User < ActiveRecord::Base
cattr_accessor :all_the_foos
self.all_the_foos = Array.new
def foo
self.all_the_foos[id]
end
def foo= new_foo
self.all_the_foos[id] = new_foo
end
end
This ALMOST works, but it doesn't work with un-saved records, e.g. User.new.foo = "bar" fails.
You can use Thread.current to set variables that are active within the context of a controller action invocation. Your current implementation doesn't guarantee context reset across calls.
class User < ActiveRecord::Base
after_create :set_thread_var
def foo
new_record? ? #foo : Thread.current["User-foo-#{id}"]
end
def foo=(val)
new_record? ? (#foo = val) : (Thread.current["User-foo-#{id}"] = val)
end
private
def set_thread_var
Thread.current["User-foo-#{id}"] = #foo if defined?(#foo)
end
end
I can't think of a better idea than your class variable solution. To solve your "new" problem I'd use after_initialize.
class User < ActiveRecord::Base
after_initialize :init_foos
cattr_accessor :all_the_foos
def foo
self.all_the_foos[id]
end
def foo= new_foo
self.all_the_foos[id] = new_foo
end
private
def init_foos
##all_the_foos ||= []
end
end