step

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 24, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

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

func Continue(err error) error

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

func Do[T any](ctx context.Context, f Func[T], h ...Handler) (err error)

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.

func Equal

func Equal[T any](a, b Func[T]) bool

Equal reports whether two step functions refer to the same function. The type parameter ensures both functions belong to the same sequence. Equal compares fully qualified runtime names, so identically named functions in different packages are not considered equal.

func Name

func Name[T any](fn Func[T]) string

Name returns the short name of a step function.

Types

type Error

type Error struct {
	Info
	// contains filtered or unexported fields
}

Error is the error type returned by Do when a step fails.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type Func

type Func[T any] func(context.Context) (Func[T], error)

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

type Handler interface {
	Handle(Info, error)
}

Handler handles step completion events.

func Log

func Log(w io.Writer) Handler

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

type HandlerFunc func(Info, error)

HandlerFunc is an adapter to allow the use of ordinary functions as step handlers.

func (HandlerFunc) Handle

func (f HandlerFunc) Handle(i Info, err error)

Handle calls f(i, err).

type Info

type Info struct {
	// Name is the name of the step function.
	Name string
}

Info holds metadata about a step.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL