I'm getting an error when trying to call a concern method within a models method. I have several concerns and I've set them up in the same way. The strange thing is that one method works but the other one doesn't. Here is my code.
param_set.rb
class ParamSet < ApplicationRecord
include Searchable
include Comparable
def self.import(file, user_id, group_id = 1, project_id = nil)
param_set_ids_map = {}
yml = YAML.load_file(file)
yml.each do |component, elements|
case component
when 'param_sets'
elements.each do |importing_attributes|
old_param_set_id = importing_attributes.delete('id')
importing_attributes['user_id'] = user_id
importing_attributes['group_id'] = group_id
importing_attributes['project_id'] = project_id
ParamSet.search(User.first, {search: "test"})
ParamSet.compare(importing_attributes)
end
end
end
param_set_ids_map
end
end
comparable.rb
module Comparable
extend ActiveSupport::Concern
module ClassMethods
def compare(importing_attributes)
logger.debug "Works!"
end
end
end
searchable.rb
module Searchable
extend ActiveSupport::Concern
module ClassMethods
def search(current_user, params, visible: true)
results = paged_filter(current_user, params[:scope], params[:page] || 1)
results = results.visible if visible
results.where!("lower(name) LIKE lower(?) OR lower(description) LIKE lower(?)", "%#{params[:search]}%", "%#{params[:search]}%")
if params[:order].blank?
results.order!('updated_at DESC')
else
results.order!(params[:order])
end
results.reverse_order! if params[:reverse] == 'true'
results || []
end
end
end
The error that I get is:
undefined method `compare' for #<Class:0x000056167cb01718>
The method ParamSet.search(User.first, {search: "test"}) works find and doesn't give an error. The method ParamSet.compare(importing_attributes) however gives the error. I don't know what is going on and what the difference is between calling the two concern methods within a model method.
Can anybody explain what is going on?
When you do
include Comparable
you're including the Comparable module of ruby, not your own module. That module doesn't have a compare method.
Try changing your module name...
module Matchable
and the file to matchable.rb
and then
include Matchable
And you should be ok.
Related
I'm confused about using "include" vs "extend, after searching for hours all I got is that module methods used with instance of the class including the module, and module methods used with the class itself when the class extending the module of those methods.
but this didn't help me to figure out, why this code give error when commenting the extend module line in "#extend Inventoryable"
while work when uncomment it, here's the code
module Inventoryable
def create(attributes)
object = new(attributes)
instances.push(object)
return object
end
def instances
#instances ||= []
end
def stock_count
#stock_count ||= 0
end
def stock_count=(number)
#stock_count = number
end
def in_stock?
stock_count > 0
end
end
class Shirt
#extend Inventoryable
include Inventoryable
attr_accessor :attributes
def initialize(attributes)
#attributes = attributes
end
end
shirt1 = Shirt.create(name: "MTF", size: "L")
shirt2 = Shirt.create(name: "MTF", size: "M")
puts Shirt.instances.inspect
the output is
store2.rb:52:in `<main>': undefined method `create' for Shirt:Class (NoMethodError)
while when uncomment the "extend Inventoryable" to make the code work:
module Inventoryable
def create(attributes)
object = new(attributes)
instances.push(object)
return object
end
def instances
#instances ||= []
end
def stock_count
#stock_count ||= 0
end
def stock_count=(number)
#stock_count = number
end
def in_stock?
stock_count > 0
end
end
class Shirt
extend Inventoryable
include Inventoryable
attr_accessor :attributes
def initialize(attributes)
#attributes = attributes
end
end
shirt1 = Shirt.create(name: "MTF", size: "L")
shirt2 = Shirt.create(name: "MTF", size: "M")
puts Shirt.instances.inspect
makes the code work and output the following
[#<Shirt:0x0055792cb93890 #attributes={:name=>"MTF", :size=>"L"}>, #<Shirt:0x0055792cb937a0 #attributes={:name=>"MTF", :size=>"M"}>]
it's kinda confusing, but all I need to know, is why I need to extend the module in order to avoid the error ?, and how to edit this code to make it work without the extend method ? , what's left in the code that still depends on the extend ?
When you extend a module, the methods in that module become "class methods"**. So, when you extend Inventoryable, create becomes available as a method on the Shirt class.
When you include a module, the methods in that module become "instance methods"**. So, when you include Inventoryable, create is not available on the Shirt class (but is available on an instance of Shirt).
To make create available on the Shirt class when using include, you can use the included hook. That might look something like:
module Inventoryable
module ClassMethods
def create
puts "create!"
end
end
module InstanceMethods
end
def self.included(receiver)
receiver.extend ClassMethods
receiver.include InstanceMethods
end
end
Then if you do:
class Shirt
include Invetoryable
end
You can do:
> Shirt.create
create!
=> nil
** The ruby purists in the crowd will correctly point out that, in ruby, everything is an instance method and that there are no class methods. That is formally 100% correct, but we'll use the colloquial meaning of class and instance methods here.
When you extend a module in a class, you get the module's methods exposed as class methods but if you include the module then you get the module's method as instance methods, in your example for you to be able to call create method of Inventoryable class you need to invoke it using an instance of Shirt class (if you include the module)
shirt1 = Shirt.new(attributes).create(attributes)
Without more info I can't tell what you are trying to do but you need to redesign the initialize and create methods to decide where or what to do in those methods.
I'll try to explain it using a simple example
module A
def test
puts "ok"
end
end
class B
include A
end
class C
extend A
end
puts C.test # here you invoke the method against the class itself
puts B.new.test #here you create an instance to do it
Hope it helps.
At the end of the day, it's really simple:
C.include(M) makes the current superclass of C the superclass of M and M the superclass of C. In other words, it inserts M into C's ancestry chain.
obj.extend(M) is (roughly) the same as obj.singleton_class.include(M).
I have the following code to represent different Value Objects in Ruby. The only thing that changes between different classes is the INITIALIZATION_ATTRIBUTES array, which represents the list of attributes of the value object. I can't find a way to DRY this code. I tried to use a Module and accessing the included classes' Constants, but I run into the weird Constant lookup behavior described here. Essentially, the Module code is evaluated multiple times and it interprets the constant of the lastly evaluated class and applies its values to all the Value Object classes.
Is there any better alternative? I also tried with a base class, but I couldn't make it work.
module Values
class MaintenanceRegimeSerializer
INITIALIZATION_ATTRIBUTES = [:distance_between_services, :months_between_services]
def self.load(json)
json ||= '{}'
hash = JSON.parse json, symbolize_names: true
self.new(*INITIALIZATION_ATTRIBUTES.map {|key| hash[key]})
end
def self.dump(obj)
unless obj.is_a?(self)
raise ::ActiveRecord::SerializationTypeMismatch,
"Attribute was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
end
obj.to_json
end
attr_reader *INITIALIZATION_ATTRIBUTES
define_method :initialize do |*args|
raise ArgumentError unless INITIALIZATION_ATTRIBUTES.length == args.length
INITIALIZATION_ATTRIBUTES.each_with_index do |attribute, index|
instance_variable_set "##{attribute}", args[index]
end
end
end
end
This can be done by layering two modules. The outer module will provide the functionality to initialize the inner module. Because class attributes are used, which are unique for every including class, one including class' attributes can not conflict with another including class' attributes.
module Values
module MaintenanceRegimeSerializer
extend ActiveSupport::Concern
class_methods do
def acts_as_maintenance_regime_serializer(attributes)
# include the inner module
# thereby adding the required methods and class attributes
include JsonMethods
# set the class variables made available by including the inner module
self.serializer_attributes = attributes
end
end
module JsonMethods
extend ActiveSupport::Concern
included do
class_attribute :serializer_attributes
def initialize(*args)
raise ArgumentError unless self.class.serializer_attributes.length == args.length
self.class.serializer_attributes.each_with_index do |attribute, index|
instance_variable_set "##{attribute}", args[index]
end
end
end
class_methods do
def load(json)
json ||= '{}'
hash = JSON.parse json, symbolize_names: true
new(*serializer_attributes.map {|key| hash[key]})
end
def dump(obj)
unless obj.is_a?(self)
raise ::ActiveRecord::SerializationTypeMismatch,
"Attribute was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
end
obj.to_json
end
end
end
end
end
# in the including class
class SomeClass
# This might also be put into an initializer patching ActiveRecord::Base
# to avoid having to call this in every class desiring the regime serializer functionalit
include Values::MaintenanceRegimeSerializer
acts_as_maintenance_regime_serializer([:distance_between_services,
:months_between_services])
end
# in another including class
class SomeOtherClass
include Values::MaintenanceRegimeSerializer
acts_as_maintenance_regime_serializer([:foo,
:bar])
end
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 have a Postgresql array column in my Rails app. These are, as far as I can tell, stored as long strings.
To search this column (Place#name), I've implemented the following methods in a module that I include like so:
class Place
include ActiveRecord::MetaExt::ArrayAccessor
array_accessor :name, :category
end
and define like so:
module ActiveRecord
module MetaExt
module ArrayAccessor
module ClassMethods
def array_accessor(*symbols)
symbols.each do |singular|
plural = singular.to_s.split("_").join(" ").pluralize.split(" ").join("_")
# These two instance-level methods not super relevant to the question
class_eval do
define_method("add_#{singular}") do |arg|
send( "#{plural}=", (send(plural) + Array(arg)).uniq )
end
define_method(singular) do
send(plural).first
end
end
metaclass.instance_eval do
define_method("without_#{singular}") do
where("? = '{}'", plural)
end
define_method("with_#{singular}") do |arg=nil|
if arg.blank?
where.not("? = '{}'", plural)
else
where("? = ANY (#{plural})", arg.is_a?(Array) ? arg.first : arg)
end
end
end
end
end
end
def self.included(base)
base.extend ClassMethods
end
end
end
end
There are a couple of issues with this.
1) in the with_name (literally, with_#{field}) method, I'm only ever searching for the first name in the array given. I'd love to be able to search for all places containing ANY of those names. Having trouble figuring this out, though. Any ideas?
2) I'd love to be able to do fuzzy matching -- so that a search for "Barbeque Place" would grab a place with the name "The Barbecue Place". Took a look at the Textacular gem, but I can't seem to adapt it to Array columns.
Any ideas how to make this work -- especially in a generalizable way?
I've got a module in my project in lib/. it's content is like this :
module Search
module Score
def get_score
return 'something'
end
end
end
This Search has many different modules I need to use Score. I realize I need to add require in my model (I'm trying to use this from model). So here is my code (model) :
require 'search'
class User < ActiveRecord::Base
def get_user_score
#tried this :
p Search::Score.get_score #error
#this as well
score_instance = Score.new #error
score = Search::Score.get_score # error undefined method `get_score'
end
end
So how do I reuse the code I have in other class (module)?
To get it working you can either mix the module into your class:
require 'search'
class User < ActiveRecord::Base
include Search::Score
def get_user_score
p get_score # => "something"
end
end
Or you can define the method inside your module similar to class methods:
module Search
module Score
def self.get_score
return 'something'
end
end
end
If you do that, you can call get_score like expected:
require 'search'
class User < ActiveRecord::Base
def get_user_score
p Search::Score.get_score # => "something"
end
end
See this tutorial for a more in depth explanation about modules in Ruby.
First, see "Best Practices for reusing code between controllers in Ruby on Rails".
About reuse code as a module, take a look at "Rethinking code reuse with Modularity for Ruby".
"Modules are crippled classes"
Modules are like crippled classes in Ruby. If you look into the inheritance chain you see that a Class actually inherits from Module.
Module cannot be instanciated. So the call to .new is not working.
What you CAN do however is to specify your method as a 'class' method (I know I said it is not a class...)
So you would add a self in front like this:
module Search
module Score
def self.get_score
return 'something'
end
end
end
Then you can call this method as a class method like you tried in your code example
Search::Score is a module and not a class, so Score.new will not work.
You can try to change the signature of the get_score function to self.get_score.
In addition to def self.get_score in the above answers, there is also extend self, like so:
module Search
module Score
extend self
def get_score
return 'something'
end
end
end
and module_function:
module Search
module Score
module_function
def get_score
return 'something'
end
end
end
The latter is actually the preferred method in RuboCop (source), though in practice I personally have not seen it so often.