Skip to content

Commit 03ecbac

Browse files
dannykoppingEmyrk
authored andcommitted
Support zero values
Signed-off-by: Danny Kopping <danny@coder.com>
1 parent 5c92688 commit 03ecbac

File tree

4 files changed

+99
-57
lines changed

4 files changed

+99
-57
lines changed

coderd/runtimeconfig/config.go

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@ import (
99
"golang.org/x/xerrors"
1010
)
1111

12+
var ErrKeyNotSet = xerrors.New("key is not set")
13+
1214
// TODO: comment
1315
type Value pflag.Value
1416

1517
type Entry[T Value] struct {
16-
val T
17-
key string
18+
k string
19+
v T
1820
}
1921

2022
func New[T Value](key, val string) (out Entry[T], err error) {
21-
out.Init(key)
23+
out.k = key
2224

23-
if err = out.Set(val); err != nil {
25+
if err = out.SetStartupValue(val); err != nil {
2426
return out, err
2527
}
2628

@@ -35,38 +37,66 @@ func MustNew[T Value](key, val string) Entry[T] {
3537
return out
3638
}
3739

38-
func (o *Entry[T]) Init(key string) {
39-
o.val = create[T]()
40-
o.key = key
40+
func (e *Entry[T]) val() T {
41+
if reflect.ValueOf(e.v).IsNil() {
42+
e.v = create[T]()
43+
}
44+
return e.v
4145
}
4246

43-
func (o *Entry[T]) Set(s string) error {
44-
if reflect.ValueOf(o.val).IsNil() {
45-
return xerrors.Errorf("instance of %T is uninitialized", o.val)
47+
func (e *Entry[T]) key() (string, error) {
48+
if e.k == "" {
49+
return "", ErrKeyNotSet
4650
}
47-
return o.val.Set(s)
51+
52+
return e.k, nil
4853
}
4954

50-
func (o *Entry[T]) Type() string {
51-
return o.val.Type()
55+
func (e *Entry[T]) SetKey(k string) {
56+
e.k = k
5257
}
5358

54-
func (o *Entry[T]) String() string {
55-
return o.val.String()
59+
func (e *Entry[T]) SetStartupValue(s string) error {
60+
return e.val().Set(s)
5661
}
5762

58-
func (o *Entry[T]) StartupValue() T {
59-
return o.val
63+
func (e *Entry[T]) MustSet(s string) {
64+
err := e.val().Set(s)
65+
if err != nil {
66+
panic(err)
67+
}
6068
}
6169

62-
func (o *Entry[T]) Resolve(ctx context.Context, r Resolver) (T, error) {
63-
return o.resolve(ctx, r)
70+
func (e *Entry[T]) Type() string {
71+
return e.val().Type()
6472
}
6573

66-
func (o *Entry[T]) resolve(ctx context.Context, r Resolver) (T, error) {
74+
func (e *Entry[T]) String() string {
75+
return e.val().String()
76+
}
77+
78+
func (e *Entry[T]) StartupValue() T {
79+
return e.val()
80+
}
81+
82+
func (e *Entry[T]) SetRuntimeValue(ctx context.Context, m Mutator, val T) error {
83+
key, err := e.key()
84+
if err != nil {
85+
return err
86+
}
87+
88+
return m.MutateByKey(ctx, key, val.String())
89+
}
90+
91+
func (e *Entry[T]) Resolve(ctx context.Context, r Resolver) (T, error) {
6792
var zero T
6893

69-
val, err := r.ResolveByKey(ctx, o.key)
94+
key, err := e.key()
95+
if err != nil {
96+
return zero, err
97+
}
98+
99+
val, err := r.ResolveByKey(ctx, key)
70100
if err != nil {
71101
return zero, err
72102
}
@@ -78,17 +108,13 @@ func (o *Entry[T]) resolve(ctx context.Context, r Resolver) (T, error) {
78108
return inst, nil
79109
}
80110

81-
func (o *Entry[T]) Save(ctx context.Context, m Mutator, val T) error {
82-
return m.MutateByKey(ctx, o.key, val.String())
83-
}
84-
85-
func (o *Entry[T]) Coalesce(ctx context.Context, r Resolver) (T, error) {
111+
func (e *Entry[T]) Coalesce(ctx context.Context, r Resolver) (T, error) {
86112
var zero T
87113

88-
resolved, err := o.resolve(ctx, r)
114+
resolved, err := e.Resolve(ctx, r)
89115
if err != nil {
90116
if errors.Is(err, EntryNotFound) {
91-
return o.StartupValue(), nil
117+
return e.StartupValue(), nil
92118
}
93119
return zero, err
94120
}

coderd/runtimeconfig/config_test.go

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package runtimeconfig_test
22

33
import (
4+
"context"
45
"testing"
56

67
"github.com/coder/serpent"
78
"github.com/stretchr/testify/require"
89

910
"github.com/coder/coder/v2/coderd/coderdtest"
1011
"github.com/coder/coder/v2/coderd/runtimeconfig"
12+
"github.com/coder/coder/v2/coderd/util/ptr"
1113
"github.com/coder/coder/v2/codersdk"
1214
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
1315
"github.com/coder/coder/v2/enterprise/coderd/license"
@@ -30,20 +32,39 @@ func TestConfig(t *testing.T) {
3032
})
3133
altOrg := coderdenttest.CreateOrganization(t, adminClient, coderdenttest.CreateOrganizationOptions{})
3234

33-
t.Run("panics unless initialized", func(t *testing.T) {
35+
t.Run("new", func(t *testing.T) {
3436
t.Parallel()
3537

36-
field := runtimeconfig.Entry[*serpent.String]{}
3738
require.Panics(t, func() {
38-
field.StartupValue().String()
39+
// "hello" cannot be set on a *serpent.Float64 field.
40+
runtimeconfig.MustNew[*serpent.Float64]("key", "hello")
3941
})
4042

41-
field.Init("my-field")
4243
require.NotPanics(t, func() {
43-
field.StartupValue().String()
44+
runtimeconfig.MustNew[*serpent.Float64]("key", "91.1234")
4445
})
4546
})
4647

48+
t.Run("zero", func(t *testing.T) {
49+
t.Parallel()
50+
51+
// A zero-value declaration of a runtimeconfig.Entry should behave as a zero value of the generic type.
52+
// NB! A key has not been set for this entry.
53+
var field runtimeconfig.Entry[*serpent.Bool]
54+
var zero serpent.Bool
55+
require.Equal(t, field.StartupValue().Value(), zero.Value())
56+
57+
// Setting a value will not produce an error.
58+
require.NoError(t, field.SetStartupValue("true"))
59+
60+
// But attempting to resolve will produce an error.
61+
_, err := field.Resolve(context.Background(), runtimeconfig.NewNoopResolver())
62+
require.ErrorIs(t, err, runtimeconfig.ErrKeyNotSet)
63+
// But attempting to set the runtime value will produce an error.
64+
val := serpent.BoolOf(ptr.Ref(true))
65+
require.ErrorIs(t, field.SetRuntimeValue(context.Background(), runtimeconfig.NewNoopMutator(), val), runtimeconfig.ErrKeyNotSet)
66+
})
67+
4768
t.Run("simple", func(t *testing.T) {
4869
t.Parallel()
4970

@@ -57,12 +78,9 @@ func TestConfig(t *testing.T) {
5778
override = serpent.String("dogfood@dev.coder.com")
5879
)
5980

60-
field := runtimeconfig.Entry[*serpent.String]{}
61-
field.Init("my-field")
62-
// Check that no default has been set.
63-
require.Empty(t, field.StartupValue().String())
64-
// Initialize the value.
65-
require.NoError(t, field.Set(base.String()))
81+
field := runtimeconfig.MustNew[*serpent.String]("my-field", base.String())
82+
// Check that default has been set.
83+
require.Equal(t, base.String(), field.StartupValue().String())
6684
// Validate that it returns that value.
6785
require.Equal(t, base.String(), field.String())
6886
// Validate that there is no org-level override right now.
@@ -73,7 +91,7 @@ func TestConfig(t *testing.T) {
7391
require.NoError(t, err)
7492
require.Equal(t, base.String(), val.String())
7593
// Set an org-level override.
76-
require.NoError(t, field.Save(ctx, mutator, &override))
94+
require.NoError(t, field.SetRuntimeValue(ctx, mutator, &override))
7795
// Coalesce now returns the org-level value.
7896
val, err = field.Coalesce(ctx, resolver)
7997
require.NoError(t, err)
@@ -88,8 +106,6 @@ func TestConfig(t *testing.T) {
88106
resolver := runtimeconfig.NewOrgResolver(altOrg.ID, runtimeconfig.NewStoreResolver(store))
89107
mutator := runtimeconfig.NewOrgMutator(altOrg.ID, runtimeconfig.NewStoreMutator(store))
90108

91-
field := runtimeconfig.Entry[*serpent.Struct[map[string]string]]{}
92-
field.Init("my-field")
93109
var (
94110
base = serpent.Struct[map[string]string]{
95111
Value: map[string]string{"access_type": "offline"},
@@ -102,10 +118,10 @@ func TestConfig(t *testing.T) {
102118
}
103119
)
104120

105-
// Check that no default has been set.
106-
require.Empty(t, field.StartupValue().Value)
107-
// Initialize the value.
108-
require.NoError(t, field.Set(base.String()))
121+
field := runtimeconfig.MustNew[*serpent.Struct[map[string]string]]("my-field", base.String())
122+
123+
// Check that default has been set.
124+
require.Equal(t, base.String(), field.StartupValue().String())
109125
// Validate that there is no org-level override right now.
110126
_, err := field.Resolve(ctx, resolver)
111127
require.ErrorIs(t, err, runtimeconfig.EntryNotFound)
@@ -114,7 +130,7 @@ func TestConfig(t *testing.T) {
114130
require.NoError(t, err)
115131
require.Equal(t, base.Value, val.Value)
116132
// Set an org-level override.
117-
require.NoError(t, field.Save(ctx, mutator, &override))
133+
require.NoError(t, field.SetRuntimeValue(ctx, mutator, &override))
118134
// Coalesce now returns the org-level value.
119135
structVal, err := field.Resolve(ctx, resolver)
120136
require.NoError(t, err)

coderd/runtimeconfig/mutator.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,13 @@ func NewOrgMutator(orgID uuid.UUID, inner Mutator) *OrgMutator {
3838
func (m OrgMutator) MutateByKey(ctx context.Context, key, val string) error {
3939
return m.inner.MutateByKey(ctx, orgKey(m.orgID, key), val)
4040
}
41+
42+
type NoopMutator struct{}
43+
44+
func (n *NoopMutator) MutateByKey(ctx context.Context, key, val string) error {
45+
return nil
46+
}
47+
48+
func NewNoopMutator() *NoopMutator {
49+
return &NoopMutator{}
50+
}

coderd/runtimeconfig/spec.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,6 @@ type Initializer interface {
66
Init(key string)
77
}
88

9-
// type RuntimeConfigResolver[T Value] interface {
10-
// StartupValue() T
11-
// Resolve(r Resolver) (T, error)
12-
// Coalesce(r Resolver) (T, error)
13-
// }
14-
//
15-
// type RuntimeConfigMutator[T Value] interface {
16-
// Save(m Mutator, val T) error
17-
// }
18-
199
type Resolver interface {
2010
ResolveByKey(ctx context.Context, key string) (string, error)
2111
}

0 commit comments

Comments
 (0)