# frozen_string_literal: true require 'steppy/version' require 'steppy/cache' require 'steppy/error' # The Steppy module you'll include in your classes to give them steps! module Steppy def self.included(base) base.extend ClassMethods base.include InstanceMethods end # :reek:TooManyStatements def self.parse_step(method:, args:, block: nil) args[:condition] = -> { steppy_run_condition(args[:if]) } if args.key?(:if) args[:condition] = -> { !steppy_run_condition(args[:unless]) } if args.key?(:unless) args[:prefix] = :step unless args.key?(:prefix) if method.is_a?(Proc) block = method method = nil end { method: method, args: args, block: block } end # Steppy class methods that will be added to your included classes. module ClassMethods def steppy(&block) steppy_cache[:block] = block end def step_set(*sets) steppy_cache[:sets] += sets end def step(method = nil, args = {}, &block) steps.push( Steppy.parse_step(method: method, args: args, block: block) ) self end alias step_return step def step_if(condition, &block) steps.push(condition: condition, block: block) self end def step_unless(condition, &block) steps.push(condition: -> { !steppy_run_condition(condition) }, block: block) self end def step_rescue(exceptions = nil, &block) steppy_cache[:rescues].push(exceptions: exceptions, block: block) self end def step_if_else(condition_block, step_steps, args = {}) if_step, else_step = step_steps steps.push Steppy.parse_step( method: if_step, args: { if: condition_block, }.merge(args) ) steps.push Steppy.parse_step( method: else_step, args: { unless: condition_block, }.merge(args) ) end def step_after(key = nil, &block) step_add_callback(:after, block, key) end def step_before(key = nil, &block) step_add_callback(:before, block, key) end def step_add_callback(type, block, key) callback_key = key ? key.to_sym : :global callbacks = step_callbacks[type][callback_key] ||= [] callbacks.push(block) end def steps steppy_cache[:steps] end def step_callbacks steppy_cache[:callbacks] end def steppy_cache @steppy_cache ||= SteppyCache.new( steps: [], sets: [], rescues: [], callbacks: { before: { global: [], }, after: { global: [], }, } ) end end # Steppy instance methods that will be added. module InstanceMethods attr_reader :steppy_cache def steppy(attributes = {}, cache = {}) steppy_initialize_cache({ attributes: attributes, prefix: :step }.merge(cache)) if steppy_cache.key?(:block) instance_exec(&steppy_cache[:block]) else steppy_run(steppy_cache) end rescue StandardError => exception steppy_rescue exception, steppy_cache[:rescues] end def step_set(*sets) steppy_sets(sets) end def step(method = nil, args = {}, &block) steppy_run_step Steppy.parse_step(method: method, args: args, block: block) end alias step_return step def step_if_else(condition_block, step_steps, args = {}) if_step, else_step = step_steps steppy_run_step Steppy.parse_step( method: if_step, args: { if: condition_block, }.merge(args) ) steppy_run_step Steppy.parse_step( method: else_step, args: { unless: condition_block, }.merge(args) ) end def step_if(condition, &block) steppy_run_condition_block condition, block end def step_unless(condition, &block) steppy_run_condition_block -> { !steppy_run_condition(condition) }, block end def step_rescue(*) raise '#step_rescue can not be used in a block, please just add rescue to the #steppy block.' end protected def steppy_run(steps:, sets:, **) steppy_sets(sets) steppy_steps(steps) end def steppy_sets(sets) sets.each { |key, value| steppy_set(key, value || steppy_attributes[key]) } end def steppy_set(key, value) key && instance_variable_set("@#{key}", value) end def steppy_steps(steps) steps.each do |step| condition = step[:condition] steppy_cache[:result] = if condition steppy_run_condition_block(condition, step[:block]) else steppy_run_step(step) end end steppy_result end def steppy_rescue(exception, rescues) exception_class = exception.class has_exception = exception_class == SteppyError || rescues.empty? raise exception if has_exception rescues.each do |exceptions:, block:| if !exceptions || (exceptions && !exceptions.include?(exception_class)) steppy_cache[:result] = instance_exec(steppy_attributes, &block) end end steppy_result end def steppy_run_condition_block(condition, block) steppy_run(steppy_cache_from_block(block)) if steppy_run_condition(condition) end def steppy_run_condition(condition) return true unless condition if condition.arity > 0 instance_exec(steppy_attributes, &condition) else instance_exec(&condition) end end def steppy_run_step(method:, args:, block:) if !steppy_run_condition(args[:condition]) || (steppy_cache[:prefix] != args[:prefix]) return steppy_result end step_callbacks(:before, method, args, args) result = if block instance_exec(steppy_attributes, &block) else steppy_run_method(method, steppy_attributes) end step_callbacks(:after, method, result, args) steppy_set(args[:set], result) result end def step_callbacks(type, method, result, args) callbacks = steppy_cache[:callbacks][type] method_callbacks = (callbacks[method] || []) callbacks[:global].concat(method_callbacks).each do |callback| instance_exec(result, args, &callback) end end def steppy_run_method(method_name, attributes) method = "#{steppy_cache[:prefix]}_#{method_name}" if method(method).arity > 0 public_send(method, attributes) else public_send(method) end end def steppy_cache_from_block(block) Class.new { include Steppy }.instance_exec(&block).steppy_cache end def steppy_initialize_cache(cache) @steppy_cache = SteppyCache.new( self.class.steppy_cache.to_h.merge(result: nil).merge(cache) ) end def steppy_attributes steppy_cache[:attributes] end def steppy_result steppy_cache[:result] end end end