Model code to module: wrong number of args, class/ instance method? - ruby-on-rails

I am trying to move some model code to a module.
The original model method:
I am trying to move some model code to a module.
The original model method:
class Book < ActiveRecord::Base
def book_royalty(period='enddate', basis="Net receipts")
#stuff
end
end
So I add
include Calculation
and move the method to a module:
module Calculation
def book_royalty(period='enddate', basis="Net receipts")
#stuff
end
end
But now I'm getting
wrong number of arguments (2 for 0)
This is the error I also get if I make the method in the book.rb model a class method i.e. if I make the method name self.book_royalty(args).
Am I inadvertently making the methods moved to the module class methods? I'm using include in book.rb, not extend. How can I get the parent model to successfully include the module's methods?
Edit
book_royalty is called in the Royaltystatement model.
book.rb:
attr_accessor :book_royalty
royaltystatement.rb:
def initialize_arrays
#royalty = []
...
end
def sum_at_book_level(contract)
contract.books.where(:exclude_from_royalty_calc => false).each do |book|
book.book_royalty("enddate", basis)
#royalty.push(book.book_royalty("enddate", basis))
# etc
end

Explanation:
Your module defines a method book_royalty that takes two arguments. Then, a couple of lines after the inclusion of that module you use class macro attr_accessor which defines two methods,
def book_royalty
#book_royalty
end
def book_royalty= val
#book_royalty = val
end
This effectively overwrites your book_royalty from the module. Now it accepts no arguments. Hence the error
wrong number of arguments (2 for 0)
when trying to execute line
book.book_royalty("enddate", basis)
You don't need attr_accessor or anything else in order to use a method from included module. It becomes available automatically.

Related

Start method just once for addition / removal of association elements

