Skip to content

Commit 9915cca

Browse files
OrestisIonorestis
andauthored
Implement fluent Task builders (#3)
Co-authored-by: orestis <orestis@thoughtmachine.net>
1 parent 7e9aaf9 commit 9915cca

6 files changed

Lines changed: 311 additions & 17 deletions

File tree

builder.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
package taskgraph
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
)
7+
8+
// TaskBuilder helps construct taskgraph Tasks with a fluent API.
9+
type TaskBuilder[T any] struct {
10+
name string
11+
resultKey Key[T]
12+
depends []any
13+
fn any
14+
condition Condition
15+
defaultVal T
16+
defaultSet bool
17+
defaultBindings []Binding
18+
}
19+
20+
// NewTaskBuilder creates a new builder for a task that produces a result of type T.
21+
func NewTaskBuilder[T any](name string, key Key[T]) *TaskBuilder[T] {
22+
return &TaskBuilder[T]{
23+
name: name,
24+
resultKey: key,
25+
}
26+
}
27+
28+
// DependsOn adds dependencies to the task.
29+
func (b *TaskBuilder[T]) DependsOn(deps ...any) *TaskBuilder[T] {
30+
b.depends = append(b.depends, deps...)
31+
return b
32+
}
33+
34+
// Run sets the function to execute. The function signature must match the dependencies.
35+
func (b *TaskBuilder[T]) Run(fn any) *TaskBuilder[T] {
36+
b.fn = fn
37+
return b
38+
}
39+
40+
// RunIf sets a condition for the task execution.
41+
func (b *TaskBuilder[T]) RunIf(cond Condition) *TaskBuilder[T] {
42+
b.condition = cond
43+
return b
44+
}
45+
46+
// RunIfAll sets a ConditionAnd (logical AND) for the task execution using the provided keys.
47+
func (b *TaskBuilder[T]) RunIfAll(keys ...ReadOnlyKey[bool]) *TaskBuilder[T] {
48+
b.condition = ConditionAnd(keys)
49+
return b
50+
}
51+
52+
// RunIfAny sets a ConditionOr (logical OR) for the task execution using the provided keys.
53+
func (b *TaskBuilder[T]) RunIfAny(keys ...ReadOnlyKey[bool]) *TaskBuilder[T] {
54+
b.condition = ConditionOr(keys)
55+
return b
56+
}
57+
58+
// Default sets the default value for the result key if the condition is false.
59+
func (b *TaskBuilder[T]) Default(val T) *TaskBuilder[T] {
60+
b.defaultVal = val
61+
b.defaultSet = true
62+
return b
63+
}
64+
65+
// WithDefaultBindings adds arbitrary default bindings if the condition is false.
66+
func (b *TaskBuilder[T]) WithDefaultBindings(bindings ...Binding) *TaskBuilder[T] {
67+
b.defaultBindings = append(b.defaultBindings, bindings...)
68+
return b
69+
}
70+
71+
// Build constructs and returns the Task.
72+
func (b *TaskBuilder[T]) Build() (TaskSet, error) {
73+
reflect := Reflect[T]{
74+
Name: b.name,
75+
ResultKey: b.resultKey,
76+
Depends: b.depends,
77+
Fn: b.fn,
78+
}
79+
reflect.location = getLocation(2)
80+
81+
// Eagerly build to validate and get the underlying task
82+
task, err := reflect.Build()
83+
if err != nil {
84+
return nil, err
85+
}
86+
var ts TaskSet = task
87+
88+
if b.condition != nil {
89+
conditional := Conditional{
90+
Wrapped: ts,
91+
Condition: b.condition,
92+
}
93+
94+
if b.defaultSet {
95+
conditional.DefaultBindings = append(
96+
conditional.DefaultBindings,
97+
b.resultKey.Bind(b.defaultVal),
98+
)
99+
}
100+
conditional.DefaultBindings = append(conditional.DefaultBindings, b.defaultBindings...)
101+
102+
conditional.location = getLocation(2)
103+
ts = conditional
104+
}
105+
106+
return ts, nil
107+
}
108+
109+
// Tasks satisfies the TaskSet interface to avoid the need to call Build(). It is equivalent to
110+
// calling Must(Build()).Tasks().
111+
func (b *TaskBuilder[T]) Tasks() []Task {
112+
return Must(b.Build()).Tasks()
113+
}
114+
115+
// MultiTaskBuilder helps construct taskgraph Tasks that provide multiple outputs or perform side effects.
116+
type MultiTaskBuilder struct {
117+
name string
118+
depends []any
119+
fn any
120+
provides []ID
121+
condition Condition
122+
defaultBindings []Binding
123+
errors []error
124+
}
125+
126+
// NewMultiTaskBuilder creates a new builder for a multi-output or side-effect task.
127+
func NewMultiTaskBuilder(name string) *MultiTaskBuilder {
128+
return &MultiTaskBuilder{
129+
name: name,
130+
}
131+
}
132+
133+
// DependsOn adds dependencies to the task.
134+
func (b *MultiTaskBuilder) DependsOn(deps ...any) *MultiTaskBuilder {
135+
b.depends = append(b.depends, deps...)
136+
return b
137+
}
138+
139+
// Provides declares the keys that this task provides.
140+
func (b *MultiTaskBuilder) Provides(keys ...any) *MultiTaskBuilder {
141+
for _, k := range keys {
142+
rk, err := newReflectKey(k)
143+
if err != nil {
144+
b.errors = append(b.errors, fmt.Errorf("invalid key passed to Provides: %w", err))
145+
continue
146+
}
147+
id, err := rk.ID()
148+
if err != nil {
149+
b.errors = append(b.errors, fmt.Errorf("invalid key ID in Provides: %w", err))
150+
continue
151+
}
152+
b.provides = append(b.provides, id)
153+
}
154+
return b
155+
}
156+
157+
// Run sets the function to execute. The function signature must match the dependencies.
158+
// Fn must return []Binding or ([]Binding, error).
159+
func (b *MultiTaskBuilder) Run(fn any) *MultiTaskBuilder {
160+
b.fn = fn
161+
return b
162+
}
163+
164+
// RunIf sets a condition for the task execution.
165+
func (b *MultiTaskBuilder) RunIf(cond Condition) *MultiTaskBuilder {
166+
b.condition = cond
167+
return b
168+
}
169+
170+
// RunIfAll sets a ConditionAnd (logical AND) for the task execution using the provided keys.
171+
func (b *MultiTaskBuilder) RunIfAll(keys ...ReadOnlyKey[bool]) *MultiTaskBuilder {
172+
b.condition = ConditionAnd(keys)
173+
return b
174+
}
175+
176+
// RunIfAny sets a ConditionOr (logical OR) for the task execution using the provided keys.
177+
func (b *MultiTaskBuilder) RunIfAny(keys ...ReadOnlyKey[bool]) *MultiTaskBuilder {
178+
b.condition = ConditionOr(keys)
179+
return b
180+
}
181+
182+
// WithDefaultBindings adds arbitrary default bindings if the condition is false.
183+
func (b *MultiTaskBuilder) WithDefaultBindings(bindings ...Binding) *MultiTaskBuilder {
184+
b.defaultBindings = append(b.defaultBindings, bindings...)
185+
return b
186+
}
187+
188+
// Build constructs and returns the Task.
189+
func (b *MultiTaskBuilder) Build() (TaskSet, error) {
190+
if len(b.errors) > 0 {
191+
return nil, errors.Join(b.errors...)
192+
}
193+
194+
reflect := ReflectMulti{
195+
Name: b.name,
196+
Depends: b.depends,
197+
Fn: b.fn,
198+
Provides: b.provides,
199+
}
200+
reflect.location = getLocation(2)
201+
var task TaskSet = reflect
202+
203+
if b.condition != nil {
204+
conditional := Conditional{
205+
Wrapped: task,
206+
Condition: b.condition,
207+
DefaultBindings: b.defaultBindings,
208+
}
209+
conditional.location = getLocation(2)
210+
task = conditional
211+
}
212+
213+
return task, nil
214+
}
215+
216+
// Tasks satisfies the TaskSet interface to avoid the need to call Build(). It is equivalent to
217+
// calling Must(Build()).Tasks().
218+
func (b *MultiTaskBuilder) Tasks() []Task {
219+
return Must(b.Build()).Tasks()
220+
}

