Skip to content
Snippets Groups Projects
Verified Commit 80c385bf authored by Rémy Coutable's avatar Rémy Coutable
Browse files

Add a Guard::API module that Guard plugins should include


This is a first step toward a sanitized plugin architecture:

Mixin instead of inheritance.

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent 02701711
No related branches found
No related tags found
No related merge requests found
require "guard"
require "guard/internals/groups"
module Guard
module API
TEMPLATE_FORMAT = "%s/lib/guard/%s/templates/Guardfile"
require "guard/ui"
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
attr_accessor :group, :watchers, :callbacks, :options
end
end
module ClassMethods
# Get all callbacks registered for all Guard plugins present in the
# Guardfile.
#
def callbacks
@callbacks ||= Hash.new { |hash, key| hash[key] = [] }
end
# Add a callback.
#
# @param [Block] listener the listener to notify
# @param [Guard::Plugin] guard_plugin the Guard plugin to add the callback
# @param [Array<Symbol>] events the events to register
#
def add_callback(listener, guard_plugin, events)
Array(events).each do |event|
callbacks[[guard_plugin, event]] << listener
end
end
# Notify a callback.
#
# @param [Guard::Plugin] guard_plugin the Guard plugin to add the callback
# @param [Symbol] event the event to trigger
# @param [Array] args the arguments for the listener
#
def notify(guard_plugin, event, *args)
callbacks[[guard_plugin, event]].each do |listener|
listener.call(guard_plugin, event, *args)
end
end
# Reset all callbacks.
#
# TODO: remove (not used anywhere)
def reset_callbacks!
@callbacks = nil
end
# Returns the non-namespaced class name of the plugin
#
#
# @example Non-namespaced class name for Guard::RSpec
# Guard::RSpec.non_namespaced_classname
# #=> "RSpec"
#
# @return [String]
#
def non_namespaced_classname
to_s.sub("Guard::", "")
end
# Returns the non-namespaced name of the plugin
#
#
# @example Non-namespaced name for Guard::RSpec
# Guard::RSpec.non_namespaced_name
# #=> "rspec"
#
# @return [String]
#
def non_namespaced_name
non_namespaced_classname.downcase
end
# Specify the source for the Guardfile template.
# Each Guard plugin can redefine this method to add its own logic.
#
# @param [String] plugin_location the plugin location
#
def template(plugin_location)
File.read(format(TEMPLATE_FORMAT, plugin_location, non_namespaced_name))
end
end
# When event is a Symbol, {#hook} will generate a hook name
# by concatenating the method name from where {#hook} is called
# with the given Symbol.
#
# @example Add a hook with a Symbol
#
# def run_all
# hook :foo
# end
#
# Here, when {Guard::Plugin#run_all} is called, {#hook} will notify
# callbacks registered for the "run_all_foo" event.
#
# When event is a String, {#hook} will directly turn the String
# into a Symbol.
#
# @example Add a hook with a String
#
# def run_all
# hook "foo_bar"
# end
#
# When {Guard::Plugin::run_all} is called, {#hook} will notify
# callbacks registered for the "foo_bar" event.
#
# @param [Symbol, String] event the name of the Guard event
# @param [Array] args the parameters are passed as is to the callbacks
# registered for the given event.
#
def hook(event, *args)
hook_name = if event.is_a? Symbol
calling_method = caller[0][/`([^']*)'/, 1]
"#{ calling_method }_#{ event }"
else
event
end
UI.debug "Hook :#{ hook_name } executed for #{ self.class }"
self.class.notify(self, hook_name.to_sym, *args)
end
# Called once when Guard starts. Please override initialize method to
# init stuff.
#
# @raise [:task_has_failed] when start has failed
# @return [Object] the task result
#
def start
end
# Called when `stop|quit|exit|s|q|e + enter` is pressed (when Guard
# quits).
#
# @raise [:task_has_failed] when stop has failed
# @return [Object] the task result
#
def stop
end
# Called when `reload|r|z + enter` is pressed.
# This method should be mainly used for "reload" (really!) actions like
# reloading passenger/spork/bundler/...
#
# @raise [:task_has_failed] when reload has failed
# @return [Object] the task result
#
def reload
end
# Called when just `enter` is pressed
# This method should be principally used for long action like running all
# specs/tests/...
#
# @raise [:task_has_failed] when run_all has failed
# @return [Object] the task result
#
def run_all
end
# Default behaviour on file(s) changes that the Guard plugin watches.
#
# @param [Array<String>] paths the changes files or paths
# @raise [:task_has_failed] when run_on_changes has failed
# @return [Object] the task result
#
def run_on_changes(paths)
end
# Called on file(s) additions that the Guard plugin watches.
#
# @param [Array<String>] paths the changes files or paths
# @raise [:task_has_failed] when run_on_additions has failed
# @return [Object] the task result
#
def run_on_additions(paths)
end
# Called on file(s) modifications that the Guard plugin watches.
#
# @param [Array<String>] paths the changes files or paths
# @raise [:task_has_failed] when run_on_modifications has failed
# @return [Object] the task result
#
def run_on_modifications(paths)
end
# Called on file(s) removals that the Guard plugin watches.
#
# @param [Array<String>] paths the changes files or paths
# @raise [:task_has_failed] when run_on_removals has failed
# @return [Object] the task result
#
def run_on_removals(paths)
end
# Returns the plugin's name (without "guard-").
#
# @example Name for Guard::RSpec
# Guard::RSpec.new.name
# #=> "rspec"
#
# @return [String]
#
def name
@name ||= self.class.non_namespaced_name
end
# Returns the plugin's class name without the Guard:: namespace.
#
# @example Title for Guard::RSpec
# Guard::RSpec.new.title
# #=> "RSpec"
#
# @return [String]
#
def title
@title ||= self.class.non_namespaced_classname
end
# String representation of the plugin.
#
# @example String representation of an instance of the Guard::RSpec plugin
#
# Guard::RSpec.new.title
# #=> "#<Guard::RSpec @name=rspec @group=#<Guard::Group @name=default
# @options={}> @watchers=[] @callbacks=[] @options={all_after_pass:
# true}>"
#
# @return [String] the string representation
#
def to_s
"#<#{self.class} @name=#{name} @group=#{group} @watchers=#{watchers}"\
" @callbacks=#{callbacks} @options=#{options}>"
end
private
# Initializes a Guard plugin.
# Don't do any work here, especially as Guard plugins get initialized even
# if they are not in an active group!
#
# @param [Hash] options the Guard plugin options
# @option options [Array<Guard::Watcher>] watchers the Guard plugin file
# watchers
# @option options [Symbol] group the group this Guard plugin belongs to
# @option options [Boolean] any_return allow any object to be returned from
# a watcher
#
def initialize(opts = {})
group_name = opts.delete(:group) { :default }
@group = Guard.state.session.groups.add(group_name)
@watchers = opts.delete(:watchers) { [] }
@callbacks = opts.delete(:callbacks) { [] }
@options = opts
_register_callbacks
end
# Add all the Guard::Plugin's callbacks to the global @callbacks array
# that's used by Guard to know which callbacks to notify.
#
def _register_callbacks
callbacks.each do |callback|
self.class.add_callback(callback[:listener], self, callback[:events])
end
end
end
end
require "guard/plugin_util"
require "guard/group"
require "guard/api"
require "guard/plugin"
 
module Guard
Loading
Loading
require "guard"
require "guard/internals/groups"
require "guard/api"
 
module Guard
# Base class from which every Guard plugin implementation must inherit.
#
# Guard will trigger the {#start}, {#stop}, {#reload}, {#run_all} and
# {#run_on_changes} ({#run_on_additions}, {#run_on_modifications} and
# {#run_on_removals}) task methods depending on user interaction and file
# modification.
#
# {#run_on_changes} could be implemented to handle all the changes task case
# (additions, modifications, removals) in once, or each task can be
# implemented separately with a specific behavior.
#
# In each of these Guard task methods you have to implement some work when
# you want to support this kind of task. The return value of each Guard task
# method is not evaluated by Guard, but it'll be passed to the "_end" hook
# for further evaluation. You can throw `:task_has_failed` to indicate that
# your Guard plugin method was not successful, and successive Guard plugin
# tasks will be aborted when the group has set the `:halt_on_fail` option.
#
# @see Guard::Group
#
# @example Throw :task_has_failed
#
# def run_all
# if !runner.run(['all'])
# throw :task_has_failed
# end
# end
#
# Each Guard plugin should provide a template Guardfile located within the Gem
# at `lib/guard/guard-name/templates/Guardfile`.
#
# Watchers for a Guard plugin should return a file path or an array of files
# paths to Guard, but if your Guard plugin wants to allow any return value
# from a watcher, you can set the `any_return` option to true.
#
# If one of those methods raises an exception other than `:task_has_failed`,
# the `Guard::GuardName` instance will be removed from the active Guard
# plugins.
#
class Plugin
TEMPLATE_FORMAT = "%s/lib/guard/%s/templates/Guardfile"
include Guard::API
 
require "guard/ui"
def initialize(*)
super
 
# Get all callbacks registered for all Guard plugins present in the
# Guardfile.
#
def self.callbacks
@callbacks ||= Hash.new { |hash, key| hash[key] = [] }
end
# Add a callback.
#
# @param [Block] listener the listener to notify
# @param [Guard::Plugin] guard_plugin the Guard plugin to add the callback
# @param [Array<Symbol>] events the events to register
#
def self.add_callback(listener, guard_plugin, events)
Array(events).each do |event|
callbacks[[guard_plugin, event]] << listener
end
end
# Notify a callback.
#
# @param [Guard::Plugin] guard_plugin the Guard plugin to add the callback
# @param [Symbol] event the event to trigger
# @param [Array] args the arguments for the listener
#
def self.notify(guard_plugin, event, *args)
callbacks[[guard_plugin, event]].each do |listener|
listener.call(guard_plugin, event, *args)
end
end
# Reset all callbacks.
#
# TODO: remove (not used anywhere)
def self.reset_callbacks!
@callbacks = nil
end
# When event is a Symbol, {#hook} will generate a hook name
# by concatenating the method name from where {#hook} is called
# with the given Symbol.
#
# @example Add a hook with a Symbol
#
# def run_all
# hook :foo
# end
#
# Here, when {Guard::Plugin#run_all} is called, {#hook} will notify
# callbacks registered for the "run_all_foo" event.
#
# When event is a String, {#hook} will directly turn the String
# into a Symbol.
#
# @example Add a hook with a String
#
# def run_all
# hook "foo_bar"
# end
#
# When {Guard::Plugin::run_all} is called, {#hook} will notify
# callbacks registered for the "foo_bar" event.
#
# @param [Symbol, String] event the name of the Guard event
# @param [Array] args the parameters are passed as is to the callbacks
# registered for the given event.
#
def hook(event, *args)
hook_name = if event.is_a? Symbol
calling_method = caller[0][/`([^']*)'/, 1]
"#{ calling_method }_#{ event }"
else
event
end
UI.debug "Hook :#{ hook_name } executed for #{ self.class }"
plugin_util = PluginUtil.new(name)
plugin_file = "#{plugin_util.plugin_location}/lib/guard/#{name}.rb"
msg = <<-MSG
Inherithing from Guard::Plugin is now deprecated.
 
self.class.notify(self, hook_name.to_sym, *args)
end
attr_accessor :group, :watchers, :callbacks, :options
# Returns the non-namespaced class name of the plugin
#
#
# @example Non-namespaced class name for Guard::RSpec
# Guard::RSpec.non_namespaced_classname
# #=> "RSpec"
#
# @return [String]
#
def self.non_namespaced_classname
to_s.sub("Guard::", "")
end
# Returns the non-namespaced name of the plugin
#
#
# @example Non-namespaced name for Guard::RSpec
# Guard::RSpec.non_namespaced_name
# #=> "rspec"
#
# @return [String]
#
def self.non_namespaced_name
non_namespaced_classname.downcase
end
# Specify the source for the Guardfile template.
# Each Guard plugin can redefine this method to add its own logic.
#
# @param [String] plugin_location the plugin location
#
def self.template(plugin_location)
File.read(format(TEMPLATE_FORMAT, plugin_location, non_namespaced_name))
end
Guard has choosen to sanitize its architecture, you can conform to it with
the following steps:
 
# Called once when Guard starts. Please override initialize method to
# init stuff.
#
# @raise [:task_has_failed] when start has failed
# @return [Object] the task result
#
# @!method start
# Called when `stop|quit|exit|s|q|e + enter` is pressed (when Guard
# quits).
#
# @raise [:task_has_failed] when stop has failed
# @return [Object] the task result
#
# @!method stop
# Called when `reload|r|z + enter` is pressed.
# This method should be mainly used for "reload" (really!) actions like
# reloading passenger/spork/bundler/...
#
# @raise [:task_has_failed] when reload has failed
# @return [Object] the task result
#
# @!method reload
# Called when just `enter` is pressed
# This method should be principally used for long action like running all
# specs/tests/...
#
# @raise [:task_has_failed] when run_all has failed
# @return [Object] the task result
#
# @!method run_all
# Default behaviour on file(s) changes that the Guard plugin watches.
#
# @param [Array<String>] paths the changes files or paths
# @raise [:task_has_failed] when run_on_changes has failed
# @return [Object] the task result
#
# @!method run_on_changes(paths)
# Called on file(s) additions that the Guard plugin watches.
#
# @param [Array<String>] paths the changes files or paths
# @raise [:task_has_failed] when run_on_additions has failed
# @return [Object] the task result
#
# @!method run_on_additions(paths)
# Called on file(s) modifications that the Guard plugin watches.
#
# @param [Array<String>] paths the changes files or paths
# @raise [:task_has_failed] when run_on_modifications has failed
# @return [Object] the task result
#
# @!method run_on_modifications(paths)
# Called on file(s) removals that the Guard plugin watches.
#
# @param [Array<String>] paths the changes files or paths
# @raise [:task_has_failed] when run_on_removals has failed
# @return [Object] the task result
#
# @!method run_on_removals(paths)
# Returns the plugin's name (without "guard-").
#
# @example Name for Guard::RSpec
# Guard::RSpec.new.name
# #=> "rspec"
#
# @return [String]
#
def name
@name ||= self.class.non_namespaced_name
end
# Returns the plugin's class name without the Guard:: namespace.
#
# @example Title for Guard::RSpec
# Guard::RSpec.new.title
# #=> "RSpec"
#
# @return [String]
#
def title
@title ||= self.class.non_namespaced_classname
end
# String representation of the plugin.
#
# @example String representation of an instance of the Guard::RSpec plugin
#
# Guard::RSpec.new.title
# #=> "#<Guard::RSpec @name=rspec @group=#<Guard::Group @name=default
# @options={}> @watchers=[] @callbacks=[] @options={all_after_pass:
# true}>"
#
# @return [String] the string representation
#
def to_s
"#<#{self.class} @name=#{name} @group=#{group} @watchers=#{watchers}"\
" @callbacks=#{callbacks} @options=#{options}>"
end
private
# Initializes a Guard plugin.
# Don't do any work here, especially as Guard plugins get initialized even
# if they are not in an active group!
#
# @param [Hash] options the Guard plugin options
# @option options [Array<Guard::Watcher>] watchers the Guard plugin file
# watchers
# @option options [Symbol] group the group this Guard plugin belongs to
# @option options [Boolean] any_return allow any object to be returned from
# a watcher
#
def initialize(options = {})
group_name = options.delete(:group) { :default }
@group = Guard.state.session.groups.add(group_name)
@watchers = options.delete(:watchers) { [] }
@callbacks = options.delete(:callbacks) { [] }
@options = options
_register_callbacks
end
- Make your gem dependent on `s.add_dependency "guard-compat", "~> 1.1"`in
your gemspec
- Make Guard::#{title} a module
- Add `require 'guard/compat/plugin'` at the beginning of #{plugin_file}
- Create a new Guard::#{title}::Plugin class that `include Compat::API`
 
# Add all the Guard::Plugin's callbacks to the global @callbacks array
# that's used by Guard to know which callbacks to notify.
#
def _register_callbacks
callbacks.each do |callback|
self.class.add_callback(callback[:listener], self, callback[:events])
end
For testing your plugin, you should follow the guard-compat guidelines at
https://github.com/guard/guard-compat#important.
MSG
UI.deprecation(msg)
end
end
end
Loading
Loading
@@ -54,6 +54,8 @@ module Guard
#
def initialize_plugin(options)
klass = plugin_class
klass = klass.const_get("Plugin") if klass.const_defined?("Plugin")
fail "Could not load class: #{_constant_name.inspect}" unless klass
if klass.superclass.to_s == "Guard::Guard"
klass.new(options.delete(:watchers), options)
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment