Skip to content

Commit 4153cd7

Browse files
authored
refactor(config,adminclient): functional options for NewService + New (#249)
Completes the functional-options sweep from #235 / #236 by revisiting the two candidates dropped from the original survey on closer reading. - config.NewService(store, cache, publisher, subscriber, opts...) — the four required deps stay positional; Logger, CacheMetrics, Metrics, Validators, and Recorder become With...() options. ServiceConfig struct removed. - adminclient.New(opts...) — all four transports are independently optional per the long-standing nil-allowed contract; the constructor takes no positional args and the `nil, nil, nil` placeholders disappear from call sites.
1 parent 73b06ff commit 4153cd7

10 files changed

Lines changed: 196 additions & 146 deletions

File tree

cmd/decree/config_integration_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,10 @@ func (f *fakeConfigTransport) SetFields(_ context.Context, req *configclient.Set
5555
func buildClients(t *testing.T, fields ...adminclient.Field) (*adminclient.Client, *configclient.Client, *fakeConfigTransport) {
5656
t.Helper()
5757
admin := adminclient.New(
58-
&fakeSchemaTransport{
58+
adminclient.WithSchemaTransport(&fakeSchemaTransport{
5959
tenant: &adminclient.Tenant{ID: "t1", SchemaID: "s1", SchemaVersion: 1},
6060
schema: &adminclient.Schema{ID: "s1", Version: 1, Fields: fields},
61-
},
62-
nil, nil, nil,
61+
}),
6362
)
6463
tr := &fakeConfigTransport{}
6564
cfg := configclient.New(tr)

cmd/server/main.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -244,17 +244,13 @@ func run() int {
244244
logger.InfoContext(ctx, "schema service enabled")
245245
}
246246
if srv.IsServiceEnabled("config") {
247-
configSvc := config.NewService(config.ServiceConfig{
248-
Store: configStore,
249-
Cache: configCache,
250-
Publisher: publisher,
251-
Subscriber: subscriber,
252-
Logger: logger,
253-
CacheMetrics: cacheMetrics,
254-
Metrics: configMetrics,
255-
Validators: validatorFactory,
256-
Recorder: recorder,
257-
})
247+
configSvc := config.NewService(configStore, configCache, publisher, subscriber,
248+
config.WithLogger(logger),
249+
config.WithCacheMetrics(cacheMetrics),
250+
config.WithMetrics(configMetrics),
251+
config.WithValidators(validatorFactory),
252+
config.WithRecorder(recorder),
253+
)
258254
pb.RegisterConfigServiceServer(srv.GRPCServer(), configSvc)
259255
srv.SetServiceHealthy("centralconfig.v1.ConfigService")
260256
logger.InfoContext(ctx, "config service enabled")

internal/config/service.go

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,41 @@ func (e *dependentRequiredError) Unwrap() error { return e.err }
3535

3636
const defaultCacheTTL = 5 * time.Minute
3737

38-
// ServiceConfig holds dependencies for the ConfigService.
39-
type ServiceConfig struct {
40-
Store Store
41-
Cache cache.ConfigCache
42-
Publisher pubsub.Publisher
43-
Subscriber pubsub.Subscriber
44-
Logger *slog.Logger
45-
CacheMetrics *telemetry.CacheMetrics
46-
Metrics *telemetry.ConfigMetrics
47-
Validators *validation.ValidatorFactory
48-
Recorder *audit.UsageRecorder
38+
// Option configures a Service.
39+
type Option func(*serviceOptions)
40+
41+
type serviceOptions struct {
42+
logger *slog.Logger
43+
cacheMetrics *telemetry.CacheMetrics
44+
metrics *telemetry.ConfigMetrics
45+
validators *validation.ValidatorFactory
46+
recorder *audit.UsageRecorder
47+
}
48+
49+
// WithLogger sets the service logger. Defaults to slog.Default() when unset.
50+
func WithLogger(l *slog.Logger) Option {
51+
return func(o *serviceOptions) { o.logger = l }
52+
}
53+
54+
// WithCacheMetrics wires cache hit/miss metrics. Nil disables them.
55+
func WithCacheMetrics(m *telemetry.CacheMetrics) Option {
56+
return func(o *serviceOptions) { o.cacheMetrics = m }
57+
}
58+
59+
// WithMetrics wires write/version metrics. Nil disables them.
60+
func WithMetrics(m *telemetry.ConfigMetrics) Option {
61+
return func(o *serviceOptions) { o.metrics = m }
62+
}
63+
64+
// WithValidators wires the schema validator factory. Nil disables per-field
65+
// validation and dependentRequired checks.
66+
func WithValidators(v *validation.ValidatorFactory) Option {
67+
return func(o *serviceOptions) { o.validators = v }
68+
}
69+
70+
// WithRecorder wires an audit usage recorder. Nil disables read tracking.
71+
func WithRecorder(r *audit.UsageRecorder) Option {
72+
return func(o *serviceOptions) { o.recorder = r }
4973
}
5074

5175
// Service implements the ConfigService gRPC server.
@@ -62,18 +86,24 @@ type Service struct {
6286
recorder *audit.UsageRecorder
6387
}
6488

65-
// NewService creates a new ConfigService.
66-
func NewService(cfg ServiceConfig) *Service {
89+
// NewService creates a new ConfigService. The four required dependencies
90+
// (store, cache, publisher, subscriber) are positional; everything else is
91+
// optional and may be passed via With...() options.
92+
func NewService(store Store, cache cache.ConfigCache, publisher pubsub.Publisher, subscriber pubsub.Subscriber, opts ...Option) *Service {
93+
o := serviceOptions{logger: slog.Default()}
94+
for _, opt := range opts {
95+
opt(&o)
96+
}
6797
return &Service{
68-
store: cfg.Store,
69-
cache: cfg.Cache,
70-
publisher: cfg.Publisher,
71-
subscriber: cfg.Subscriber,
72-
logger: cfg.Logger,
73-
cacheMetrics: cfg.CacheMetrics,
74-
metrics: cfg.Metrics,
75-
validators: cfg.Validators,
76-
recorder: cfg.Recorder,
98+
store: store,
99+
cache: cache,
100+
publisher: publisher,
101+
subscriber: subscriber,
102+
logger: o.logger,
103+
cacheMetrics: o.cacheMetrics,
104+
metrics: o.metrics,
105+
validators: o.validators,
106+
recorder: o.recorder,
77107
}
78108
}
79109

internal/config/service_test.go

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,9 @@ func newTestService() (*Service, *mockStore, *mockCache, *mockPublisher) {
3636
c := &mockCache{}
3737
pub := &mockPublisher{}
3838
sub := &mockSubscriber{}
39-
svc := NewService(ServiceConfig{
40-
Store: store,
41-
Cache: c,
42-
Publisher: pub,
43-
Subscriber: sub,
44-
Logger: testLogger,
45-
})
39+
svc := NewService(store, c, pub, sub,
40+
WithLogger(testLogger),
41+
)
4642
return svc, store, c, pub
4743
}
4844

@@ -52,14 +48,10 @@ func newTestServiceWithValidation() (*Service, *mockStore) {
5248
pub := &mockPublisher{}
5349
sub := &mockSubscriber{}
5450
vf := validation.NewValidatorFactory(store)
55-
svc := NewService(ServiceConfig{
56-
Store: store,
57-
Cache: c,
58-
Publisher: pub,
59-
Subscriber: sub,
60-
Logger: testLogger,
61-
Validators: vf,
62-
})
51+
svc := NewService(store, c, pub, sub,
52+
WithLogger(testLogger),
53+
WithValidators(vf),
54+
)
6355
return svc, store
6456
}
6557

@@ -552,12 +544,10 @@ func TestGetField_RecordsUsage(t *testing.T) {
552544
audit.WithFlushInterval(time.Hour),
553545
audit.WithLogger(testLogger),
554546
)
555-
svc := NewService(ServiceConfig{
556-
Store: store,
557-
Cache: c,
558-
Logger: testLogger,
559-
Recorder: recorder,
560-
})
547+
svc := NewService(store, c, nil, nil,
548+
WithLogger(testLogger),
549+
WithRecorder(recorder),
550+
)
561551
ctx := context.Background()
562552

563553
store.On("GetLatestConfigVersion", ctx, tenantID1).
@@ -587,12 +577,10 @@ func TestGetConfig_RecordsUsage(t *testing.T) {
587577
audit.WithFlushInterval(time.Hour),
588578
audit.WithLogger(testLogger),
589579
)
590-
svc := NewService(ServiceConfig{
591-
Store: store,
592-
Cache: c,
593-
Logger: testLogger,
594-
Recorder: recorder,
595-
})
580+
svc := NewService(store, c, nil, nil,
581+
WithLogger(testLogger),
582+
WithRecorder(recorder),
583+
)
596584
ctx := context.Background()
597585

598586
store.On("GetLatestConfigVersion", ctx, tenantID1).
@@ -630,12 +618,10 @@ func TestGetFields_RecordsUsage(t *testing.T) {
630618
audit.WithFlushInterval(time.Hour),
631619
audit.WithLogger(testLogger),
632620
)
633-
svc := NewService(ServiceConfig{
634-
Store: store,
635-
Cache: c,
636-
Logger: testLogger,
637-
Recorder: recorder,
638-
})
621+
svc := NewService(store, c, nil, nil,
622+
WithLogger(testLogger),
623+
WithRecorder(recorder),
624+
)
639625
ctx := context.Background()
640626

641627
store.On("GetLatestConfigVersion", ctx, tenantID1).
@@ -669,12 +655,10 @@ func TestGetConfig_CacheHit_RecordsUsage(t *testing.T) {
669655
audit.WithFlushInterval(time.Hour),
670656
audit.WithLogger(testLogger),
671657
)
672-
svc := NewService(ServiceConfig{
673-
Store: store,
674-
Cache: c,
675-
Logger: testLogger,
676-
Recorder: recorder,
677-
})
658+
svc := NewService(store, c, nil, nil,
659+
WithLogger(testLogger),
660+
WithRecorder(recorder),
661+
)
678662
ctx := context.Background()
679663

680664
store.On("GetLatestConfigVersion", ctx, tenantID1).

internal/server/memory_integration_test.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,12 @@ func TestMemoryBackend_Integration(t *testing.T) {
5454
schemaSvc := schema.NewService(memSchema, slog.Default(), telemetry.NewSchemaMetrics(telemetry.Config{}), validatorFactory)
5555
pb.RegisterSchemaServiceServer(srv.GRPCServer(), schemaSvc)
5656

57-
configSvc := config.NewService(config.ServiceConfig{
58-
Store: memConfig,
59-
Cache: cache.NewMemoryCache(0),
60-
Publisher: memPubSub,
61-
Subscriber: memPubSub,
62-
Logger: slog.Default(),
63-
CacheMetrics: telemetry.NewCacheMetrics(telemetry.Config{}),
64-
Metrics: telemetry.NewConfigMetrics(telemetry.Config{}),
65-
Validators: validatorFactory,
66-
})
57+
configSvc := config.NewService(memConfig, cache.NewMemoryCache(0), memPubSub, memPubSub,
58+
config.WithLogger(slog.Default()),
59+
config.WithCacheMetrics(telemetry.NewCacheMetrics(telemetry.Config{})),
60+
config.WithMetrics(telemetry.NewConfigMetrics(telemetry.Config{})),
61+
config.WithValidators(validatorFactory),
62+
)
6763
pb.RegisterConfigServiceServer(srv.GRPCServer(), configSvc)
6864

6965
auditSvc := audit.NewService(audit.NewMemoryStore(), slog.Default(), nil)

sdk/adminclient/client.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,27 @@ type Client struct {
1616
server ServerTransport
1717
}
1818

19-
// New creates a new admin client using the given transport implementations.
20-
// Any of the transports may be nil if that service is not needed;
21-
// methods for a nil service will return [ErrServiceNotConfigured].
19+
// New creates a new admin client. Pass [WithSchemaTransport],
20+
// [WithConfigTransport], [WithAuditTransport], and [WithServerTransport] for
21+
// the services you need. Methods on services without a wired transport return
22+
// [ErrServiceNotConfigured].
2223
//
2324
// Example (with grpctransport):
2425
//
2526
// client := grpctransport.NewAdminClient(conn, grpctransport.WithSubject("admin"))
2627
//
2728
// Example (with explicit transports):
2829
//
29-
// client := adminclient.New(schemaTransport, configTransport, auditTransport, serverTransport)
30-
func New(schema SchemaTransport, config ConfigTransport, audit AuditTransport, server ServerTransport) *Client {
31-
return &Client{schema: schema, config: config, audit: audit, server: server}
30+
// client := adminclient.New(
31+
// adminclient.WithSchemaTransport(schemaTransport),
32+
// adminclient.WithConfigTransport(configTransport),
33+
// adminclient.WithAuditTransport(auditTransport),
34+
// adminclient.WithServerTransport(serverTransport),
35+
// )
36+
func New(opts ...Option) *Client {
37+
o := clientOptions{}
38+
for _, opt := range opts {
39+
opt(&o)
40+
}
41+
return &Client{schema: o.schema, config: o.config, audit: o.audit, server: o.server}
3242
}

0 commit comments

Comments
 (0)