2015-08-26 6 views
11

Ich baue ein Plugin, das es einem Entwickler ermöglicht, einer Klasse verschiedene Funktionen mit einer einfachen Deklaration in der Klassendefinition hinzuzufügen (nach dem normalen Muster acts_as).Wie kann ich einen Hook festlegen, um Code am Ende einer Ruby-Klassendefinition auszuführen?

Zum Beispiel Code, um das Plugin raubend könnte so aussehen

class YourClass 
    consumes_my_plugin option1: :value1, specific_method_to_use: :your_method 
end 

Meine Frage stellt sich, weil ich überprüfen, um Fehler möchten, dass der bereitgestellte Wert für die: specific_method_to_use Parameter als Methode existiert, aber die Art und Weise Code typischerweise organisiert und geladen, existiert die Methode noch nicht.

Der Code in meinem Plugin sieht versuchsweise wie folgt aus:

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 

Dies funktionieren würde:

class YourClass 
    def your_method; true; end 

    consumes_my_plugin option1: :value1, specific_method_to_use: :your_method 
end 

Aber das ist, wie die meisten Leute Code zu schreiben, und es wäre nicht:

class YourClass 
    consumes_my_plugin option1: :value1, specific_method_to_use: :your_method 

    def your_method; true; end 
end 

Wie kann ich bei YourClass Ladezeit fehlschlagen? Ich möchte es dann auf Fehler setzen, nicht zur Laufzeit mit einem NoMethodError. Kann ich die Ausführung der Zeile, die den ArgumentError auslöst, verschieben, bis die gesamte Klasse geladen ist, oder etwas anderes machen, um das zu erreichen?

+1

können Sie die Prüfung an der Stelle tun, wo Sie aufgerufen haben würde 'specific_method_to_use' anstatt die Prüfung in der Klassenladezeit zu machen? –

+0

: specific_method_to_use wird nur zur Laufzeit aufgerufen, also konnte ich zwar den NoMethodError abfangen und konstruktive Nachrichten an den Entwickler senden, aber der Bug wurde nur zur Laufzeit und nicht zur Ladezeit erkannt ... Letzteres ist meiner Meinung nach vorzuziehen. – LikeMaBell

+1

Die Überprüfung, die Sie mit 'self.respond_to?' Durchführen, wird nicht nach der Instanzmethode 'YourClass # your_method' suchen; Es wird nach der Singleton/Klassenmethode 'YourClass.your_method' gesucht. In meiner Antwort änderte ich es zu 'self.instance_methods.include?'. Fühlen Sie sich frei, es zurück zu 'reagieren_to?"Wenn ich deine Absicht falsch verstanden habe. –

Antwort

5

Verwenden Sie TracePoint, um zu verfolgen, wenn Ihre Klasse ein Ereignis :end sendet.


Spezifische Lösung für Ihr Problem

Hier ist, wie Sie TracePoint direkt in Ihr bereits bestehendes Modul passen.

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 

Die folgenden Beispiele zeigen, dass es wie angegeben funktioniert:

# `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 

Allgemeine Lösung

Dieses Modul lässt Sie in jeder Klasse einen self.finalize Rückruf erstellen.

module Finalize 
    def self.extended(obj) 
    TracePoint.trace(:end) do |t| 
     if obj == t.self 
     obj.finalize 
     t.disable 
     end 
    end 
    end 
end 

Jetzt können Sie Ihre Klasse erweitern und definieren self.finalize, die so schnell laufen wie die Klassendefinition endet:

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 
+0

Das sieht genau nach der Antwort aus, nach der ich gesucht habe. Vielen Dank. Ich entschuldige mich, dass es Ihnen nicht das volle Kopfgeld gegeben hat - ich war während der Bounty-Vergabe unterwegs und es scheint mir nicht möglich zu sein, den vollen Betrag zu vergeben. – LikeMaBell