Start method just once for addition / removal of association elements - ruby-on-rails

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.

Related

How to improve lisibility/split "fat" class method (inside ruby class/rails model)

as a new Rubyist, I'm running into a recurring problem when it comes to structure my models.
When a method is too long:
I try to refactor to a better/shorter syntax
I try to split some parts into "sub methods"
PROBLEM: I don't know how to split the method properly + whith which tool (private method, modules etc.)
For example:
I need to run Foo.main_class_method
My model looks like this:
class Foo < Applicationrecord
def self.main_class_method
[...] # way too long method with nasty iterations
end
end
I try to split my method to improve lisibility. It becomes :
class Foo < Applicationrecord
def self.main_class_method
[...] # fewer code
self.first_splitted_class_method
self.second_splitted_class_method
end
private
def self.first_splitted_class_method
[...] # some code
end
def self.second_splitted_class_method
[...] # some code
end
end
Result: It works, but I fell like this is not the proper way to do it + I have side effects
expected: splitted_methods are not accessible, except inside main_class_method
got: I can call Foo.first_splitted_class_method since class methods "ignore" Private. splitted_class_methods under Private are not private
Question: Is it an acceptable way to split main_class_method or is it a complete misuse of private method ?
Using private method to split your code:
Possible but not the real solution if the code belongs somewhere else
It's rather about "does it belongs here?" than "does it look nicer?"
To fix the "not private" private class method (original post) :
use private_class_method :your_method_name after you defined it
or right before
private_class_method def your_method_name
[...] # your code
end
If your splitting a class/instance method:
the splitted_method must be the same type(class/instance) as the main_class_method calling it
In the main_method you can call the splitted_method with or without using self.method syntax
class Foo < Applicationrecord
def self.main_class_method
# Here, self == Foo class
# first_splitted_class == class method, I can call self.first_splitted_class_method
self.first_splitted_class_method
# I can also call directly without self because self is implicit
second_splitted_class_method
end
def self.first_splitted_class_method
end
def self.second_splitted_class_method
end
private_class_method :first_splitted_class_method, :second_splitted_class_method
end

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.

Inherit a class from a gem and add local methods

