From dcbbab0869a85c818f2207ea4390b06581de59ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20B=C3=B6hme?= Date: Tue, 13 Jun 2023 12:02:26 +0200 Subject: [PATCH 1/2] Implement If() macro This commit adds an `If()` macro to conditionally execute steps in an operation. It's implementation is based on the implementation of the `Wrap()` macro to execute the nested sub-activity. Co-authored-by: Roland Schwarzer --- lib/trailblazer/macro.rb | 3 +- lib/trailblazer/macro/if.rb | 23 +++++ test/docs/if_test.rb | 180 ++++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 lib/trailblazer/macro/if.rb create mode 100644 test/docs/if_test.rb diff --git a/lib/trailblazer/macro.rb b/lib/trailblazer/macro.rb index 738c4e0..22c3cf1 100644 --- a/lib/trailblazer/macro.rb +++ b/lib/trailblazer/macro.rb @@ -11,6 +11,7 @@ require "trailblazer/macro/rescue" require "trailblazer/macro/wrap" require "trailblazer/macro/each" +require "trailblazer/macro/if" module Trailblazer module Macro @@ -83,6 +84,6 @@ module Activity::DSL::Linear::Helper # Extending the {Linear::Helper} namespace is the canonical way to import # macros into Railway, FastTrack, Operation, etc. extend Forwardable - def_delegators Trailblazer::Macro, :Model, :Nested, :Wrap, :Rescue, :Each + def_delegators Trailblazer::Macro, :Model, :Nested, :Wrap, :Rescue, :Each, :If end # Helper end diff --git a/lib/trailblazer/macro/if.rb b/lib/trailblazer/macro/if.rb new file mode 100644 index 0000000..e5e50e5 --- /dev/null +++ b/lib/trailblazer/macro/if.rb @@ -0,0 +1,23 @@ +module Trailblazer + module Macro + def self.If(condition, name: :default, id: Macro.id_for(condition, macro: :If, hint: condition), &block) + unless block_given? + raise ArgumentError, "If() requires a block" + end + + option = Trailblazer::Option(condition) + wrap = ->((ctx, flow_options), **circuit_args, &block) { + ctx[:"result.condition.#{name}"] = result = + option.call(ctx, keyword_arguments: ctx.to_hash, **circuit_args) + + if result + block.call + else + [Trailblazer::Activity::Right, [ctx, flow_options]] + end + } + + Wrap(wrap, id: id, &block) + end + end +end diff --git a/test/docs/if_test.rb b/test/docs/if_test.rb new file mode 100644 index 0000000..d76175e --- /dev/null +++ b/test/docs/if_test.rb @@ -0,0 +1,180 @@ +require "test_helper" + +class IfMacroTest < Minitest::Spec + class Song + module Activity + class Upload < Trailblazer::Activity::FastTrack + step :model + step If(:condition) { + step :update # this changes the database. + step :transfer # this might even break! + } + step :notify + fail :log_error + #~meths + include T.def_steps(:model, :condition, :update, :transfer, :notify, :log_error) + #~meths end + end + end + end + + it do + assert_invoke Song::Activity::Upload, condition: true, seq: "[:model, :condition, :update, :transfer, :notify]", expected_ctx_variables: { "result.condition.default": true } + assert_invoke Song::Activity::Upload, condition: false, seq: "[:model, :condition, :notify]", expected_ctx_variables: { "result.condition.default": false } + assert_invoke Song::Activity::Upload, condition: true, transfer: false, seq: "[:model, :condition, :update, :transfer, :log_error]", terminus: :failure, expected_ctx_variables: { "result.condition.default": true } + end +end + +class IfWithCustomNameTest < Minitest::Spec + class Song + module Activity + class Upload < Trailblazer::Activity::FastTrack + step If(:condition, name: :custom_name) { + step :update # this changes the database. + } + #~meths + include T.def_steps(:condition, :update) + #~meths end + end + end + end + + it do + assert_invoke Song::Activity::Upload, condition: true, seq: "[:condition, :update]", expected_ctx_variables: { "result.condition.custom_name": true } + assert_invoke Song::Activity::Upload, condition: false, seq: "[:condition]", expected_ctx_variables: { "result.condition.custom_name": false } + end +end + +class IfWithProcTest < Minitest::Spec + class Song + module Activity + class Upload < Trailblazer::Activity::FastTrack + step If(->(_ctx, condition:, **) { condition }) { + step :update # this changes the database. + } + #~meths + include T.def_steps(:update) + #~meths end + end + end + end + + it do + assert_invoke Song::Activity::Upload, condition: true, seq: "[:update]", expected_ctx_variables: { "result.condition.default": true } + assert_invoke Song::Activity::Upload, condition: false, seq: "[]", expected_ctx_variables: { "result.condition.default": false } + end +end + +class IfWithCallableTest < Minitest::Spec + class Song + module Activity + class Upload < Trailblazer::Activity::FastTrack + class Callable + def self.call(_ctx, condition:, **) + condition + end + end + + step If(Callable) { + step :update # this changes the database. + } + #~meths + include T.def_steps(:update) + #~meths end + end + end + end + + it do + assert_invoke Song::Activity::Upload, condition: true, seq: "[:update]", expected_ctx_variables: { "result.condition.default": true } + assert_invoke Song::Activity::Upload, condition: false, seq: "[]", expected_ctx_variables: { "result.condition.default": false } + end +end + +class IfWithNestedIfTest < Minitest::Spec + class Song + module Activity + class Upload < Trailblazer::Activity::FastTrack + step If(:condition) { + step :update # this changes the database. + step If(:nested_condition, name: :nested) { + step :notify + } + step :finalize + } + #~meths + include T.def_steps(:condition, :update, :nested_condition, :notify, :finalize) + #~meths end + end + end + end + + it do + assert_invoke Song::Activity::Upload, condition: true, nested_condition: true, seq: "[:condition, :update, :nested_condition, :notify, :finalize]", expected_ctx_variables: { "result.condition.default": true, "result.condition.nested": true } + assert_invoke Song::Activity::Upload, condition: false, seq: "[:condition]", expected_ctx_variables: { "result.condition.default": false } + assert_invoke Song::Activity::Upload, condition: true, nested_condition: false, seq: "[:condition, :update, :nested_condition, :finalize]", expected_ctx_variables: { "result.condition.default": true, "result.condition.nested": false } + end +end + +class IfTracingTest < Minitest::Spec + class Song + module Activity + class Upload < Trailblazer::Activity::FastTrack + class DecideWhatToDo + def self.call(*); end + end + + def self.my_condition_handler(*); end + + step If(:condition) {} + step If(DecideWhatToDo) {} + step If(method(:my_condition_handler)) {} + step If(->(*) {}, id: "proc.my_condition_handler") {} + + #~meths + include T.def_steps(:condition) + #~meths end + end + end + end + + it do + [ + "If/condition", + "If/IfTracingTest::Song::Activity::Upload::DecideWhatToDo", + "If/method(:my_condition_handler)", + "proc.my_condition_handler" + ].each do |id| + assert_equal Trailblazer::Developer::Introspect.find_path(Song::Activity::Upload, [id])[0].id, id + end + + assert_equal trace(Song::Activity::Upload, { seq: [], condition: false })[0], <<~SEQ.chomp + TOP + |-- Start.default + |-- If/condition + |-- If/IfTracingTest::Song::Activity::Upload::DecideWhatToDo + |-- If/method(:my_condition_handler) + |-- proc.my_condition_handler + `-- End.success + SEQ + end +end + +class IfWithoutBlockTest < Minitest::Spec + it do + exception = assert_raises ArgumentError do + class Song + module Activity + class Upload < Trailblazer::Activity::FastTrack + step If(:condition) + + #~meths + include T.def_steps(:condition) + #~meths end + end + end + end + end + assert_equal "If() requires a block", exception.message + end +end From 101456432a829a06800fbd32964864d79e385d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20B=C3=B6hme?= Date: Tue, 21 Nov 2023 15:15:18 +0100 Subject: [PATCH 2/2] Refactor If()-Macro to use Nested::Decider --- lib/trailblazer/macro/if.rb | 45 ++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/lib/trailblazer/macro/if.rb b/lib/trailblazer/macro/if.rb index e5e50e5..bcf71e2 100644 --- a/lib/trailblazer/macro/if.rb +++ b/lib/trailblazer/macro/if.rb @@ -5,19 +5,42 @@ def self.If(condition, name: :default, id: Macro.id_for(condition, macro: :If, h raise ArgumentError, "If() requires a block" end - option = Trailblazer::Option(condition) - wrap = ->((ctx, flow_options), **circuit_args, &block) { - ctx[:"result.condition.#{name}"] = result = - option.call(ctx, keyword_arguments: ctx.to_hash, **circuit_args) + block_activity, outputs = Macro.block_activity_for(nil, &block) + success_output = outputs.find { |output| output.semantic == :success } + state = Declarative::State( + block_activity: [block_activity, {copy: Trailblazer::Declarative::State.method(:subclass)}], + name: [name, {}], + success_signal: [success_output.signal, {}] + ) - if result - block.call - else - [Trailblazer::Activity::Right, [ctx, flow_options]] - end - } + task = Class.new(If) do + extend Macro::Strategy::State + initialize!(state) + end + + merge = [ + [Nested::Decider.new(condition), id: "If.compute_condition", prepend: "task_wrap.call_task"], + ] + + task_wrap_extension = Activity::TaskWrap::Extension::WrapStatic.new(extension: Activity::TaskWrap::Extension(*merge)) + + Activity::Railway.Subprocess(task).merge( + id: id, + extensions: [task_wrap_extension], + ) + end + end - Wrap(wrap, id: id, &block) + class If < Macro::Strategy + def self.call((ctx, flow_options), **circuit_options) + name = @state.get(:name) + ctx[:"result.condition.#{name}"] = flow_options[:decision] + + if flow_options[:decision] + Activity::Circuit::Runner.(block_activity, [ctx, flow_options], **circuit_options) + else + [@state.get(:success_signal), [ctx, flow_options]] + end end end end