branch.go (153 lines of code) (raw):

package flow import ( "context" ) // BranchCheckFunc checks the target and returns true if the branch should be selected. type BranchCheckFunc[T Steper] func(context.Context, T) (bool, error) // If adds a conditional branch to the workflow. // // If(someStep, func(ctx context.Context, someStep *SomeStep) (bool, error) { // // branch condition here, true -> Then, false -> Else. // // if error is returned, then fail the selected branch step. // }). // Then(thenStep). // Else(elseStep) func If[T Steper](target T, check BranchCheckFunc[T]) *IfBranch[T] { return &IfBranch[T]{Target: target, BranchCheck: BranchCheck[T]{Check: check}} } // IfBranch adds target step, then and else step to workflow, // and check the target step and determine which branch to go. type IfBranch[T Steper] struct { Target T // the target to check BranchCheck BranchCheck[T] ThenStep []Steper ElseStep []Steper Cond Condition // Cond is the When condition for both ThenStep and ElseStep, not target Step! } // Then adds steps to the Then branch. func (i *IfBranch[T]) Then(th ...Steper) *IfBranch[T] { i.ThenStep = append(i.ThenStep, th...) return i } // Else adds steps to the Else branch. func (i *IfBranch[T]) Else(el ...Steper) *IfBranch[T] { i.ElseStep = append(i.ElseStep, el...) return i } // When adds a condition to both Then and Else steps, not the Target! // Default to DefaultCondition. func (i *IfBranch[T]) When(cond Condition) *IfBranch[T] { i.Cond = cond return i } func (i *IfBranch[T]) isThen(isThen bool) Condition { return func(ctx context.Context, ups map[Steper]StepResult) StepStatus { if status := ConditionOrDefault(i.Cond)(ctx, ups); status != Running { return status } if i.BranchCheck.OK == isThen { return Running } return Skipped } } func (i *IfBranch[T]) AddToWorkflow() map[Steper]*StepConfig { return Steps().Merge( Steps(i.Target).AfterStep(func(ctx context.Context, s Steper, err error) error { i.BranchCheck.Do(ctx, i.Target) return err }), Steps(i.ThenStep...).When(i.isThen(true)), Steps(i.ElseStep...).When(i.isThen(false)), Steps(append(append([]Steper{}, i.ThenStep...), i.ElseStep..., )...). DependsOn(i.Target). BeforeStep(func(ctx context.Context, s Steper) (context.Context, error) { if i.BranchCheck.Error != nil { return ctx, i.BranchCheck.Error } return ctx, nil }), ).AddToWorkflow() } // Switch adds a switch branch to the workflow. // // Switch(someStep). // Case(case1, func(ctx context.Context, someStep *SomeStep) (bool, error) { // // branch condition here, true to select this branch // // error will fail the case // }). // Default(defaultStep), // the step to run if all case checks return false // ) func Switch[T Steper](target T) *SwitchBranch[T] { return &SwitchBranch[T]{Target: target, CasesToCheck: make(map[Steper]*BranchCheck[T])} } // SwitchBranch adds target step, cases and default step to workflow, // and check the target step and determine which branch to go. type SwitchBranch[T Steper] struct { Target T CasesToCheck map[Steper]*BranchCheck[T] DefaultStep []Steper Cond Condition } // BranchCheck represents a branch to be checked. type BranchCheck[T Steper] struct { Check BranchCheckFunc[T] OK bool Error error } func (bc *BranchCheck[T]) Do(ctx context.Context, target T) { bc.OK, bc.Error = bc.Check(ctx, target) } // Case adds a case to the switch branch. func (s *SwitchBranch[T]) Case(step Steper, check BranchCheckFunc[T]) *SwitchBranch[T] { return s.Cases([]Steper{step}, check) } // Cases adds multiple cases to the switch branch. // The check function will be executed for each case step. func (s *SwitchBranch[T]) Cases(steps []Steper, check BranchCheckFunc[T]) *SwitchBranch[T] { for _, step := range steps { s.CasesToCheck[step] = &BranchCheck[T]{Check: check} } return s } // Default adds default step(s) to the switch branch. func (s *SwitchBranch[T]) Default(step ...Steper) *SwitchBranch[T] { s.DefaultStep = append(s.DefaultStep, step...) return s } // When adds a condition to all case steps and default, not the Target! func (s *SwitchBranch[T]) When(cond Condition) *SwitchBranch[T] { s.Cond = cond return s } func (s *SwitchBranch[T]) isCase(c Steper) func(ctx context.Context, ups map[Steper]StepResult) StepStatus { return func(ctx context.Context, ups map[Steper]StepResult) StepStatus { if status := ConditionOrDefault(s.Cond)(ctx, ups); status != Running { return status } if check, ok := s.CasesToCheck[c]; ok { check.Do(ctx, s.Target) if check.OK { return Running } } return Skipped } } func (s *SwitchBranch[T]) isDefault(ctx context.Context, ups map[Steper]StepResult) StepStatus { for _, check := range s.CasesToCheck { if check.OK { return Skipped } } // default branch ignores the status from cases up := make(map[Steper]StepResult) for step, status := range ups { if _, isCase := s.CasesToCheck[step]; !isCase { up[step] = status } } if status := ConditionOrDefault(s.Cond)(ctx, up); status != Running { return status } return Running } func (s *SwitchBranch[T]) AddToWorkflow() map[Steper]*StepConfig { steps := Steps() cases := []Steper{} for step := range s.CasesToCheck { step := step cases = append(cases, step) steps.Merge( Steps(step). DependsOn(s.Target). When(s.isCase(step)). BeforeStep(func(ctx context.Context, step Steper) (context.Context, error) { for c, check := range s.CasesToCheck { if HasStep(step, c) && check.Error != nil { return ctx, check.Error } } return ctx, nil }), ) } if s.DefaultStep != nil { steps.Merge( Steps(s.DefaultStep...). DependsOn(s.Target). DependsOn(cases...). When(s.isDefault), ) } return steps.AddToWorkflow() }