I use a gem to manage certain attributes of a gmail api integration, and I'm pretty happy with the way it works.
I want to add some local methods to act on the Gmail::Message class that is used in that gem.
i.e. I want to do something like this.
models/GmailMessage.rb
class GmailMessage < Gmail::Message
def initialize(gmail)
#create a Gmail::Message instance as a GmailMessage instance
self = gmail
end
def something_clever
#do something clever utilising the Gmail::Message methods
end
end
I don't want to persist it. But obviously I can't define self in that way.
To clarify, I want to take an instance of Gmail::Message and create a GmailMessage instance which is a straight copy of that other message.
I can then run methods like #gmail.subject and #gmail.html, but also run #gmail.something_clever... and save local attributes if necessary.
Am I completely crazy?
You can use concept of mixin, wherein you include a Module in another class to enhance it with additional functions.
Here is how to do it. To create a complete working example, I have created modules that resemble what you may have in your code base.
# Assumed to be present in 3rd party gem, dummy implementation used for demonstration
module Gmail
class Message
def initialize
#some_var = "there"
end
def subject
"Hi"
end
end
end
# Your code
module GmailMessage
# You can code this method assuming as if it is an instance method
# of Gmail::Message. Once we include this module in that class, it
# will be able to call instance methods and access instance variables.
def something_clever
puts "Subject is #{subject} and #some_var = #{#some_var}"
end
end
# Enhance 3rd party class with your code by including your module
Gmail::Message.include(GmailMessage)
# Below gmail object will actually be obtained by reading the user inbox
# Lets create it explicitly for demonstration purposes.
gmail = Gmail::Message.new
# Method can access methods and instance variables of gmail object
p gmail.something_clever
#=> Subject is Hi and #some_var = there
# You can call the methods of original class as well on same object
p gmail.subject
#=> "Hi"
Following should work:
class GmailMessage < Gmail::Message
def initialize(extra)
super
# some additional stuff
#extra = extra
end
def something_clever
#do something clever utilising the Gmail::Message methods
end
end
GmailMessage.new # => will call first the initializer of Gmail::Message class..
Building upon what the other posters have said, you can use built-in class SimpleDelegator in ruby to wrap an existing message:
require 'delegate'
class MyMessage < SimpleDelegator
def my_clever_method
some_method_on_the_original_message + "woohoo"
end
end
class OriginalMessage
def some_method_on_the_original_message
"hey"
end
def another_original_method
"zoink"
end
end
original = OriginalMessage.new
wrapper = MyMessage.new(original)
puts wrapper.my_clever_method
# => "heywoohoo"
puts wrapper.another_original_method
# => "zoink"
As you can see, the wrapper automatically forwards method calls to the wrapped object.
I'm not sure why you can't just have a simple wrapper class...
class GmailMessage
def initialize(message)
#message = message
end
def something_clever
# do something clever here
end
def method_missing(m, *args, &block)
if #message.class.instance_methods.include?(m)
#message.send(m, *args, &block)
else
super
end
end
end
Then you can do...
#my_message = GmailMessage.new(#original_message)
#my_message will correctly respond to all the methods that were supported with #original_message and you can add your own methods to the class.
EDIT - changed thanks to #jeeper's observations in the comments
It's not the prettiest, but it works...
class GmailMessage < Gmail::Message
def initialize(message)
message.instance_variables.each do |variable|
self.instance_variable_set(
variable,
message.instance_variable_get(variable)
)
end
end
def something_clever
# do something clever here
end
end
Thanks for all your help guys.

Wrap Ruby methods at run-time to intercept method calls

I have a parent class that is similar to ActiveRecord, and I'm trying to open the class on load in our test suite. I want to wrap the initialize method so that it maintains a list of all of the subclasses that are initialized so I can make sure that all of the data for those classes is cleaned up between tests. Running a full wipe between tests winds up being too inefficient (plus I'm just interested in what the code would like to do this is).
My goal is to insert a new initialize method in the inheritance tree and just call super. All the while maintaining some list of all of the instantiated classes inside the parent class.
My attempt so far:
class NewActiveRecord
##__instantiated_classes = Set.new
def initialize(*args)
##__instantiated_classes.add(self.class)
super *args
end
def self.reset_tracking
##__instantiated_classes = Set.new
end
def self.get_instantiated_classes
##__instantiated_classes.to_a
end
end
class ActiveSupport::TestCase
teardown do
NewActiveRecord.get_instantiated_classes.each {|c| c.destroy_all}
NewActiveRecord.reset_tracking
end
end
Basically I want to wrap all methods that are called on subclasses of some parent to send their class to some predefined object their class
Did some exploring today and came up with the following solution. I'm still not sure how to unspy something yet though:
module Spy
def self.on_instance_method(mod, method, &block)
mod.class_eval do
# Stash the old method
old_method = instance_method(method)
# Create a new proc that will call both our block and the old method
proc = Proc.new do
block.call if block
old_method.bind(self).call
end
# Bind that proc to the original module
define_method(method, proc)
end
end
def self.on_class_method(mod, method, &block)
mod.class_eval do
# Stash the old method
old_method = singleton_method(method)
# Create a new proc that will call both our block and the old method
proc = Proc.new do
block.call if block
old_method.call
end
# Bind that proc to the original module
define_singleton_method(method, proc)
end
end
end
Usage from my tests looks like:
count = 0
Spy.on_instance_method(FakeClass, :value) { count += 1 }
fake = FakeClass.new(6)
fake.value.must_equal 6
count.must_equal 1

Model code to module: wrong number of args, class/ instance method?

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.

Resources