Skip to content

Commit

Permalink
State::Resolver executes state guards and via state_table finds the
Browse files Browse the repository at this point in the history
correct lanes configuration.
  • Loading branch information
apotonick committed Mar 29, 2024
1 parent f2e0692 commit c1c83e6
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 58 deletions.
1 change: 1 addition & 0 deletions lib/trailblazer/workflow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module Workflow
require "trailblazer/workflow/collaboration"
require "trailblazer/workflow/collaboration/lane"
require "trailblazer/workflow/collaboration/messages"
require "trailblazer/workflow/collaboration/state"
require "trailblazer/workflow/collaboration/state_guards" # DISCUSS: it's an Advance concept, not necessary for normal operations.
require "trailblazer/workflow/event"

Expand Down
43 changes: 38 additions & 5 deletions lib/trailblazer/workflow/advance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,59 @@ module Workflow
# TODO:
# * wire the event_valid? step to {invalid_state} terminus and handle that differently in the endpoint.
class Advance < Trailblazer::Activity::Railway
step task: :compute_catch_event_tuple
step task: :find_position_options
step task: :event_valid?, Output(:failure) => End(:not_authorized)
step task: :advance

def find_position_options((ctx, flow_options), **)
def compute_catch_event_tuple((ctx, flow_options), **)
iteration_set, event_label = flow_options[:iteration_set], flow_options[:event_label]

# DISCUSS: maybe it's not a future-compatible idea to use the {iteration_set} for the state label lookup?
# it's probably cleaner to have a dedicated state_table.
# it should compute activity/start_task (catch event ID) from the event label.
planned_iteration = iteration_set.to_a.find { |iteration| iteration.event_label == event_label }

# TODO: those positions could also be passed in manually, without using an Iteration::Set.
flow_options[:position_options] = position_options_from_iteration(planned_iteration) # :start_task_position and :start_positions
position_options_FIXME = position_options_from_iteration(planned_iteration) # :start_task_position and :start_positions
catch_event_tuple = Introspect::Iteration::Set::Serialize.id_tuple_for(*position_options_FIXME[:start_task_position].to_a, lanes_cfg: flow_options[:lanes]) # TODO: this should be done via state_table.

flow_options[:catch_event_tuple] = catch_event_tuple

return Activity::Right, [ctx, flow_options]
end

# TODO: Position object with "tuple", resolved activity/task, comment, lane label, etc. Instead of recomputing it continuously.


# TODO: this should be done in the StateGuard realm.
def find_position_options((ctx, flow_options), **)
state_resolver = flow_options[:state_guards]

_, state_options = state_resolver.(*flow_options[:catch_event_tuple], [ctx], **ctx.to_hash)


lanes_cfg = flow_options[:lanes]
fixme_tuples = state_options[:suspend_tuples].collect { |tuple| {"tuple" => tuple} }
fixme_label_2_activity = lanes_cfg.to_h.values.collect { |cfg| [cfg[:label], cfg[:activity]] }.to_h

flow_options[:position_options] = {
start_task_position: Introspect::Iteration::Set::Deserialize.position_from_tuple(*flow_options[:catch_event_tuple].to_a, label_2_activity: fixme_label_2_activity), # FIXME: we're doing the literal opposite one step before this.
lane_positions: Introspect::Iteration::Set::Deserialize.positions_from(fixme_tuples, label_2_activity: fixme_label_2_activity)
}

# raise flow_options[:position_options].inspect

return Activity::Right, [ctx, flow_options]
end

def event_valid?((ctx, flow_options), **)
position_options = flow_options[:position_options]
state_guards = flow_options[:state_guards]
# position_options = flow_options[:position_options]
# state_guards = flow_options[:state_guards]

# result = state_guards.(ctx, start_task_position: position_options[:start_task_position])

result = state_guards.(ctx, start_task_position: position_options[:start_task_position])
result = flow_options[:position_options][:lane_positions].is_a? Trailblazer::Workflow::Collaboration::Positions # FIXME

return result ? Activity::Right : Activity::Left, [ctx, flow_options]
end
Expand Down
29 changes: 29 additions & 0 deletions lib/trailblazer/workflow/collaboration/state.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module Trailblazer
module Workflow
module State
# Finding a configuration for a specific event:
# 1. From the event label, figure out lane and event ID.
# 2. a StateTable instance knows which event ID maps to one or many configurations.
# 3. Resolver finds the matching configuration by running the respective state guards.
class Resolver
def initialize(guards, table)
@guards = guards
@table = table
end

def call(lane_label, catch_id, args, **kws)
# First, find all state guards that "point to" this catch event.
possible_states = @table.find_all { |state_name, cfg| cfg[:catch_tuples].include?([lane_label, catch_id]) }


# Execute those, the first returning true indicates the configuration.
target_state = possible_states.find { |state_name, cfg| @guards.(state_name, args, **kws) }

raise "No state configuration found for #{state_name.inspect}" if target_state.nil?

target_state
end
end
end
end
end
26 changes: 15 additions & 11 deletions lib/trailblazer/workflow/collaboration/state_guards.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,27 @@ class StateGuards
# Note: this is called at every boot-up and needs to be reasonably fast, as we're mapping
# catch events to state guards.
def self.from_user_hash(state_guards_cfg, state_table:)
catch_2_guard = state_guards_cfg.flat_map do |state_label, user_cfg|
catch_event_ids = state_table.fetch(state_label)[:id] # raise if we don't know this state label.
# state_2_guard = state_guards_cfg.flat_map do |state_label, user_cfg|
# raise state_label.inspect
# catch_event_ids = state_table.fetch(state_label)[:id] # raise if we don't know this state label.

catch_event_ids.collect { |catch_id| [catch_id, user_cfg[:guard]] }
end
# catch_event_ids.collect { |catch_id| [catch_id, user_cfg[:guard]] }
# end

StateGuards.new(catch_2_guard.to_h)
end
# TODO: optimize this for runtime.

guards = StateGuards.new(state_guards_cfg)

def initialize(catch_2_guard)
@catch_2_guard = catch_2_guard
# FIXME: rename this very method!
State::Resolver.new(guards, state_table)
end

def call(ctx, start_task_position:)
catch_id = Trailblazer::Activity::Introspect.Nodes(start_task_position.activity, task: start_task_position.task).id # TODO: do that at compile-time.
def initialize(state_2_guard)
@state_2_guard = state_2_guard
end

@catch_2_guard.fetch(catch_id).(ctx, **ctx.to_hash) # TODO: discuss exec interface
def call(state_name, args, **kws) # DISCUSS: do we really need that abstraction here?
@state_2_guard.fetch(state_name)[:guard].(*args, **kws) # TODO: discuss exec interface
end
end
end # Collaboration
Expand Down
2 changes: 1 addition & 1 deletion lib/trailblazer/workflow/generate/state_guards.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def render(ctx, rows:, namespace:, **options)
state_guard_rows = rows.collect do |row|
# id_snippet = %(, id: #{row[:key].inspect}) # TODO: move me to serializer code.

