Documentation
¶
Overview ¶
Package lesiw.io/step runs sequences of step functions.
A step is a function that receives a context and returns the next step to run. Step functions are typically defined as methods on a state type, then passed to Do as method values:
type etl struct {
raw []byte
parsed []string
}
func (e *etl) extract(context.Context) (step.Func[etl], error) {
e.raw = []byte("a,b,c")
return e.transform, nil
}
func (e *etl) transform(context.Context) (step.Func[etl], error) {
e.parsed = strings.Split(string(e.raw), ",")
return e.load, nil
}
func (e *etl) load(context.Context) (step.Func[etl], error) {
fmt.Println(e.parsed)
return nil, nil
}
Run the sequence by passing a context and the first step:
var e etl
if err := step.Do(ctx, e.extract); err != nil {
log.Fatal(err)
}
Branching ¶
Steps can branch by returning different functions:
func (d *deploy) detectOS(context.Context) (step.Func[deploy], error) {
switch d.os {
case "linux":
return d.installLinux, nil
case "darwin":
return d.installDarwin, nil
}
return nil, fmt.Errorf("unsupported OS: %s", d.os)
}
Error Handling ¶
When a step returns a non-nil error, Do wraps it in *Error with the step name:
err := step.Do(ctx, e.extract)
if stepErr, ok := errors.AsType[*step.Error](err); ok {
fmt.Println("failed at:", stepErr.Name)
}
Do also checks for context cancellation before each step.
To signal a non-fatal condition, wrap the error with Continue:
func (d *deploy) install(context.Context) (step.Func[deploy], error) {
if !d.needsInstall {
return d.configure, step.Continue(fmt.Errorf("skip"))
}
// ... do the install ...
}
Do passes Continue errors to handlers but does not stop the sequence. Handlers can inspect the underlying error to decide how to render it. Log prints continued steps with ⊘:
✔ detectOS ⊘ install: skip ✔ configure
Handlers ¶
A Handler receives step completion events. Log provides a default handler that prints check marks and X marks:
step.Do(ctx, e.extract, step.Log(os.Stderr)) ✔ extract ✔ transform ✘ load: something went wrong
Multiple handlers run in sequence:
step.Do(ctx, e.extract, step.Log(os.Stderr), step.HandlerFunc(e.handle))
Since the handler is called after each step, the handler itself can be a method on the state type. This is useful for buffered logging, where step output is captured and only shown on failure:
type etl struct {
bytes.Buffer
raw []byte
parsed []string
}
func (e *etl) handle(i step.Info, err error) {
if err != nil {
io.Copy(os.Stderr, e)
}
e.Reset()
}
Testing ¶
Use Equal and Name to test transitions:
func TestDetectLinux(t *testing.T) {
d := &deploy{os: "linux"}
got, err := d.detectOS(t.Context())
if err != nil {
t.Fatalf("detectOS err: %v", err)
}
if want := d.installLinux; !step.Equal(got, want) {
t.Errorf("got %s, want %s", step.Name(got), step.Name(want))
}
}
Equal and Name compare and identify functions by name using the runtime, making step transitions testable without comparing function values directly.
Example ¶
package main
import (
"context"
"os"
"strings"
"lesiw.io/step"
)
type etl struct {
raw []byte
parsed []string
}
func main() {
var e etl
err := step.Do(context.Background(), e.extract, step.Log(os.Stdout))
if err != nil {
os.Exit(1)
}
}
func (e *etl) extract(context.Context) (step.Func[etl], error) {
e.raw = []byte("a,b,c")
return e.transform, nil
}
func (e *etl) transform(context.Context) (step.Func[etl], error) {
e.parsed = strings.Split(string(e.raw), ",")
return e.load, nil
}
func (e *etl) load(context.Context) (step.Func[etl], error) {
return nil, nil
}
Output: ✔ extract ✔ transform ✔ load
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Continue ¶ added in v0.3.0
Continue wraps an error to indicate that the sequence should not stop. When a step returns an error wrapped with Continue, Do passes it to handlers but continues to the next step instead of returning. This allows custom non-fatal signals like skip or warn:
var Skip = step.Continue(fmt.Errorf("skip"))
Handlers can inspect the underlying error with errors.Is or errors.As:
func handle(i step.Info, err error) {
if errors.Is(err, Skip) {
fmt.Printf("⊘ %s\n", i.Name)
}
}
func Do ¶
Do executes a sequence starting from fn. It checks for context cancellation before each step and stops on the first non-nil error. If the error wraps Continue, handlers are called but the sequence continues to the next step. Handlers are called in order after each step.
Types ¶
type Error ¶
type Error struct {
Info
// contains filtered or unexported fields
}
Error is the error type returned by Do when a step fails.
type Func ¶
Func is a step in a sequence. Each step receives a context and returns the next step to run or nil to stop. Step functions are typically methods on a state type, bound as method values:
step.Do(ctx, e.extract)
Example (Plain) ¶
package main
import (
"context"
"log"
"os"
"lesiw.io/step"
)
func main() {
err := step.Do(context.Background(), fetch, step.Log(os.Stdout))
if err != nil {
log.Fatal(err)
}
}
func fetch(context.Context) (step.Func[any], error) { return process, nil }
func process(context.Context) (step.Func[any], error) { return store, nil }
func store(context.Context) (step.Func[any], error) { return nil, nil }
Output: ✔ fetch ✔ process ✔ store
type Handler ¶
Handler handles step completion events.
func Log ¶
Log returns a Handler that writes step results to w.
✔ passed step ✘ failed step: error message ⊘ continued step: error message
Example ¶
package main
import (
"context"
"fmt"
"os"
"lesiw.io/step"
)
func main() {
var p pipeline
err := step.Do(context.Background(), p.step1, step.Log(os.Stdout))
if err != nil {
fmt.Fprintln(os.Stderr, "pipeline failed")
}
}
type pipeline struct{}
func (p pipeline) step1(context.Context) (step.Func[pipeline], error) {
return p.step2, nil
}
func (p pipeline) step2(context.Context) (step.Func[pipeline], error) {
return p.step3, nil
}
func (p pipeline) step3(context.Context) (step.Func[pipeline], error) {
return nil, fmt.Errorf("something went wrong")
}
Output: ✔ step1 ✔ step2 ✘ step3: something went wrong
type HandlerFunc ¶
HandlerFunc is an adapter to allow the use of ordinary functions as step handlers.