Ruby reflection composition: call original method from redefined method - ruby

A bit of context first
I have a class Phone that defines a method advertise like this:
class Phone
def advertise(phone_call)
'ringtone'
end
end
I would like to have some adaptations for this method.
For example when the user is in a quiet environment, the phone should vibrate and not ring.
To do so, I define modules like
module DiscreetPhone
def advertise_quietly (phone_call)
'vibrator'
end
end
Then my program can do
# add the module to the class so that we can redefine the method
Phone.include(DiscreetPhone)
# redefine the method with its adaptation
Phone.send(:define_method, :advertise, DiscreetPhone.instance_method(:advertise_quietly ))
Of course for this example I hardcoded the class and module's name but they should be parameters of a function.
And so, an execution example would give:
phone = Phone.new
phone.advertise(a_call) # -> 'ringtone'
# do some adaptation stuff to redefine the method
...
phone.advertise(a_call) # -> 'vibrator'
Finally coming to my question
I want to have an adaptation that call the original function and append something to its result. I would like to write it like
module ScreeningPhone
def advertise_with_screening (phone_call)
proceed + ' with screening'
end
end
But I don't know what the proceed call should do or even where should I define it.
I'm using Ruby 2.3.0 on Windows.
proceed could be replaced by something else but I'd like to keep it as clean as possible in the module that defines the adaptation.

You can do this by prepending your module instead of including it.
Instead of using define_method as a sort of ersatz alias_method, just call the method advertise in your modules too.
Within your advertise method, you can call super to call up the inheritance hierarchy.

In my opinion, this approach is way too complex, and an inappropriate use of Modules.
I recommend thinking about a simpler way to implement this.
One simple way is to just include all the methods in the Phone class.
Or, you could use a hash as a lookup table for ring strategies:
class Phone
attr_accessor :ring_strategy
RING_STRATEGIES = {
ringtone: -> { ring_with_tone },
discreet: -> { ring_quietly },
screening: -> { ring_with_tone; ring_screening_too }
# ...
}
def initialize(ring_strategy = :ringtone)
#ring_strategy = ring_strategy
end
def ring
RING_STRATEGIES[:ring_strategy].()
end
end

Related

How to disable rspec's checking for access to double outside of original example?

A class double is created in a let which is accessed by multiple examples. For example:
let(:foo_klass) do
class_double('A::B::C::Foo')
end
Other unit tests use ObjectSpace.each_object(Class) to examine the classes to select, for example, all modules including another module:
def find_modules_including_module(target_module)
ObjectSpace.each_object(Module).select do |object|
# All Class'es are modules, but we are not interested in them, so we exclude them.
!object.is_a?(Class) \
&& \
object.ancestors.include?(target_module) \
&& \
!object.equal?(target_module)
end
end
It is only the unit tests that call find_modules_including_module that fail. The error looks something like this:
Failure/Error: object.ancestors.include?(target_module) \
#<ClassDouble(MyModule::MyClass) (anonymous)> was originally created in one example but has leaked into another example and can no longer be used. rspec-mocks' doubles are designed to only last for one example, and you need to create a new one in each example you wish to use it for.
I understand why failing on any access on the double class is almost always the right thing to do, but mine is a special case in which that behavior is problematic and IMO inappropriate.
Is there an option to suppress this temporarily? Or do you have another suggestion?
In response to #engineersmnky's comments, and to provide context, I am adding the following:
I am doing this whole exercise because I am using the SemanticLogger logging gem, which provides a Loggable module which provides a class method and an instance method named 'logger'. The instance method calls the class method (self.class.logger).
If a module (let's call it M) includes Loggable, and a class (let's call it C) includes M, then C will assimilate M's instance method as if it were its own. When it is called, it will try to get self.class.logger, but self is the C instance and not the module, and since there is no C class method logger, a NoMethodError is raised.
My solution is to add this to every module that includes Loggable:
def self.included(klass)
klass.class_exec do
include SemanticLogger::Loggable
end
end
This works, but there is a risk that in the future a new module will include Loggable but not this included implementation. I want to be able to detect this situation programmatically in a test. My current test looks like this:
specify 'all classes including modules that include Loggable include Loggable themselves' do
modules_including_loggable = modules_including_module(SemanticLogger::Loggable)
classes_including_those_modules = classes_including_modules(modules_including_loggable)
suspicious_classes = classes_not_having_ancestor(classes_including_those_modules, SemanticLogger::Loggable)
expect(suspicious_classes.map(&:name).sort.join("\n")).to eq('')
end
The methods used are defined as follows:
def modules_including_module(target_module)
ObjectSpace.each_object(Module).select do |object|
# All Class'es are modules, but we are not interested in them, so we exclude them.
!object.is_a?(Class) \
&& \
object.ancestors.include?(target_module) \
&& \
!object.equal?(target_module)
end
end
def classes_including_modules(modules)
ObjectSpace.each_object(Class).select do |klass|
(klass.ancestors & modules).any?
end
end
def classes_not_having_ancestor(classes, ancestor)
classes.select do |klass|
!klass.ancestors.include?(ancestor)
end
end

