I'm building a plugin that will allow a developer to add various features to a class with a simple declaration in the class definition (following the normal acts_as pattern).
For example, code consuming the plugin might look like
class YourClass
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
My question arises because I want to error check that the value provided for the :specific_method_to_use parameter exists as a method, but the way code is typically organized and loaded, the method doesn't exist yet.
The code in my plugin tentatively looks like this:
module MyPlugin
extend ActiveSupport::Concern
module ClassMethods
def consumes_my_plugin(options = {})
raise ArgumentError.new("#{options[:specific_method_to_use]} is not defined") if options[:specific_method_to_use].present? && !self.respond_to?(options[:specific_method_to_use])
end
end
end
This would work:
class YourClass
def your_method; true; end
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
But this is how most people write code, and it would not:
class YourClass
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
def your_method; true; end
end
How can I fail at YourClass load time? I want it to error then, not at run time with a NoMethodError. Can I defer execution of the line that raises the ArgumentError until the entire class is loaded, or do something else clever to achieve that?
Use TracePoint
to track when your class sends up an :end
event.
General solution
This module will let you create a self.finalize
callback in any class.
module Finalize
def self.extended(obj)
TracePoint.trace(:end) do |t|
if obj == t.self
obj.finalize
t.disable
end
end
end
end
Now you can extend your class and define self.finalize
, which will run as soon as the class definition ends:
class Foo
puts "Top of class"
extend Finalize
def self.finalize
puts "Finalizing #{self}"
end
puts "Bottom of class"
end
puts "Outside class"
# output:
# Top of class
# Bottom of class
# Finalizing Foo
# Outside class
Specific solution to OP's problem
Here's how you can fit TracePoint
directly into your pre-existing module.
require 'active_support/all'
module MyPlugin
extend ActiveSupport::Concern
module ClassMethods
def consumes_my_plugin(**options)
m = options[:specific_method_to_use]
TracePoint.trace(:end) do |t|
break unless self == t.self
raise ArgumentError.new("#{m} is not defined") unless instance_methods.include?(m)
t.disable
end
end
end
end
The examples below demonstrate that it works as specified:
# `def` before `consumes`: evaluates without errors
class MethodBeforePlugin
include MyPlugin
def your_method; end
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
# `consumes` before `def`: evaluates without errors
class PluginBeforeMethod
include MyPlugin
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
def your_method; end
end
# `consumes` with no `def`: throws ArgumentError at load time
class PluginWithoutMethod
include MyPlugin
consumes_my_plugin option1: :value1, specific_method_to_use: :your_method
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With