Skip to content

lesiw/step

Repository files navigation

lesiw.io/step

Go Reference

Package 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) install(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

The returned function controls whether the sequence continues (non-nil) or stops (nil). The returned error is passed to handlers.

Do may return an *Error containing 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.

When a step returns an error but the sequence does not stop, Log renders it with ⊘:

✔ download
⊘ 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) {
    if i.Err != nil {
        io.Copy(os.Stderr, e)
    }
    e.Reset()
}

Testing

Use Equal and Name to test transitions:

func TestInstallLinux(t *testing.T) {
    d := &deploy{os: "linux"}
    got, err := d.install(t.Context())
    if err != nil {
        t.Fatalf("install 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.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages