limited-effort/snfoil-context

View on GitHub
lib/snfoil/context.rb

Summary

Maintainability
A
1 hr
Test Coverage
# 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