diff --git a/lib/crack_pipe/action/exec.rb b/lib/crack_pipe/action/exec.rb index 1f5356e..a582abb 100644 --- a/lib/crack_pipe/action/exec.rb +++ b/lib/crack_pipe/action/exec.rb @@ -13,6 +13,7 @@ def call(action, context, track = :default) def action(action, context, track = :default) action.class.steps.each_with_object([]) do |s, results| next unless track == s.track + results!(results, action, s, context).last.tap do |r| action.after_flow_control(r) context = r[:context] @@ -53,12 +54,10 @@ def halt(output, success = nil) def step(action, step, context) kwargs = kwargs_with_context(action, context) + return :skipped unless should_exec?(step.exec_if, action, context, kwargs) + output = catch(:signal) do - if (e = step.exec).is_a?(Symbol) - action.public_send(e, context, **kwargs) - else - e.call(context, **kwargs) - end + exec_with_args(step.exec, action, context, kwargs) end action.after_step(output) @@ -70,14 +69,34 @@ def success_with_step?(action, step, output) private + def callable?(e) + e.is_a?(Symbol) || e.respond_to?(:call) + end + + def should_exec?(exec_if, action, context, kwargs) + return exec_with_args(exec_if, action, context.dup, kwargs) if callable?(exec_if) + + exec_if + end + + def exec_with_args(e, action, context, kwargs) + if e.is_a?(Symbol) + action.public_send(e, context, **kwargs) + else + e.call(context, **kwargs) + end + end + def kwargs_with_context(action, context) return context if action.kwargs_overrides.empty? + context.merge(action.kwargs_overrides) end def results!(results, action, step, context) o = step(action, step, context) return results.concat(o.history) if o.is_a?(Result) + results << flow_control_hash(action, step, context, o) end end diff --git a/lib/crack_pipe/action/step.rb b/lib/crack_pipe/action/step.rb index 8fb7105..7b7d95f 100644 --- a/lib/crack_pipe/action/step.rb +++ b/lib/crack_pipe/action/step.rb @@ -3,17 +3,19 @@ module CrackPipe class Action class Step - attr_reader :exec, :track + attr_reader :exec, :exec_if, :track - def initialize(exec = nil, always_pass: false, track: :default, **, &blk) + def initialize(exec = nil, always_pass: false, track: :default, **opts, &blk) if block_given? raise ArgumentError, '`exec` must be `nil` with a block' unless exec.nil? + exec = blk end @always_pass = always_pass @exec = instantiate_action(exec) + @exec_if = opts.key?(:if) ? opts[:if] : true @track = track end @@ -28,6 +30,7 @@ def always_pass? # `step SomeAction.new` when nesting actions. def instantiate_action(obj) return obj.new if obj.is_a?(Class) && obj < Action + obj end end diff --git a/lib/crack_pipe/version.rb b/lib/crack_pipe/version.rb index 63f5de6..289ae9c 100644 --- a/lib/crack_pipe/version.rb +++ b/lib/crack_pipe/version.rb @@ -2,8 +2,8 @@ module CrackPipe MAJOR = 0 - MINOR = 2 - TINY = 4 + MINOR = 3 + TINY = 0 VERSION = [MAJOR, MINOR, TINY].join('.').freeze def self.version diff --git a/spec/action_spec.rb b/spec/action_spec.rb index 7f638aa..577f79b 100644 --- a/spec/action_spec.rb +++ b/spec/action_spec.rb @@ -50,14 +50,85 @@ def after(ctx, value:, **) value.to_s.upcase end - def after_fail(ctx, **) + def after_fail(_ctx, **) :custom_error_code_02 end end end + let(:action_with_skips) do + klass = skipped_action + + Class.new(Action) do + step :before + step klass, if: ->(ctx, **) { ctx[:run_all_steps] } + step :always_run, if: true + step :never_run, if: false + step :conditional_method, if: :run_all_steps? + + def before(ctx, **) + ctx[:before] = true + end + + def conditional_method(ctx, **) + ctx[:conditional_method] = "exec'd" + end + + def always_run(ctx, **) + ctx[:always_run] = true + end + + def never_run(ctx, **) + ctx[:never_run] = true + end + + def run_all_steps?(_, run_all_steps:, **) + run_all_steps + end + end + end + + let(:skipped_action) do + Class.new(Action) do + step :maybe_hit_1 + step :maybe_hit_2 + + def maybe_hit_1(ctx, **) + ctx[:maybe_hit_1] = true + end + + def maybe_hit_2(ctx, **) + ctx[:maybe_hit_2] = true + end + end + end + + it 'conditionally skips steps' do + r = action_with_skips.call(run_all_steps: false) + r.history.size.must_equal(5) + assert r.success? + assert r[:before] + assert r[:always_run] + refute r.context.key?(:never_run) + refute r.context.key?(:maybe_hit_1) + refute r.context.key?(:maybe_hit_2) + refute r.context.key?(:conditional_method) + r.output.must_equal(:skipped) + r.history.select { |h| h[:output] == :skipped }.size.must_equal(3) + + r = action_with_skips.call(run_all_steps: true) + r.history.size.must_equal(6) + assert r.success? + assert r[:always_run] + assert r[:maybe_hit_1] + assert r[:maybe_hit_2] + refute r.context.key?(:never_run) + r[:conditional_method].must_equal("exec'd") + r.output.must_equal("exec'd") + end + it 'results in a success with a truthy value' do - r = action.(value: 'x') + r = action.call(value: 'x') r.history.size.must_equal(3) r.history.select { |h| h[:next] == :default }.size.must_equal(3) @@ -69,7 +140,7 @@ def after_fail(ctx, **) end it 'results in a failure and uses the fail track with a falsy value' do - r = action.(value: false) + r = action.call(value: false) r.history.size.must_equal(4) r.history.select { |h| h[:next] == :fail }.size.must_equal(3) @@ -80,7 +151,7 @@ def after_fail(ctx, **) end it 'short circuits execution with `pass!`' do - r = action.new.(value: :short_circuit) + r = action.new.call(value: :short_circuit) r.history.size.must_equal(2) assert r.success? @@ -88,13 +159,13 @@ def after_fail(ctx, **) end it 'short circuits execution with `fail!`' do - r = action.(value: :short_circuit!) + r = action.call(value: :short_circuit!) assert r.failure? r.output.must_equal(:short_circuit!) end it 'nests one action in another' do - r = nesting_action.(value: 'x') + r = nesting_action.call(value: 'x') r.history.size.must_equal(5) r.output.must_equal('X') @@ -102,13 +173,13 @@ def after_fail(ctx, **) r[:before].must_equal(true) r[:value_class].must_equal('String') - r = nesting_action.(value: false) + r = nesting_action.call(value: false) r.history.size.must_equal(6) r.output.must_equal(:custom_error_code_02) r[:before].must_equal(true) - r = nesting_action.(value: :short_circuit!) + r = nesting_action.call(value: :short_circuit!) assert r.failure? r.output.must_equal(:short_circuit!) end @@ -145,7 +216,7 @@ def after_step(output) end end - r = a.({}) + r = a.call({}) r.history[0][:output].must_equal(1) r.history[1][:output].must_equal('two') end @@ -170,7 +241,7 @@ def after_flow_control(flow_control_hash) end end - r = a.({}) + r = a.call({}) r.history[0][:context][:one].must_equal('one') r.output.must_equal('one!') end