Dry::Web::Container yielding different objects with multiple calls to resolve

I'm trying write a test to assert that all defined operations are called on a successful run. I have the operations for a given process defined in a list and resolve them from a container, like so:
class ProcessController
def call(input)
operations.each { |o| container[o].(input) }
end
def operations
['operation1', 'operation2']
end
def container
My::Container # This is a Dry::Web::Container
end
end
Then I test is as follows:
RSpec.describe ProcessController do
let(:container) { My::Container }
it 'executes all operations' do
subject.operations.each do |op|
expect(container[op]).to receive(:call).and_call_original
end
expect(subject.(input)).to be_success
end
end
This fails because calling container[operation_name] from inside ProcessController and from inside the test yield different instances of the operations. I can verify it by comparing the object ids. Other than that, I know the code is working correctly and all operations are being called.
The container is configured to auto register these operations and has been finalized before the test begins to run.
How do I make resolving the same key return the same item?
TL;DR - https://dry-rb.org/gems/dry-system/test-mode/
Hi, to get the behaviour you're asking for, you'd need to use the memoize option when registering items with your container.
Note that Dry::Web::Container inherits Dry::System::Container, which includes Dry::Container::Mixin, so while the following example is using dry-container, it's still applicable:
require 'bundler/inline'
gemfile(true) do
source 'https://rubygems.org'
gem 'dry-container'
end
class MyItem; end
class MyContainer
extend Dry::Container::Mixin
register(:item) { MyItem.new }
register(:memoized_item, memoize: true) { MyItem.new }
end
MyContainer[:item].object_id
# => 47171345299860
MyContainer[:item].object_id
# => 47171345290240
MyContainer[:memoized_item].object_id
# => 47171345277260
MyContainer[:memoized_item].object_id
# => 47171345277260
However, to do this from dry-web, you'd need to either memoize all objects auto-registered under the same path, or add the # auto_register: false magic comment to the top of the files that define the dependencies and boot them manually.
Memoizing could cause concurrency issues depending on which app server you're using and whether or not your objects are mutated during the request lifecycle, hence the design of dry-container to not memoize by default.
Another, arguably better option, is to use stubs:
# Extending above code
require 'dry/container/stub'
MyContainer.enable_stubs!
MyContainer.stub(:item, 'Some string')
MyContainer[:item]
# => "Some string"
Side note:
dry-system provides an injector so that you don't need to call the container manually in your objects, so your process controller would become something like:
class ProcessController
include My::Importer['operation1', 'operation2']
def call(input)
[operation1, operation2].each do |operation|
operation.(input)
end
end
end

Ruby: Include a dynamic module name

