Skip to content

Commit 86cfde6

Browse files
committed
feat(configwatcher): add write-through methods (SetString/Bool/Int/Float)
Closes #17
1 parent 5d46ab0 commit 86cfde6

2 files changed

Lines changed: 199 additions & 0 deletions

File tree

sdk/configwatcher/write.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package configwatcher
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/opendecree/decree/sdk/configclient"
8+
)
9+
10+
// SetString writes a string value to the server and optimistically updates the
11+
// local cached value for any registered watcher on the same field path.
12+
func (w *Watcher) SetString(ctx context.Context, fieldPath, value string, opts ...WriteOption) error {
13+
return w.set(ctx, fieldPath, configclient.StringVal(value), opts)
14+
}
15+
16+
// SetBool writes a bool value to the server and optimistically updates the
17+
// local cached value for any registered watcher on the same field path.
18+
func (w *Watcher) SetBool(ctx context.Context, fieldPath string, value bool, opts ...WriteOption) error {
19+
return w.set(ctx, fieldPath, configclient.BoolVal(value), opts)
20+
}
21+
22+
// SetInt writes an int64 value to the server and optimistically updates the
23+
// local cached value for any registered watcher on the same field path.
24+
func (w *Watcher) SetInt(ctx context.Context, fieldPath string, value int64, opts ...WriteOption) error {
25+
return w.set(ctx, fieldPath, configclient.IntVal(value), opts)
26+
}
27+
28+
// SetFloat writes a float64 value to the server and optimistically updates the
29+
// local cached value for any registered watcher on the same field path.
30+
func (w *Watcher) SetFloat(ctx context.Context, fieldPath string, value float64, opts ...WriteOption) error {
31+
return w.set(ctx, fieldPath, configclient.FloatVal(value), opts)
32+
}
33+
34+
// WriteOption configures a write-through operation.
35+
type WriteOption func(*configclient.SetFieldRequest)
36+
37+
// WithDescription sets the change description on the write.
38+
func WithDescription(desc string) WriteOption {
39+
return func(req *configclient.SetFieldRequest) { req.Description = desc }
40+
}
41+
42+
// WithExpectedChecksum adds an optimistic-concurrency guard to the write.
43+
func WithExpectedChecksum(cs string) WriteOption {
44+
return func(req *configclient.SetFieldRequest) { req.ExpectedChecksum = &cs }
45+
}
46+
47+
func (w *Watcher) set(ctx context.Context, fieldPath string, tv *configclient.TypedValue, opts []WriteOption) error {
48+
req := &configclient.SetFieldRequest{
49+
TenantID: w.tenantID,
50+
FieldPath: fieldPath,
51+
Value: tv,
52+
}
53+
for _, o := range opts {
54+
o(req)
55+
}
56+
57+
_, err := w.transport.SetField(ctx, req)
58+
if err != nil {
59+
return fmt.Errorf("configwatcher: set %s: %w", fieldPath, err)
60+
}
61+
62+
// Optimistically update the local value if this field is registered.
63+
w.mu.RLock()
64+
entry, ok := w.fields[fieldPath]
65+
w.mu.RUnlock()
66+
if ok && entry.typedUpdate != nil {
67+
entry.typedUpdate(tv)
68+
}
69+
return nil
70+
}