I have a Composition model which has a has_and_belongs_to_many :authors.
I need to fire a method after a composition changed its authors, although, since it involves the creation of a PDF file (with the name of the authors), I want to call this method only once, regardless of the number of authors added / removed.
Of course I can add / remove existing authors from the composition, so a before_save / after_save won't work here (somehow it recognizes new authors added to the composition, but not existing ones).
So I tried using after_add / after_remove, but the callbacks specified here will be invoked for every author item added to / removed from the composition.
Is there a way to have a method called only once for every "batch action" of adding / removing items from this kind of relationship?
Here's what a service might look like:
class UpdateCompositionAuthorsService
attr_accessor *%w(
args
).freeze
class << self
def call(args={})
new(args).call
end
end # Class Methods
#======================================================================================
# Instance Methods
#======================================================================================
def initialize(args={})
#args = args
assign_args
end
def call
do_stuff_to_update_authors
generate_the_pdf
end
private
def do_stuff_to_update_authors
# do your 'batch' stuff here
end
def generate_the_pdf
# do your one-time logic here
end
def assign_args
args.each do |k,v|
class_eval do
attr_accessor k
end
send("#{k}=",v)
end
end
end
You would call it something like:
UpdateCompositionAuthorsService.call(composition: #composition, authors: #authors)
I got sick of remembering what args to send to my service classes, so I created a module called ActsAs::CallingServices. When included in a class that wants to call services, the module provides a method called call_service that lets me do something like:
class FooClass
include ActsAs::CallingServices
def bar
call_service UpdateCompositionAuthorsService
end
end
Then, in the service class, I include some additional class-level data, like this:
class UpdateCompositionAuthorsService
SERVICE_DETAILS = [
:composition,
:authors
].freeze
...
def call
do_stuff_to_update_authors
generate_the_pdf
end
...
end
The calling class (FooClass, in this case) uses UpdateCompositionAuthorsService::SERVICE_DETAILS to build the appropriate arguments hash (detail omitted).
I also have a method called good_to_go? (detail omitted) that is included in my service classes, so my call method typically looks like:
class UpdateCompositionAuthorsService
...
def call
raise unless good_to_go?
do_stuff_to_update_authors
generate_the_pdf
end
...
end
So, if the argument set is bad, I know right away instead of bumping into a nil error somewhere in the middle of my service.

Rails concern method override another concern method doesn't work like normal modules

Let's say I have the following structure in ruby (no rails)
module Parent
def f
puts "in parent"
end
end
module Child
def f
super
puts "in child"
end
end
class A
include Parent
include Child
end
A.new.f # prints =>
#in parent
#in child
Now when using rails concerns
module Parent
extend ActiveSupport::Concern
included do
def f
puts "In Parent"
end
end
end
module Child
extend ActiveSupport::Concern
included do
def f
super
puts "In Child"
end
end
end
class A < ActiveRecord::Base
include Parent
include Child
end
A.new.f #exception
NoMethodError: super: no superclass method `f' for #<A:0x00000002244490>
So what am I missing here? I need to use super in concerns like in normal modules. I searched but I could not find help on this topic
The reason for this is that included method block is actually evaluated in the context of the class. That mean, that method defined in it is defined on a class when module is included, and as such takes precedence over included modules.
module Child1
extend ActiveSupport::Concern
included do
def foo
end
end
end
module Child2
def bar
end
end
class A
include Child1
include Child2
end
A.new.method(:foo).owner #=> A
A.new.method(:bar).owner #=> Child2
Method lookup
In ruby, every time you want to call a method, ruby has to find it first (not knowing whether it is method or a variable). It is done with so called method lookup. When no receiver is specified (pure call like puts) it firstly searches the current scope for any variables. When not found it searches for that method on current self. When receiver is specified (foo.bar) it naturally search for the method on given receiver.
Now the lookup - in ruby all the methods always belongs to some module/class. The first in the order is receiver's eigenclass, if it exists. If not, regular receiver's class is first.
If the method is not found on the class, it then searches all the included modules in given class in the reversed order. If nothing is found there, superclass of given class is next. The whole process goes recursively until something is found. When lookup reaches BasicObject and fails to find the method it quit and triggers search for method_missing, with default implementation defined on BasicObject.
Important thing to notice is that methods which belongs to the class always take precedence over module methods:
module M
def foo
:m_foo
end
end
class MyClass
def foo
:class_foo
end
include M
end
MyClass.new.foo #=> :class_foo
About super
Search for a super method is very similar - it is simply trying to find a method with the same name which is further in the method lookup:
module M1
def foo
"M1-" + super
end
end
module M2
def foo
'M2-' + super
end
end
module M3
def foo
'M3-' + super
end
end
class Object
def foo
'Object'
end
end
class A
include M2
include M3
end
class B < A
def foo
'B-' + super
end
include M1
end
B.new.foo #=> 'B-M1-M3-M2-Object'
ActiveSupport::Concern#included
included is a very simple method that takes a block and creates a self.included method on the current module. The block is executed using instance_eval, which means that any code in there is actually executed in the context of the class given module is being included in. Hence, when you define a method in it, this method will be owned by the class including the module, not by the module itself.
Every module can hold only one method with given name, once you tries to define second one with the same name, the previous definition is completely erased and there is no way it can be find using ruby method lookup. Since in your example you included two modules with same method definition in included block, the second definition completely overrides the first one and there is no other definition higher in method lookup, so super is bound to fail.

Rails: Concern with before_filter type of method

I am just getting my hands on Concerns in Rails and try to implement a simple logging for ActiveRecord classes. In there I want to define the field that should go into the log and have the log written automatically after save.
What I have is this:
#logable.rb (the concern)
module Logable
extend ActiveSupport::Concern
#field = nil
module ClassMethods
def set_log_field(field)
#feild = field
end
end
def print_log
p "LOGGING: #{self[#index.to_s]}"
end
end
#houses.rb (the model using the concern)
class House < ActiveRecord::Base
include Logable
after_save :print_log
set_log_field :id
end
Unfortunately the call to set_log_field does not have an effect - or rather the given value does not make it to print_log.
What am I doing wrong?
Thanks for your help!
You probably mean this (btw, why not Loggable?):
# logable.rb
module Logable
extend ActiveSupport::Concern
# Here we define class-level methods.
# Note, that #field, defined here cannot be referenced as #field from
# instance (it's class level!).
# Note also, in Ruby there is no need to declare #field in the body of a class/module.
class_methods do
def set_log_field(field)
#field = field
end
def log_field
#field
end
end
# Here we define instance methods.
# In order to access class level method (log_field), we use self.class.
included do
def print_log
p "LOGGING: #{self.class.log_field}"
end
end
end
Update You also asked about what's the difference between methods in included block and those within method body.
To make a short resume there is seemingly no difference. In very good approximation you can consider them the same. The only minor difference is in dependency management. Great illustration of it is given in the end of ActiveSupport::Concern documentation. It worth reading, take a look!

Returning Module Class instead of Model Class with self.class Ruby/Rails

I am trying to DRY my code by implementing modules. However, I have constants stored in models (not the module) that I am trying to access with self.class.
Here are (I hope) the relevant snippets:
module Conversion
def constant(name_str)
self.class.const_get(name_str.upcase)
end
end
module DarkElixir
def dark_elixir(th_level)
structure.map { |name_str| structure_dark_elixir(name_str, th_level) if constant(name_str)[0][:dark_elixir_cost] }.compact.reduce(:+)
end
end
class Army < ActiveRecord::Base
include Conversion, DarkElixir
TH_LEVEL = [...]
end
def structure_dark_elixir(name_str, th_level)
name_sym = name_str.to_sym
Array(0..send(name_sym)).map { |level| constant(name_str)[level][:dark_elixir_cost] }.reduce(:+) * TH_LEVEL[th_level][sym_qty(name)]
end
When I place the structure_dark_elixir method inside the DarkElixir module, I get an error, "uninitialized constant DarkElixir::TH_LEVEL"
While if I place it inside the Army class, it finds the appropriate constant.
I believe it is because I am not scoping the self.constant_get correctly. I would like to keep the method in question in the module as other models need to run the method referencing their own TH_LEVEL constants.
How might I accomplish this?
Why not just use class methods?
module DarkElixir
def dark_elixir(th_level)
# simplified example
th_level * self.class.my_th_level
end
end
class Army < ActiveRecord::Base
include DarkElixir
def self.my_th_level
5
end
end
Ugh. Method in question uses two constants. It was the second constant that was tripping up, not the first. Added "self.class::" prior to the second constant--back in business.
def structure_dark_elixir(name_str, th_lvl)
name_sym = name_str.to_sym
Array(0..send(name_sym)).map { |level| constant(name_str)[level][:dark_elixir_cost] }.reduce(:+) * self.class::TH_LEVEL[th_lvl][sym_qty(name_str)]
end

Injecting custom callback in rails 3

I have a custom module which sets up a hash to be stored in my sql. As part of this it rolls a its own _changed accessor.
module MyAwesomeCustomModule
extend ActiveSupport::Concern
included do
after_save: wipe_preferences_changed
end
module ClassMethods
def blah
end
etc
end
end
and then in my model:
class MyModel < ActiveRecord::Base
include MyAwesomeCustomModule
after_save :something_that_expects_preferences_changed_to_be_available
blah
end
unfortunately, the after_save defined in the custom module runs before the one defined in the model. Is there a way to get the array of all callbacks and append to it? Is there a way to write a custom after_after_save callback? Is there a way to specify priority/ordering of after_save callbacks?
What would be a good way to resolve this race condition?
In spite of the order of model callbacks, the current design makes the module and the class very coupled.
To solve the current problem as well as improve design, you can define an expected callback in the module's method, and then the class who includes this module is free to respond it or not.
module MyAwesomeCustomModule
extend ActiveSupport::Concern
included do
after_save: wipe_preferences_changed
end
def wipe_preferences_changed
# previous logic to wipe
process_further if respond_to :process_further
end
end
class MyModel < ActiveRecord::Base
include MyAwesomeCustomModule
# Feel free to write this or not
# The content is the previous
# :something_that_expects_preferences_changed_to_be_available
def process_further
end
end
If you want to keep your original strategy (2 after_save callbacks) all you should need to do is move the include statement below the model after_save.
class MyModel < ActiveRecord::Base
after_save :something_that_expects_preferences_changed_to_be_available
include MyAwesomeCustomModule
blah
end
Callbacks are executed in the order they are defined. The include statement acts (very roughly) like you had copy and pasted the code from the module at that point, so by putting the include statement above the after_save in your model you were causing that callback to execute first.

Resources