builder_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package taskgraph
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestTaskBuilder_RunIfAll(t *testing.T) {
8+
k1 := NewKey[bool]("k1")
9+
k2 := NewKey[bool]("k2")
10+
res := NewKey[string]("res")
11+
12+
task, err := NewTaskBuilder[string]("test", res).
13+
Run(func() string { return "ok" }).
14+
RunIfAll(k1, k2).
15+
Default("default").
16+
Build()
17+
if err != nil {
18+
t.Fatalf("unexpected error: %v", err)
19+
}
20+
21+
// Simulate execution (simplified verification)
22+
tasks := task.Tasks()
23+
if len(tasks) != 1 {
24+
t.Fatalf("expected 1 task, got %d", len(tasks))
25+
}
26+
}
27+
28+
func TestMultiTaskBuilder_Provides(t *testing.T) {
29+
k1 := NewKey[string]("k1")
30+
k2 := NewKey[int]("k2")
31+
32+
task, err := NewMultiTaskBuilder("multi").
33+
Provides(k1, k2).
34+
Run(func() ([]Binding, error) {
35+
return []Binding{k1.Bind("s"), k2.Bind(1)}, nil
36+
}).
37+
Build()
38+
if err != nil {
39+
t.Fatalf("unexpected error: %v", err)
40+
}
41+
42+
tasks := task.Tasks()
43+
if len(tasks) != 1 {
44+
t.Fatalf("expected 1 task, got %d", len(tasks))
45+
}
46+
provided := tasks[0].Provides()
47+
if len(provided) != 2 {
48+
t.Fatalf("expected 2 provided keys, got %d", len(provided))
49+
}
50+
}
51+
52+
func TestMultiTaskBuilder_Provides_InvalidKey(t *testing.T) {
53+
_, err := NewMultiTaskBuilder("fail").Provides("not a key").Build()
54+
if err == nil {
55+
t.Errorf("expected error on invalid key")
56+
}
57+
}
58+
59+
func TestMultiTaskBuilder_RunIfAny(t *testing.T) {
60+
k1 := NewKey[bool]("k1")
61+
k2 := NewKey[bool]("k2")
62+
63+
task, err := NewMultiTaskBuilder("multi_cond").
64+
RunIfAny(k1, k2).
65+
Run(func() []Binding { return nil }).
66+
Build()
67+
if err != nil {
68+
t.Fatalf("unexpected error: %v", err)
69+
}
70+
71+
if task == nil {
72+
t.Fatal("expected task to be built")
73+
}
74+
}