sdk/configwatcher/write_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package configwatcher
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
"github.com/opendecree/decree/sdk/configclient"
9+
)
10+
11+
type writeTransport struct {
12+
mockTransport
13+
setFieldFn func(ctx context.Context, req *configclient.SetFieldRequest) (*configclient.SetFieldResponse, error)
14+
}
15+
16+
func (t *writeTransport) SetField(ctx context.Context, req *configclient.SetFieldRequest) (*configclient.SetFieldResponse, error) {
17+
return t.setFieldFn(ctx, req)
18+
}
19+
20+
func TestSetString_WritesAndUpdatesLocal(t *testing.T) {
21+
var written *configclient.SetFieldRequest
22+
tr := &writeTransport{
23+
mockTransport: mockTransport{
24+
getConfigFn: func(_ context.Context, _ *configclient.GetConfigRequest) (*configclient.GetConfigResponse, error) {
25+
return &configclient.GetConfigResponse{
26+
Values: []configclient.ConfigValue{{FieldPath: "app.env", Value: configclient.StringVal("staging")}},
27+
}, nil
28+
},
29+
subscribeFn: func(ctx context.Context, _ *configclient.SubscribeRequest) (configclient.Subscription, error) {
30+
return newMockSubscription(ctx), nil
31+
},
32+
},
33+
setFieldFn: func(_ context.Context, req *configclient.SetFieldRequest) (*configclient.SetFieldResponse, error) {
34+
written = req
35+
return &configclient.SetFieldResponse{}, nil
36+
},
37+
}
38+
39+
w := New(tr, "tenant1")
40+
val, err := w.String("app.env", "default")
41+
if err != nil {
42+
t.Fatalf("String: %v", err)
43+
}
44+
if err := w.Start(context.Background()); err != nil {
45+
t.Fatalf("Start: %v", err)
46+
}
47+
defer w.Close()
48+
49+
if err := w.SetString(context.Background(), "app.env", "production"); err != nil {
50+
t.Fatalf("SetString: %v", err)
51+
}
52+
53+
// Server was called with correct params.
54+
if written == nil || written.FieldPath != "app.env" {
55+
t.Errorf("SetField not called with expected field path, got %+v", written)
56+
}
57+
sv, _ := written.Value.StringValue()
58+
if sv != "production" {
59+
t.Errorf("SetField value = %q, want %q", sv, "production")
60+
}
61+
62+
// Optimistic local update applied.
63+
if got := val.Get(); got != "production" {
64+
t.Errorf("local value = %q, want %q", got, "production")
65+
}
66+
}
67+
68+
func TestSetString_TransportError(t *testing.T) {
69+
tr := &writeTransport{
70+
mockTransport: mockTransport{
71+
getConfigFn: func(_ context.Context, _ *configclient.GetConfigRequest) (*configclient.GetConfigResponse, error) {
72+
return &configclient.GetConfigResponse{}, nil
73+
},
74+
subscribeFn: func(ctx context.Context, _ *configclient.SubscribeRequest) (configclient.Subscription, error) {
75+
return newMockSubscription(ctx), nil
76+
},
77+
},
78+
setFieldFn: func(_ context.Context, _ *configclient.SetFieldRequest) (*configclient.SetFieldResponse, error) {
79+
return nil, errors.New("server error")
80+
},
81+
}
82+
83+
w := New(tr, "tenant1")
84+
if err := w.Start(context.Background()); err != nil {
85+
t.Fatalf("Start: %v", err)
86+
}
87+
defer w.Close()
88+
89+
err := w.SetString(context.Background(), "app.env", "production")
90+
if err == nil {
91+
t.Error("expected error from transport, got nil")
92+
}
93+
}
94+
95+
func TestSetBool_WriteThroughOptions(t *testing.T) {
96+
var req *configclient.SetFieldRequest
97+
tr := &writeTransport{
98+
mockTransport: mockTransport{
99+
getConfigFn: func(_ context.Context, _ *configclient.GetConfigRequest) (*configclient.GetConfigResponse, error) {
100+
return &configclient.GetConfigResponse{}, nil
101+
},
102+
subscribeFn: func(ctx context.Context, _ *configclient.SubscribeRequest) (configclient.Subscription, error) {
103+
return newMockSubscription(ctx), nil
104+
},
105+
},
106+
setFieldFn: func(_ context.Context, r *configclient.SetFieldRequest) (*configclient.SetFieldResponse, error) {
107+
req = r
108+
return &configclient.SetFieldResponse{}, nil
109+
},
110+
}
111+
w := New(tr, "tenant1")
112+
if err := w.Start(context.Background()); err != nil {
113+
t.Fatalf("Start: %v", err)
114+
}
115+
defer w.Close()
116+
117+
if err := w.SetBool(context.Background(), "app.debug", true,
118+
WithDescription("enable debug"),
119+
WithExpectedChecksum("abc123"),
120+
); err != nil {
121+
t.Fatalf("SetBool: %v", err)
122+
}
123+
if req.Description != "enable debug" {
124+
t.Errorf("Description = %q, want %q", req.Description, "enable debug")
125+
}
126+
if req.ExpectedChecksum == nil || *req.ExpectedChecksum != "abc123" {
127+
t.Errorf("ExpectedChecksum = %v, want abc123", req.ExpectedChecksum)
128+
}
129+
}

0 commit comments

Comments
 (0)