Skip to content

Commit d1e2796

Browse files
committed
Support context.Context in op method signatures
Thread a cancelable context through the op handler so that methods with a func(context.Context) error signature receive a live context. The context is canceled after afters run, making context.AfterFunc a natural way to register cleanup.
1 parent 61c849c commit d1e2796

File tree

3 files changed

+98
-15
lines changed

3 files changed

+98
-15
lines changed

README.md

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,21 @@ Add a file to `.ops/main.go`, for example:
99
```go
1010
package main
1111

12-
import "lesiw.io/ops"
12+
import (
13+
"context"
14+
"fmt"
15+
16+
"lesiw.io/ops"
17+
)
1318

1419
type Ops struct{}
1520

16-
func main() { ops.Handle(Ops{}) }
17-
func (Ops) Hello() { println("Hello world!") }
21+
func main() { ops.Handle(Ops{}) }
22+
23+
func (Ops) Hello(ctx context.Context) error {
24+
fmt.Println("Hello world!")
25+
return nil
26+
}
1827
```
1928

2029
Then use `op` to run it.
@@ -27,11 +36,32 @@ op hello # => Hello world!
2736

2837
You can also play with a basic example on the [Go playground][play].
2938

30-
## Error handling
39+
The context provided by the framework is canceled after the op completes,
40+
making `context.AfterFunc` a natural way to register cleanup:
41+
42+
```go
43+
func (o Ops) Deploy(ctx context.Context) error {
44+
env, err := createEnvironment()
45+
if err != nil {
46+
return err
47+
}
48+
context.AfterFunc(ctx, func() {
49+
env.Cleanup()
50+
})
51+
return runTests(env)
52+
}
53+
```
54+
55+
Ops can call each other directly:
3156

32-
Op functions can be of type `func()` or `func() error`. If a `func()` op panics,
33-
that panic will be printed as if it were an error. If a `func() error` op
34-
panics, it is treated as a true panic and will print a stacktrace.
57+
```go
58+
func (o Ops) All(ctx context.Context) error {
59+
if err := o.Build(ctx); err != nil {
60+
return err
61+
}
62+
return o.Test(ctx)
63+
}
64+
```
3565

3666
## Post handler functions
3767

@@ -49,5 +79,5 @@ multiple methods with the same name at the same depth will be run sequentially
4979
in the same order as their embedded types.
5080

5181
[go]: https://go.dev/doc/install
52-
[play]: https://go.dev/play/p/YcUCt5RLoPR
82+
[play]: https://go.dev/play/p/Ff4ZL0rh3Nc
5383
[selectors]: https://go.dev/ref/spec#Selectors

handler.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package ops
55

66
import (
7+
"context"
78
"errors"
89
"fmt"
910
"io"
@@ -16,6 +17,8 @@ import (
1617
"lesiw.io/flag"
1718
)
1819

20+
var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
21+
1922
var (
2023
exit = defers.Exit
2124
flags = flag.NewSet(stderr, "op [-l] OPERATION")
@@ -65,10 +68,12 @@ func opHandler(a any, args ...string) (err error) {
6568
return err
6669
}
6770
}
71+
ctx, cancel := context.WithCancel(context.Background())
72+
defer cancel()
6873
val := reflect.ValueOf(a)
6974
var ran bool
7075
for _, method := range methodsByName(t, args[0]) {
71-
if err := exec(val, method); err != nil {
76+
if err := exec(ctx, val, method); err != nil {
7277
return err
7378
}
7479
ran = true
@@ -93,7 +98,7 @@ func validate(fn reflect.Method) error {
9398
return nil
9499
}
95100

96-
func exec(val reflect.Value, fn reflect.Method) (err error) {
101+
func exec(ctx context.Context, val reflect.Value, fn reflect.Method) (err error) {
97102
var catch bool
98103
defer func() {
99104
if !catch {
@@ -120,16 +125,16 @@ func exec(val reflect.Value, fn reflect.Method) (err error) {
120125
var ret []reflect.Value
121126
if typ.In(0).Kind() == reflect.Ptr {
122127
if val.Kind() == reflect.Ptr {
123-
ret = call(val, fn)
128+
ret = call(ctx, val, fn)
124129
} else {
125130
ptr := reflect.New(val.Type())
126131
ptr.Elem().Set(val)
127-
ret = call(ptr, fn)
132+
ret = call(ctx, ptr, fn)
128133
}
129134
} else if val.Kind() == reflect.Ptr {
130-
ret = call(val.Elem(), fn)
135+
ret = call(ctx, val.Elem(), fn)
131136
} else {
132-
ret = call(val, fn)
137+
ret = call(ctx, val, fn)
133138
}
134139
if len(ret) > 0 {
135140
if errv := ret[0]; !errv.IsNil() {
@@ -139,12 +144,14 @@ func exec(val reflect.Value, fn reflect.Method) (err error) {
139144
return
140145
}
141146

142-
func call(rcvr reflect.Value, fn reflect.Method) []reflect.Value {
147+
func call(ctx context.Context, rcvr reflect.Value, fn reflect.Method) []reflect.Value {
143148
t := fn.Type
144149
in := make([]reflect.Value, t.NumIn())
145150
for i := range t.NumIn() {
146151
if i == 0 {
147152
in[i] = rcvr
153+
} else if i == 1 && t.In(i) == contextType {
154+
in[i] = reflect.ValueOf(ctx)
148155
} else {
149156
in[i] = reflect.Zero(t.In(i))
150157
}

handler_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ops
22

33
import (
44
"bytes"
5+
"context"
56
"errors"
67
"io"
78
"os"
@@ -352,6 +353,51 @@ func TestCallPointerReceiverWithNonPointer(t *testing.T) {
352353
}
353354
}
354355

356+
type CtxHandler struct{ ctx context.Context }
357+
358+
func (h *CtxHandler) Run(ctx context.Context) error {
359+
h.ctx = ctx
360+
return nil
361+
}
362+
363+
func TestContextHandler(t *testing.T) {
364+
h := CtxHandler{}
365+
366+
err := opHandler(&h, "run")
367+
368+
if err != nil {
369+
t.Errorf(`opHandler(&h, "run") = %q, want <nil>`, err)
370+
}
371+
if h.ctx == nil {
372+
t.Error("ctx is nil, want non-nil")
373+
}
374+
}
375+
376+
func TestContextAfterFunc(t *testing.T) {
377+
done := make(chan struct{})
378+
h := struct {
379+
CtxAfterFuncHandler
380+
}{
381+
CtxAfterFuncHandler: func(ctx context.Context) error {
382+
context.AfterFunc(ctx, func() {
383+
close(done)
384+
})
385+
return nil
386+
},
387+
}
388+
389+
err := opHandler(&h, "run")
390+
391+
if err != nil {
392+
t.Errorf(`opHandler(&h, "run") = %q, want <nil>`, err)
393+
}
394+
<-done
395+
}
396+
397+
type CtxAfterFuncHandler func(context.Context) error
398+
399+
func (h CtxAfterFuncHandler) Run(ctx context.Context) error { return h(ctx) }
400+
355401
func methodname(m reflect.Method) string {
356402
return m.Func.Type().In(0).Elem().Name() + "." + m.Name
357403
}

0 commit comments

Comments
 (0)