key.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func (k *key[T]) Get(b Binder) (T, error) {
100100
func NewKey[T any](id string) Key[T] {
101101
return &key[T]{
102102
id: newID("", id),
103-
location: getLocation(),
103+
location: getLocation(2),
104104
}
105105
}
106106

@@ -109,7 +109,7 @@ func NewKey[T any](id string) Key[T] {
109109
func NewNamespacedKey[T any](namespace, id string) Key[T] {
110110
return &key[T]{
111111
id: newID(namespace, id),
112-
location: getLocation(),
112+
location: getLocation(2),
113113
}
114114
}
115115

@@ -130,7 +130,7 @@ func (k *presenceKey[T]) Get(b Binder) (bool, error) {
130130
func Presence[T any](key ReadOnlyKey[T]) ReadOnlyKey[bool] {
131131
return &presenceKey[T]{
132132
ReadOnlyKey: key,
133-
location: getLocation(),
133+
location: getLocation(2),
134134
}
135135
}
136136

@@ -159,7 +159,7 @@ func Mapped[In, Out any](key ReadOnlyKey[In], fn func(In) Out) ReadOnlyKey[Out]
159159
return &mappedKey[In, Out]{
160160
ReadOnlyKey: key,
161161
fn: fn,
162-
location: getLocation(),
162+
location: getLocation(2),
163163
}
164164
}
165165

@@ -188,6 +188,6 @@ func (k *optionalKey[T]) Get(b Binder) (Maybe[T], error) {
188188
func Optional[T any](base ReadOnlyKey[T]) ReadOnlyKey[Maybe[T]] {
189189
return &optionalKey[T]{
190190
ReadOnlyKey: base,
191-
location: getLocation(),
191+
location: getLocation(2),
192192
}
193193
}

reflect.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ type Reflect[T any] struct {
258258
// Locate annotates the Reflect with its location in the source code, to make error messages
259259
// easier to understand. Calling it is recommended but not required if wrapped in a Conditional
260260
func (r Reflect[T]) Locate() Reflect[T] {
261-
r.location = getLocation()
261+
r.location = getLocation(2)
262262
return r
263263
}
264264

@@ -337,7 +337,7 @@ type ReflectMulti struct {
337337
// Locate annotates the ReflectMulti with its location in the source code, to make error messages
338338
// easier to understand. Calling it is recommended but not required if wrapped in a Conditional
339339
func (r ReflectMulti) Locate() ReflectMulti {
340-
r.location = getLocation()
340+
r.location = getLocation(2)
341341
return r
342342
}
343343

0 commit comments

Comments
 (0)