问题
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?
回答1:
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
来源:https://stackoverflow.com/questions/32233860/how-can-i-set-a-hook-to-run-code-at-the-end-of-a-ruby-class-definition