wrap.go (100 lines of code) (raw):
package flow
import (
"fmt"
"log/slog"
"strings"
)
// # What is a Composite Step?
//
// Consider this case, Alice writes a Step implementation,
//
// type DoSomeThing struct{}
// func (d *DoSomeThing) Do(context.Context) error { /* do fancy things */ }
//
// After that, Bob finds the above implementation is useful, but still not enough.
// So Bob combines the above Steps into a new Step,
//
// type DoManyThings struct {
// DoSomeThing
// DoOtherThing
// }
// func (d *DoManyThings) Do(context.Context) error { /* do fancy things then other thing */ }
//
// Let's call the above DoManyThings a Composite Step, the below Decorator is another example.
//
// type Decorator struct { Steper }
// func (d *Decorator) Do(ctx context.Context) error {
// /* do something before */
// err := d.Steper.Do(ctx)
// /* do something after */
// return err
// }
//
// Since Workflow only requires a Step to satisfy the below interface:
//
// type Steper interface {
// Do(context.Context) error
// }
//
// It's easy, intuitive, flexible and yet powerful to use Composite Steps.
//
// Actually, Workflow itself also implements Steper interface,
// meaning you can use Workflow as a Step in another Workflow!
// # How to audit / retrieve / update all steps from the Workflow?
//
// workflow := func() *Workflow {
// ...
// workflow.Add(Step(doSomeThing))
// return workflow
// }
//
// from now on, we don't have reference to the internal steps in Workflow directly, like doSomeThing
// however, it's totally possible have necessary to update doSomeThing,
// like modify its input, configuration, or even its behavior (by decorator).
//
// # Introduce Unwrap()
//
// Kindly remind that, this nesting problem is not a new issue in Go.
// In Go, we have a very common error pattern:
//
// type MyError struct { Err error }
// func (e *MyError) Error() string { return fmt.Sprintf("MyError(%v)", e.Err) }
//
// The solution is using Unwrap() method:
//
// func (e *MyError) Unwrap() error { return e.Err }
//
// Then standard package errors provides Is() and As() functions to help us deal with warped errors.
// We also provides a similar Has() and As() functions for Steper.
//
// Users only need to implement the below methods for your Step implementations:
//
// type WrapStep struct { Steper }
// func (w *WrapStep) Unwrap() Steper { return w.Steper }
// // or
// type WrapSteps struct { Steps []Steper }
// func (w *WrapSteps) Unwrap() []Steper { return w.Steps }
//
// to expose your inner Steps.
type TraverseDecision int
const (
TraverseContinue = iota // TraverseContinue continue the traversal
TraverseStop // TraverseStop stop and exit the traversal immediately
TraverseEndBranch // TraverseEndBranch end the current branch, but continue sibling branches
)
// Traverse performs a pre-order traversal of the tree of step.
func Traverse(s Steper, f func(Steper, []Steper) TraverseDecision, walked ...Steper) TraverseDecision {
if f == nil {
return TraverseStop
}
for {
if s == nil {
return TraverseEndBranch
}
if dec := f(s, walked); dec != TraverseContinue {
return dec
}
walked = append(walked, s)
switch u := s.(type) {
case interface{ Unwrap() Steper }:
s = u.Unwrap()
case interface{ Unwrap() []Steper }:
for _, s := range u.Unwrap() {
if dec := Traverse(s, f, walked...); dec == TraverseStop {
return dec
}
}
return TraverseContinue
default:
return TraverseContinue
}
}
}
// Has reports whether there is any step inside matches target type.
func Has[T Steper](s Steper) bool {
find := false
Traverse(s, func(s Steper, walked []Steper) TraverseDecision {
if _, ok := s.(T); ok {
find = true
return TraverseStop
}
return TraverseContinue
})
return find
}
// As finds all steps in the tree of step that matches target type, and returns them.
// The sequence of the returned steps is pre-order traversal.
func As[T Steper](s Steper) []T {
var rv []T
Traverse(s, func(s Steper, walked []Steper) TraverseDecision {
if v, ok := s.(T); ok {
rv = append(rv, v)
}
return TraverseContinue
})
return rv
}
// HasStep reports whether there is any step matches target step.
func HasStep(step, target Steper) bool {
if target == nil {
return false
}
find := false
Traverse(step, func(s Steper, walked []Steper) TraverseDecision {
if s == target {
find = true
return TraverseStop
}
return TraverseContinue
})
return find
}
// String unwraps step and returns a proper string representation.
func String(step Steper) string {
if step == nil {
return "<nil>"
}
switch u := step.(type) {
case interface{ String() string }:
return u.String()
case interface{ Unwrap() Steper }:
return fmt.Sprintf("%T(%p) {\n\t%s\n}", u, u, indent(String(u.Unwrap())))
case interface{ Unwrap() []Steper }:
stepStrs := []string{}
for _, step := range u.Unwrap() {
stepStrs = append(stepStrs, String(step))
}
return fmt.Sprintf("%T(%p) {\n\t%s\n}", u, u, indent(strings.Join(stepStrs, "\n")))
default:
return fmt.Sprintf("%T(%p)", step, step)
}
}
// LogValue is used with log/slog, you can use it like:
//
// logger.With("step", LogValue(step))
//
// To prevent expensive String() calls,
//
// logger.With("step", String(step))
func LogValue(step Steper) logValue { return logValue{Steper: step} }
type logValue struct{ Steper }
func (lv logValue) String() string { return String(lv.Steper) }
func (lv logValue) LogValue() slog.Value { return slog.StringValue(String(lv.Steper)) }
func (lv logValue) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%q", String(lv.Steper))), nil
}