-
Notifications
You must be signed in to change notification settings - Fork 5
/
wrap.go
198 lines (185 loc) · 5.43 KB
/
wrap.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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 String(u.Unwrap())
case interface{ Unwrap() []Steper }:
stepStrs := []string{}
for _, step := range u.Unwrap() {
stepStrs = append(stepStrs, String(step))
}
return fmt.Sprintf("[%s]", strings.Join(stepStrs, ", "))
default:
return fmt.Sprintf("%T(%v)", 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
}