lib/snfoil/context.rb
# frozen_string_literal: true
# Copyright 2021 Matthew Howes, Cliff Campbell
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require_relative './context/error'
require_relative './context/argument_error'
require_relative './context/structure'
require 'active_support/concern'
module SnFoil
# ActiveSupport::Concern for adding SnFoil Context functionality
#
# @author Matthew Howes
#
# @since 0.1.0
module Context
extend ActiveSupport::Concern
included do
include SnFoil::Context::Structure
end
class_methods do
def action(name, with: nil, &block)
raise SnFoil::Context::Error, "action #{name} already defined for #{self.name}" if (@snfoil_actions ||= []).include?(name)
@snfoil_actions << name
define_workflow(name)
define_action_primary(name, with, block)
end
def interval(name)
define_singleton_methods(name)
define_instance_methods(name)
end
def intervals(*names)
names.each { |name| interval(name) }
end
end
def run_interval(interval, **options)
hooks = self.class.instance_variable_get("@snfoil_#{interval}_hooks") || []
options = hooks.reduce(options) { |opts, hook| run_hook(hook, **opts) }
send(interval, **options)
end
private
# rubocop:disable reason: These are builder/mapping methods that are just too complex to simplify without
# making them more complex. If anyone has a better way please let me know
class_methods do # rubocop:disable Metrics/BlockLength
def define_workflow(name)
interval "setup_#{name}"
interval "before_#{name}"
interval "after_#{name}_success"
interval "after_#{name}_failure"
interval "after_#{name}"
end
def define_action_primary(name, method, block) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
define_method(name) do |*_args, **options| # rubocop:disable Metrics/MethodLength
options[:action] ||= name.to_sym
options = run_interval(format('setup_%s', name), **options)
authorize(name, **options) if respond_to?(:authorize) && !options[:skip_first_authorize]
options = run_interval(format('before_%s', name), **options)
authorize(name, **options) if respond_to?(:authorize) && !options[:skip_last_authorize]
options = if run_action_primary(method, block, **options)
run_interval(format('after_%s_success', name), **options)
else
run_interval(format('after_%s_failure', name), **options)
end
run_interval(format('after_%s', name), **options)
end
end
def define_singleton_methods(method_name)
singleton_var = "snfoil_#{method_name}_hooks"
instance_variable_set("@#{singleton_var}", [])
define_singleton_method(singleton_var) { instance_variable_get("@#{singleton_var}") }
define_singleton_method(method_name) do |with: nil, **options, &block|
raise SnFoil::Context::ArgumentError, "\##{method_name} requires either a method name or a block" if with.nil? && block.nil?
instance_variable_get("@#{singleton_var}") << { method: with,
block: block,
if: options[:if],
unless: options[:unless] }
end
end
def define_instance_methods(method_name)
return if method_defined? method_name
define_method(method_name) do |**options|
options
end
end
end
def run_action_primary(method, block, **options)
return send(method, **options) if method
instance_exec(**options, &block)
end
end
end