Improving on hacky ruby 'method_alias' fix for conflicting redmine plugins? - ruby

Using redmine 3.x, I have a dependency conflict between two plugins - redmine subtask list accordion and Subtask list inherited fields. Installing both raises 500 errors when trying to view an issue.
ActionView::Template::Error (undefined method `sla_has_grandson_issues?' for #<#<Class:0x000056319e27d668>:0x00007f237ad02588>):
1: <% if sla_has_grandson_issues?(#issue) %>
2: <%= content_for :header_tags do
3: stylesheet_link_tag(sla_use_css, :plugin => "redmine_subtask_list_accordion") +
4: javascript_include_tag("subtask_list_accordion" + (subtask_tree_client_processing? ? "_client" : ""), :plugin => "redmine_subtask_list_accordion")
plugins/redmine_subtask_list_accordion/app/views/issues/_subtask_list_accordion_partial.html.erb:1:in `_292e8187f64bee60c61b7b15c99630ab'
After lots of trial and error we fixed the issue by adding the following to the original source code of the first plugin:
included do
alias_method :render_descendants_tree_original, :render_descendants_tree
alias_method :render_descendants_tree, :switch_render_descendants_tree
alias_method :sla_use_css, :sla_use_css
alias_method :switch_render_descendants_tree, :switch_render_descendants_tree
alias_method :render_descendants_tree_accordion, :render_descendants_tree_accordion
alias_method :expand_tree_at_first?, :expand_tree_at_first?
alias_method :sla_has_grandson_issues?, :sla_has_grandson_issues?
alias_method :subtask_tree_client_processing?, :subtask_tree_client_processing?
alias_method :subtask_list_accordion_tree_render_32?, :subtask_list_accordion_tree_render_32?
alias_method :subtask_list_accordion_tree_render_33?, :subtask_list_accordion_tree_render_33?
alias_method :subtask_list_accordion_tree_render_34?, :subtask_list_accordion_tree_render_34?
This is the original code:
https://github.com/GEROMAX/redmine_subtask_list_accordion/blob/master/lib/redmine_subtask_list_accordion/patches/issues_helper_patch.rb
Which has the first two alias_method calls from the above source code.
By making an alias method with the same name for each method in the original class, the code works fine. However this seems like a hacky fix, and I don't understand why it works. Can someone explain why the fix works and how to rewrite it properly?

alias_method creates a copy of the named method in the context where it is called.
Now, if the original method (second argument to alias_method) is defined somewhere up the inheritance chain and changed in any way later on, you still have that copy around that did not change. Which might explain the behavior you see.
As for the rewriting: As a rule of thumb, throw out all method aliasing (in both plugins) and use Module#prepend. This SO Answer is a nice overview of good and bad techniques for (monkey) patching Ruby code.
Patching Rails' view helpers as appears to be the case in your plugins can be tricky due to the way Rails handles them (and results may vary depending on code loading order). Wherever possible, avoid it or create new helper modules and add them to the relevant controllers using something like
module RedmineSubtaskListAccordion
module IssuesHelper
def render_descendants_tree(issue)
if sla_has_grandson_issues?(issue) && !subtask_tree_client_processing?
render_descendants_tree_accordion(issue)
else
super # this will call the stock Redmine IssuesHelper#render_descendants_tree method
end
end
end
end
# in init.rb:
IssuesController.class_eval do
helper RedmineSubtaskListAccordion::IssuesHelper
end
I happened to write a blog post about this exact thing a while ago. Hope it's OK to refer to that here.
Obviously there may still be conflicts if two plugins expect a certain Redmine method they are changing to behave in the way it does in stock Redmine, but removing method aliasing and not trying to monkey patch already existing helpers in my experience helps to avoid many problems.
Update: Most of the other methods in that particular module you linked to actually do not belong there, but could for example be mixed into the Issue model (like sla_has_grandson_issues? or declared as methods in the top level plugin namespace (all the settings and Redmine version related stuff - eg RedmineSubtaskListAccordion.tree_render_34?).

Related

ActiveRecord: override attribute writers by using a class method

I don't know how to correctly phrase the title, I think the best way to explain this issue is just with code samples.
My goal
I want to define a meta method like this (in Rails 5):
class Post < ApplicationRecord
override_this_attribute_writer :some_attribute
end
The override_this_attribute_writer follows a common pattern, it overrides the original writer by doing some filtering on top of it. I find this way of overriding very convenient and clear.
First approach
module MyCommonModule
extend ActiveSupport::Concern
module ClassMethods
def override_this_attribute_writer(attribute_name)
alias_method :"#{attribute_name}_old=", :"#{attribute_name}="
define_method :"#{attribute_name}=" do |a_value|
# Do my stuff
send(:"#{attribute_name}_old=", a_value)
end
end
end
When doing this, I was getting an exception at the call of alias_method, because, apparently, the method I was trying to copy didn't exist (yet).
Second approach
module MyCommonModule
extend ActiveSupport::Concern
module ClassMethods
def override_this_attribute_writer(attribute_name)
define_method :"#{attribute_name}=" do |a_value|
# Do my stuff
send(:write_attribute, attribute_name, a_value)
end
end
end
I was expecting this not to work: if, when running the meta method, ActiveRecord hasn't created the attribute writer yet, this means that it will do it later and override the method that I just defined.
But surprisingly it worked! So I put my hands inside ActiveRecord (5.1.5) to find out more.
Dig into ActiveRecord 5.1.5
I wanted to ensure that what I did was safe and it wasn't just working by accident: I looked into the definition of method writer, and put binding.pry around the method.
This is the result of the experiment:
For attributes that I did not override,
This line is called
Then the method is defined inside this module eval call
Finally, the newly created writer method is correctly called when performing object.attribute=
For attributes that I DID override,
My own method is defined before anything else (when the ActiveRecord writers aren't there yet
Then ActiveRecord calls the same line that handles writer creation, as in the previous example
The method gets (apparently) correctly created by ActiveRecord, since it passes again by this point
But now, surprisingly, when calling object.attribute= my own method is still called in place of the ActiveRecord one
So, this is what I don't understand: if ActiveRecord seems to be overriding my method but it doesn't, what prevents it from doing it?
My questions
What in the end I need to know is whether the fix I have done is actually a good practice (and robust) or it's at risk and it might break if in the future we do upgrades.
If you think that my fix is dangerous, would you be able to suggest a different way to achieve the same goal?
Calling super is even more idiomatic:
module MyCommonModule
extend ActiveSupport::Concern
module ClassMethods
def override_this_attribute_writer(attribute_name)
define_method :"#{attribute_name}=" do |value|
# do some stuff
super value
end
end
end
end

A better way to call methods on an instance

My question has a couple layers to it so please bear with me? I built a module that adds workflows from the Workflow gem to an instance, when you call a method on that instance. It has to be able to receive the description as a Hash or some basic data structure and then turn that into something that puts the described workflow onto the class, at run-time. So everything has to happen at run-time. It's a bit complex to explain what all the crazy requirements are for but it's still a good question, I hope. Anyways, The best I can do to be brief for a context, here, is this:
Build a class and include this module I built.
Create an instance of Your class.
Call the inject_workflow(some_workflow_description) method on the instance. It all must be dynamic.
The tricky part for me is that when I use public_send() or eval() or exec(), I still have to send some nested method calls and it seems like they use 2 different scopes, the class' and Workflow's (the gem). When someone uses the Workflow gem, they hand write these method calls in their class so it scopes everything correctly. The gem gets to have access to the class it creates methods on. The way I'm trying to do it, the user doesn't hand write the methods on the class, they get added to the class via the method shown here. So I wasn't able to get it to work using blocks because I have to do nested block calls e.g.
workflow() do # first method call
# first nested method call. can't access my scope from here
state(:state_name) do
# second nested method call. can't access my scope
event(:event_name, transitions_to: :transition_to_state)
end
end
One of the things I'm trying to do is call the Workflow#state() method n number of times, while nesting the Workflow#event(with, custom_params) 0..n times. The problem for me seems to be that I can't get the right scope when I nest the methods like that.
It works just like I'd like it to (I think...) but I'm not too sure I hit the best implementation. In fact, I think I'll probably get some strong words for what I've done. I tried using public_send() and every other thing I could find to avoid using class_eval() to no avail.
Whenever I attempted to use one of the "better" methods, I couldn't quite get the scope right and sometimes, I was invoking methods on the wrong object, altogether. So I think this is where I need the help, yeah?
This is what a few of the attempts were going for but this is more pseudo-code because I could never get this version or any like it to fly.
# Call this as soon as you can, after .new()
def inject_workflow(description)
public_send :workflow do
description[:workflow][:states].each do |state|
state.map do |name, event|
public_send name.to_sym do # nested call occurs in Workflow gem
# nested call occurs in Workflow gem
public_send :event, event[:name], transitions_to: event[:transitions_to]
end
end
end
end
end
From what I was trying, all these kinds of attempts ended up in the same result, which was my scope isn't what I need because I'm evaluating code in the Workflow gem, not in the module or user's class.
Anyways, here's my implementation. I would really appreciate it if someone could point me in the right direction!
module WorkflowFactory
# ...
def inject_workflow(description)
# Build up an array of strings that will be used to create exactly what
# you would hand-write in your class, if you wanted to use the gem.
description_string_builder = ['include Workflow', 'workflow do']
description[:workflow][:states].each do |state|
state.map do |name, state_description|
if state_description.nil? # if this is a final state...
description_string_builder << "state :#{name}"
else # because it is not a final state, add event information too.
description_string_builder.concat([
"state :#{name} do",
"event :#{state_description[:event]}, transitions_to: :#{state_description[:transitions_to]}",
"end"
])
end
end
end
description_string_builder << "end\n"
begin
# Use class_eval to run that workflow specification by
# passing it off to the workflow gem, just like you would when you use
# the gem normally. I'm pretty sure this is where everyone's head pops...
self.class.class_eval(description_string_builder.join("\n"))
define_singleton_method(:has_workflow?) { true }
rescue Exception => e
define_singleton_method(:has_workflow?) { !!(puts e.backtrace) }
end
end
end
end
# This is the class in question.
class Job
include WorkflowFactory
# ... some interesting code for your class goes here
def next!
current_state.events.#somehow choose the correct event
end
end
# and in some other place where you want your "job" to be able to use a workflow, you have something like this...
job = Job.new
job.done?
# => false
until job.done? do job.next! end
# progresses through the workflow and manages its own state awareness
I started this question off under 300000 lines of text, I swear. Thanks for hanging in there! Here's even more documentation, if you're not asleep yet.
module in my gem

Chef NoMethodError when using library module method with registry_key in recipe

I receive the error
NoMethodError
-------------
undefined method `registry_key' for HashOperations:Module
when converging a Chef cookbook.
This is the short version of the code from libraries/hash_operations.rb:
module HashOperations
# Tried: require 'chef/mixin/shell_out'
# Tried: include Chef::Mixin::ShellOut
def self.create_reg_keys(item_hash)
item_hash.each do |item_name, item_props|
# (...) construct the item_keys array
registry_key "HKEY_LOCAL_MACHINE\\SOFTWARE\\#{item_name}" do
recursive true
values item_keys
action :create
end
end
end
def self.generate_reg_keys_for_item_key(...)
# some hash operations here
create_reg_keys(item_hash)
end
end unless defined?(HashOperations)
class Chef::Resource
# Tried: Chef::Resource.send(:include, HashOperations)
# Tried: include HashOperations
extend HashOperations
end
and here is recipes/default.rb:
Chef::Resource.send(:include, HashOperations)
ruby_block "test" do
block do
# Tried: Chef::Recipe.send(:include, HashOperations)
items_keys.each do |item_key|
HashOperations.generate_reg_keys_for_item_key(..., item_key)
end
end
end
I guess the main problem comes from trying to use a Chef resource, registry_key, inside a method inside a module, which in turn is called from the recipe.
I have a working version if I'm not using a module, but I need a module if I want to test the code with ChefSpec, as several articles pointed (like this one: Stubbing library class methods in ChefSpec )
The link mentioned above is the reason for which I use end unless defined?(HashOperations) inside the module.
I've tried using include statements, it can be seen in the comments, or in the recipe's first line, as several StackOverflow posts suggested, with no luck. One post was discussing the usage of LWRP, but I really don't think it's the case here, as the code is strictly related to this recipe and wouldn't be used in some other cookbook.
As a note: I'm using self. in order for the defs to be visible to one another, otherwise I receive an error about generate_reg_keys_for_item_key not being available.
So, taking into account the fact that I've spent quite some hours searching for solutions on this, including the ones suggested by StackOverflow, the questions: what's the best approach to solve this error and have a simple solution that can be tested with ChefSpec (I'm not entirely excluding LWRPs though), and WHAT and HOW should I include for the registry_key to be visible on converge operation ?
You can't use the recipe DSL from helpers like this unless the helper is itself set up as a DSL extension. Check out https://coderanger.net/chef-tips/#3 to see how to do that.

Monkey patching built-in ruby classes in limited scopes

I'm working on an internal Ruby DSL and to make it look as pretty as possible I need to monkey patch the Symbol class and add some operators. I want to be responsible in how I do this and would like to limit the scope and lifetime of the patches to a specific block of code. Is there a standard pattern for doing this? Here's some pseudo-code to show what I'm thinking:
class SomeContext
def self.monkey_patch_region(&block)
context = SomeContext.new
context.monkey_patch_Symbol
context.instance_eval(&block)
context.unmonkey_patch_Symbol
end
# magical method
def monkey_patch_Symbol
#...
end
# another magical method
def unmonkey_patch_Symbol
#...
end
end
I believe, that you're looking for ruby refinements. The feature has landed in ruby trunk, but it might be reverted before 2.0
I've heard about mixology gem. It was designed to mixin and unmix modules. Maybe it can be useful to monkey and unmonkey patches.
UPDATE: mixology won't help you, as it (un)mixes modules to objects (as with extend), not to classes (as with include), and you want monkey/unmonkey core classes, not their objects individually. Anyway I intend to maintain this answer as possibly useful reference for someone else.

Is it possible to namespace methods added to existing classes in Ruby?

I'm writing some code in which I'd like to add some methods to a predefined class, like so:
class Model # this class already exists
def my_method
# code here
end
end
Is there any way to namespace this, using Modules or otherwise?
There will be a mechanism to do this in Ruby 2.0, although it is not exactly clear what exactly that mechanism is going to be. For the past almost 10 years, the frontrunner seemed to be Selector Namespaces, but recently Classboxes and even more recently, Refinements have taken the lead. In fact, if I am not mistaken, Refinements are actually currently implemented in the YARV trunk.
With all currently existing versions (including the soon to be released 1.9.3), however, there is no way to do this.
That's one of the reasons why monkey patching should generally be avoided.
Just write your methods inside a Module and include it in Model:
module SomeModule
def my_method
end
end
class Model
include SomeModule
end

Resources