Skip to content

Commit 27f1362

Browse files
author
orestis
committed
Improved error propagation
1 parent 4ded8ff commit 27f1362

2 files changed

Lines changed: 59 additions & 22 deletions

File tree

builder.go

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package taskgraph
22

3-
import "fmt"
3+
import (
4+
"errors"
5+
"fmt"
6+
)
47

58
// TaskBuilder helps construct taskgraph Tasks with a fluent API.
69
type TaskBuilder[T any] struct {
@@ -66,32 +69,47 @@ func (b *TaskBuilder[T]) WithDefaultBindings(bindings ...Binding) *TaskBuilder[T
6669
}
6770

6871
// Build constructs and returns the Task.
69-
func (b *TaskBuilder[T]) Build() TaskSet {
72+
func (b *TaskBuilder[T]) Build() (TaskSet, error) {
7073
reflect := Reflect[T]{
7174
Name: b.name,
7275
ResultKey: b.resultKey,
7376
Depends: b.depends,
7477
Fn: b.fn,
7578
}
7679
reflect.location = getLocation(2)
77-
var task TaskSet = reflect
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
7887

7988
if b.condition != nil {
8089
conditional := Conditional{
81-
Wrapped: task,
90+
Wrapped: ts,
8291
Condition: b.condition,
8392
}
8493

8594
if b.defaultSet {
86-
conditional.DefaultBindings = append(conditional.DefaultBindings, b.resultKey.Bind(b.defaultVal))
95+
conditional.DefaultBindings = append(
96+
conditional.DefaultBindings,
97+
b.resultKey.Bind(b.defaultVal),
98+
)
8799
}
88100
conditional.DefaultBindings = append(conditional.DefaultBindings, b.defaultBindings...)
89101

90102
conditional.location = getLocation(2)
91-
task = conditional
103+
ts = conditional
92104
}
93105

94-
return task
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()).
111+
func (b *TaskBuilder[T]) Tasks() []Task {
112+
return Must(b.Build()).Tasks()
95113
}
96114

97115
// MultiTaskBuilder helps construct taskgraph Tasks that provide multiple outputs or perform side effects.
@@ -102,6 +120,7 @@ type MultiTaskBuilder struct {
102120
provides []ID
103121
condition Condition
104122
defaultBindings []Binding
123+
errors []error
105124
}
106125

107126
// NewMultiTaskBuilder creates a new builder for a multi-output or side-effect task.
@@ -122,11 +141,13 @@ func (b *MultiTaskBuilder) Provides(keys ...any) *MultiTaskBuilder {
122141
for _, k := range keys {
123142
rk, err := newReflectKey(k)
124143
if err != nil {
125-
panic(fmt.Errorf("invalid key passed to Provides: %w", err))
144+
b.errors = append(b.errors, fmt.Errorf("invalid key passed to Provides: %w", err))
145+
continue
126146
}
127147
id, err := rk.ID()
128148
if err != nil {
129-
panic(fmt.Errorf("invalid key ID in Provides: %w", err))
149+
b.errors = append(b.errors, fmt.Errorf("invalid key ID in Provides: %w", err))
150+
continue
130151
}
131152
b.provides = append(b.provides, id)
132153
}
@@ -165,7 +186,11 @@ func (b *MultiTaskBuilder) WithDefaultBindings(bindings ...Binding) *MultiTaskBu
165186
}
166187

167188
// Build constructs and returns the Task.
168-
func (b *MultiTaskBuilder) Build() TaskSet {
189+
func (b *MultiTaskBuilder) Build() (TaskSet, error) {
190+
if len(b.errors) > 0 {
191+
return nil, errors.Join(b.errors...)
192+
}
193+
169194
reflect := ReflectMulti{
170195
Name: b.name,
171196
Depends: b.depends,
@@ -185,5 +210,11 @@ func (b *MultiTaskBuilder) Build() TaskSet {
185210
task = conditional
186211
}
187212

188-
return task
189-
}
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()).
218+
func (b *MultiTaskBuilder) Tasks() []Task {
219+
return Must(b.Build()).Tasks()
220+
}

builder_test.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,35 @@ func TestTaskBuilder_RunIfAll(t *testing.T) {
99
k2 := NewKey[bool]("k2")
1010
res := NewKey[string]("res")
1111

12-
task := NewTaskBuilder[string]("test", res).
12+
task, err := NewTaskBuilder[string]("test", res).
1313
Run(func() string { return "ok" }).
1414
RunIfAll(k1, k2).
1515
Default("default").
1616
Build()
17+
if err != nil {
18+
t.Fatalf("unexpected error: %v", err)
19+
}
1720

1821
// Simulate execution (simplified verification)
1922
tasks := task.Tasks()
2023
if len(tasks) != 1 {
2124
t.Fatalf("expected 1 task, got %d", len(tasks))
2225
}
23-
// We can't easily execute it without a full graph, but we can check if it didn't panic and produced a task.
2426
}
2527

2628
func TestMultiTaskBuilder_Provides(t *testing.T) {
2729
k1 := NewKey[string]("k1")
2830
k2 := NewKey[int]("k2")
2931

30-
task := NewMultiTaskBuilder("multi").
32+
task, err := NewMultiTaskBuilder("multi").
3133
Provides(k1, k2).
3234
Run(func() ([]Binding, error) {
3335
return []Binding{k1.Bind("s"), k2.Bind(1)}, nil
3436
}).
3537
Build()
38+
if err != nil {
39+
t.Fatalf("unexpected error: %v", err)
40+
}
3641

3742
tasks := task.Tasks()
3843
if len(tasks) != 1 {
@@ -45,22 +50,23 @@ func TestMultiTaskBuilder_Provides(t *testing.T) {
4550
}
4651

4752
func TestMultiTaskBuilder_Provides_InvalidKey(t *testing.T) {
48-
defer func() {
49-
if r := recover(); r == nil {
50-
t.Errorf("expected panic on invalid key")
51-
}
52-
}()
53-
NewMultiTaskBuilder("fail").Provides("not a key")
53+
_, err := NewMultiTaskBuilder("fail").Provides("not a key").Build()
54+
if err == nil {
55+
t.Errorf("expected error on invalid key")
56+
}
5457
}
5558

5659
func TestMultiTaskBuilder_RunIfAny(t *testing.T) {
5760
k1 := NewKey[bool]("k1")
5861
k2 := NewKey[bool]("k2")
5962

60-
task := NewMultiTaskBuilder("multi_cond").
63+
task, err := NewMultiTaskBuilder("multi_cond").
6164
RunIfAny(k1, k2).
6265
Run(func() []Binding { return nil }).
6366
Build()
67+
if err != nil {
68+
t.Fatalf("unexpected error: %v", err)
69+
}
6470

6571
if task == nil {
6672
t.Fatal("expected task to be built")

0 commit comments

Comments
 (0)