%( #{row["state name"].inspect.ljust(max_length)} => {guard: ->(ctx, process_model:, **) { raise "implement me!" }},)
%( #{row["state name"].inspect.ljust(max_length)} => {guard: ->(ctx, model:, **) { model.state == #{row["state name"].inspect} }},)
end.join("\n")

snippet = %(module #{namespace}
Expand Down
2 changes: 1 addition & 1 deletion lib/trailblazer/workflow/generate/state_table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def render(ctx, rows:, namespace:, **)
state_guard_rows = available_states.collect do |row|
id_snippet = %(suspend_tuples: #{row[:suspend_tuples].inspect}) # TODO: move me to serializer code.

%( #{row[:suggested_state_name].inspect.ljust(max_length)} => {#{id_snippet}, catch_tuples: #{row[:catch_events]}})
%( #{row[:suggested_state_name].inspect.ljust(max_length)} => {#{id_snippet}, catch_tuples: #{row[:catch_events]}},)
end.join("\n")

snippet = %(# This file is generated by trailblazer-workflow.
Expand Down
40 changes: 22 additions & 18 deletions test/advance_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,32 @@ class AdvanceTest < Minitest::Spec
iteration_set = Trailblazer::Workflow::Introspect::Iteration::Set::Deserialize.(JSON.parse(serialized_iteration_set), lanes_cfg: lanes_cfg)

state_guards_from_user = {state_guards: {
"⏸︎ Create form " => {guard: ->(ctx, model: nil, **) { model.nil? }, id: ["catch-before-Activity_0wc2mcq"]},
"⏸︎ Create " => {guard: ->(ctx, model: nil, **) { model.nil? }, id: ["catch-before-Activity_1psp91r"]},
"⏸︎ Update form♦Notify approver" => {guard: ->(ctx, model:, **) { model }, id: ["catch-before-Activity_1165bw9", "catch-before-Activity_1dt5di5"]},
"⏸︎ Update " => {guard: ->(ctx, model:, **) { model }, id: ["catch-before-Activity_0j78uzd"]},
"⏸︎ Delete? form♦Publish " => {guard: ->(ctx, process_model:, **) { raise "implement me!" }, id: ["catch-before-Activity_0bsjggk", "catch-before-Activity_0ha7224"]},
"⏸︎ Revise form " => {guard: ->(ctx, process_model:, **) { raise "implement me!" }, id: ["catch-before-Activity_0zsock2"]},
"⏸︎ Delete♦Cancel " => {guard: ->(ctx, process_model:, **) { raise "implement me!" }, id: ["catch-before-Activity_15nnysv", "catch-before-Activity_1uhozy1"]},
"⏸︎ Archive " => {guard: ->(ctx, process_model:, **) { raise "implement me!" }, id: ["catch-before-Activity_0fy41qq"]},
"⏸︎ Revise " => {guard: ->(ctx, process_model:, **) { raise "implement me!" }, id: ["catch-before-Activity_1wiumzv"]},
"⏸︎ Archive [10u]" => {guard: ->(ctx, model:, **) { model.state == "⏸︎ Archive [10u]" }},
"⏸︎ Create [01u]" => {guard: ->(ctx, model: nil, **) { model.nil? }},
"⏸︎ Create form [00u]" => {guard: ->(ctx, model:, **) { model.state == "⏸︎ Create form [00u]" }},
"⏸︎ Delete♦Cancel [11u]" => {guard: ->(ctx, model:, **) { model.state == "⏸︎ Delete♦Cancel [11u]" }},
"⏸︎ Revise [01u]" => {guard: ->(ctx, model:, **) { model.state == "⏸︎ Revise [01u]" }},
"⏸︎ Revise form [00u]" => {guard: ->(ctx, model:, **) { model.state == "⏸︎ Revise form [00u]" }},
"⏸︎ Revise form♦Notify approver [10u]" => {guard: ->(ctx, model:, **) { model.state == "⏸︎ Revise form♦Notify approver [10u]" }},
"⏸︎ Update [00u]" => {guard: ->(ctx, model:, **) { model.id == 1 }},
"⏸︎ Update form♦Delete? form♦Publish [11u]" => {guard: ->(ctx, model:, **) { model.state == "⏸︎ Update form♦Delete? form♦Publish [11u]" }},
"⏸︎ Update form♦Notify approver [00u]" => {guard: ->(ctx, model:, **) { model.state == "⏸︎ Update form♦Notify approver [00u]" }},
"⏸︎ Update form♦Notify approver [11u]" => {guard: ->(ctx, model:, **) { model.state == "⏸︎ Update form♦Notify approver [11u]" }},
}}[:state_guards]

# auto-generated. this structure could also hold alternative state names, etc.
state_table = {
"⏸︎ Create form " => {id: ["catch-before-Activity_0wc2mcq"]},
"⏸︎ Create " => {id: ["catch-before-Activity_1psp91r"]},
"⏸︎ Update form♦Notify approver" => {id: ["catch-before-Activity_1165bw9", "catch-before-Activity_1dt5di5"]},
"⏸︎ Update " => {id: ["catch-before-Activity_0j78uzd"]},
"⏸︎ Delete? form♦Publish " => {id: ["catch-before-Activity_0bsjggk", "catch-before-Activity_0ha7224"]},
"⏸︎ Revise form " => {id: ["catch-before-Activity_0zsock2"]},
"⏸︎ Delete♦Cancel " => {id: ["catch-before-Activity_15nnysv", "catch-before-Activity_1uhozy1"]},
"⏸︎ Archive " => {id: ["catch-before-Activity_0fy41qq"]},
"⏸︎ Revise " => {id: ["catch-before-Activity_1wiumzv"]},
"⏸︎ Archive [10u]" => {suspend_tuples: [["lifecycle", "suspend-gw-to-catch-before-Activity_1hgscu3"], ["UI", "suspend-gw-to-catch-before-Activity_0fy41qq"], ["approver", "~suspend~"]], catch_tuples: [["UI", "catch-before-Activity_0fy41qq"]]},
"⏸︎ Create [01u]" => {suspend_tuples: [["lifecycle", "suspend-gw-to-catch-before-Activity_0wwfenp"], ["UI", "suspend-Gateway_14h0q7a"], ["approver", "~suspend~"]], catch_tuples: [["UI", "catch-before-Activity_1psp91r"]]},
"⏸︎ Create form [00u]" => {suspend_tuples: [["lifecycle", "suspend-gw-to-catch-before-Activity_0wwfenp"], ["UI", "suspend-gw-to-catch-before-Activity_0wc2mcq"], ["approver", "~suspend~"]], catch_tuples: [["UI", "catch-before-Activity_0wc2mcq"]]},
"⏸︎ Delete♦Cancel [11u]" => {suspend_tuples: [["lifecycle", "suspend-Gateway_1hp2ssj"], ["UI", "suspend-Gateway_100g9dn"], ["approver", "~suspend~"]], catch_tuples: [["UI", "catch-before-Activity_15nnysv"], ["UI", "catch-before-Activity_1uhozy1"]]},
"⏸︎ Revise [01u]" => {suspend_tuples: [["lifecycle", "suspend-Gateway_01p7uj7"], ["UI", "suspend-Gateway_1xs96ik"], ["approver", "~suspend~"]], catch_tuples: [["UI", "catch-before-Activity_1wiumzv"]]},
"⏸︎ Revise form [00u]" => {suspend_tuples: [["lifecycle", "suspend-Gateway_01p7uj7"], ["UI", "suspend-gw-to-catch-before-Activity_0zsock2"], ["approver", "~suspend~"]], catch_tuples: [["UI", "catch-before-Activity_0zsock2"]]},
"⏸︎ Revise form♦Notify approver [10u]" => {suspend_tuples: [["lifecycle", "suspend-Gateway_1kl7pnm"], ["UI", "suspend-Gateway_00n4dsm"], ["approver", "~suspend~"]], catch_tuples: [["UI", "catch-before-Activity_0zsock2"], ["UI", "catch-before-Activity_1dt5di5"]]},
"⏸︎ Update [00u]" => {suspend_tuples: [["lifecycle", "suspend-Gateway_0fnbg3r"], ["UI", "suspend-Gateway_0nxerxv"], ["approver", "~suspend~"]], catch_tuples: [["UI", "catch-before-Activity_0j78uzd"]]},
"⏸︎ Update form♦Delete? form♦Publish [11u]" => {suspend_tuples: [["lifecycle", "suspend-Gateway_1hp2ssj"], ["UI", "suspend-Gateway_1sq41iq"], ["approver", "~suspend~"]], catch_tuples: [["UI", "catch-before-Activity_1165bw9"], ["UI", "catch-before-Activity_0ha7224"], ["UI", "catch-before-Activity_0bsjggk"]]},
"⏸︎ Update form♦Notify approver [00u]" => {suspend_tuples: [["lifecycle", "suspend-Gateway_0fnbg3r"], ["UI", "suspend-Gateway_0kknfje"], ["approver", "~suspend~"]], catch_tuples: [["UI", "catch-before-Activity_1165bw9"], ["UI", "catch-before-Activity_1dt5di5"]]},
"⏸︎ Update form♦Notify approver [11u]" => {suspend_tuples: [["lifecycle", "suspend-Gateway_1wzosup"], ["UI", "suspend-Gateway_1g3fhu2"], ["approver", "~suspend~"]], catch_tuples: [["UI", "catch-before-Activity_1165bw9"], ["UI", "catch-before-Activity_1dt5di5"]]},
}

state_guards = Trailblazer::Workflow::Collaboration::StateGuards.from_user_hash( # TODO: unify naming, DSL.state_guards_from_user or something like that.
Expand Down
Loading

0 comments on commit c1c83e6

Please sign in to comment.