I have a situation in my Rails application where I need to include arbitrary modules depending on the current runtime state. The module provides custom application code that is only needed when certain conditions are true. Basically, I'm pulling the name of a company from the current context and using that as the filename for the module and its definition:
p = self.user.company.subdomain + ".rb"
if File.exists?(Rails.root + "lib/" + p)
include self.class.const_get(self.user.company.subdomain.capitalize.to_sym)
self.custom_add_url
end
My test module looks like this:
module Companyx
def custom_add_url
puts "Calling custom_add_url"
end
end
Now in the console, this actually works fine. I can pull a user and include the module like so:
[1] pry(main)> c = Card.find_by_personal_url("username")
[2] pry(main)> include c.class.const_get(c.user.company.subdomain.capitalize)=> Object
[3] pry(main)> c.custom_add_url
Calling custom_add_url
If I try to run the include line from my model, I get
NoMethodError: undefined method `include' for #<Card:0x007f91f9094fb0>
Can anyone suggest why the include statement would work on the console, but not in my model code?
I'm doing a similar thing. I found this answer useful:
How to convert a string to a constant in Ruby?
Turns out I was looking for the constantize method. This is the line I'm using in my code:
include "ModuleName::#{var.attr}".constantize
Edit:
So ack, I ran into various problems with actually using that line myself. Partially because I was trying to call it inside a method in a class. But since I'm only calling one method in the class (which calls/runs everything else) the final working version I have now is
"ModuleName::#{var.attr}".constantize.new.methodname
Obviously methodname is an instance method, so you could get rid of the new if yours is a class method.
Include is a method on a class.
If you want to call it inside a model, you need to execute the code in the context of its singleton class.
p = self.user.company.subdomain + ".rb"
if File.exists?(Rails.root + "lib/" + p)
myself = self
class_eval do
include self.const_get(myself.user.company.subdomain.capitalize.to_sym)
end
self.custom_add_url
EDIT:
class << self doesn't accept a block; class_eval does, hence it preserves the state of local variables. I've modified my solution to use it.

Alternative initialize for a Class to avoid processing already known information

I have a class, Autodrop, that contains several methods , a.o. 'metadata', that call an external API (dropbox). They are slow.
However, I already often have that metadata around when initializing the AutodropImage, so I should make the methods smarter.
What I have in mind is this:
class Autodrop
include Dropbox
attr_reader :path
def initialize(path)
#path = path
end
def self.from_entry(drop_entry)
#drop_entry = drop_entry
self.initialize(#drop_entry.path)
end
def metadata
if #drop_entry = nil
return heavy_lifting_and_network_traffic
else
return #drop_entry.metadata
end
end
#...
end
Now, I would expect to call
entry = BarEntry.new()
foo = Autodrop.from_entry(entry)
foo.metadata
In order to avoid that heavy lifting and network traffic call.
But this does not work. And somehow, in all my newbieness, I am sure I am goind at this all wrong.
Is there a term I should look for and read about first? How would you go for this?
Note, that the examples are simplified: in my code, I inherit AutodropImage < Autodrop for example, which is called from withing AutodropGallery < Autodrop. The latter already knows all metadata for the AutodropImage, so I mostly want to avoid AutodropImage going over the heavy lifting again.
You are creating an instance variable #drop_entry in your class method from_entry and obviously it wont be available to your object that you are creating in this method. One workaround is to pass it as a parameter when you are initializing the class. It should work if you do the following modifications:
In your from_entry class method change
self.initialize(#drop_entry)
to
new(#drop_entry)
Modify initialize method to:
def initialize(drop_entry)
#drop_entry = drop_entry
#path = #drop_entry.path
end
Or if your class is tied up to pass only the path parameter, ie. you dont want to change the other existing code then you can use an optional parameter drop entry like so
def initialize(path, drop_entry=nil)
You would need to cache the metadata in a class variable.
Edit: Or in a class level instance variable.
Maybe this read will help: http://railstips.org/blog/archives/2006/11/18/class-and-instance-variables-in-ruby/

ruby alternative to class << thing

I want to rewrite several methods of HighLine to customise my console and at the moment my code looks like this:
cmd = ask("#{#prompt_label} #{#prompt_separator} ", #tab_completion_candidates) do |q|
q.readline = true
# rewriting the Question class to make it do what we want
class << q
HERE I WRITE MY CODE
end
end
I would like to be able to separate my changes from my main console file, so let's say i have a class Console::Question that contains all the changes I want to do in HighLine::Console, I'd like to be able to do something like that:
Console::Question << q
end
But unfortunately that doesn't work :)
Any solution?
Thanks for your time.
If you put your changes in a module rather than a class then you can then do
q.extend(YourModule)
e.g. to override valid_answer?
module QuestionCustomisations
def valid_answer?
# your code here
end
end
q.extend(QuestionCustomisations)
This will apply your changes in just the object instance which is passed to the block.

Resources