diff --git a/cmd/decree/config_integration_test.go b/cmd/decree/config_integration_test.go index e6e87736..6586c6ff 100644 --- a/cmd/decree/config_integration_test.go +++ b/cmd/decree/config_integration_test.go @@ -55,11 +55,10 @@ func (f *fakeConfigTransport) SetFields(_ context.Context, req *configclient.Set func buildClients(t *testing.T, fields ...adminclient.Field) (*adminclient.Client, *configclient.Client, *fakeConfigTransport) { t.Helper() admin := adminclient.New( - &fakeSchemaTransport{ + adminclient.WithSchemaTransport(&fakeSchemaTransport{ tenant: &adminclient.Tenant{ID: "t1", SchemaID: "s1", SchemaVersion: 1}, schema: &adminclient.Schema{ID: "s1", Version: 1, Fields: fields}, - }, - nil, nil, nil, + }), ) tr := &fakeConfigTransport{} cfg := configclient.New(tr) diff --git a/cmd/server/main.go b/cmd/server/main.go index 8acc6384..9857f5a2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -244,17 +244,13 @@ func run() int { logger.InfoContext(ctx, "schema service enabled") } if srv.IsServiceEnabled("config") { - configSvc := config.NewService(config.ServiceConfig{ - Store: configStore, - Cache: configCache, - Publisher: publisher, - Subscriber: subscriber, - Logger: logger, - CacheMetrics: cacheMetrics, - Metrics: configMetrics, - Validators: validatorFactory, - Recorder: recorder, - }) + configSvc := config.NewService(configStore, configCache, publisher, subscriber, + config.WithLogger(logger), + config.WithCacheMetrics(cacheMetrics), + config.WithMetrics(configMetrics), + config.WithValidators(validatorFactory), + config.WithRecorder(recorder), + ) pb.RegisterConfigServiceServer(srv.GRPCServer(), configSvc) srv.SetServiceHealthy("centralconfig.v1.ConfigService") logger.InfoContext(ctx, "config service enabled") diff --git a/internal/config/service.go b/internal/config/service.go index 78444001..f74d88b8 100644 --- a/internal/config/service.go +++ b/internal/config/service.go @@ -35,17 +35,41 @@ func (e *dependentRequiredError) Unwrap() error { return e.err } const defaultCacheTTL = 5 * time.Minute -// ServiceConfig holds dependencies for the ConfigService. -type ServiceConfig struct { - Store Store - Cache cache.ConfigCache - Publisher pubsub.Publisher - Subscriber pubsub.Subscriber - Logger *slog.Logger - CacheMetrics *telemetry.CacheMetrics - Metrics *telemetry.ConfigMetrics - Validators *validation.ValidatorFactory - Recorder *audit.UsageRecorder +// Option configures a Service. +type Option func(*serviceOptions) + +type serviceOptions struct { + logger *slog.Logger + cacheMetrics *telemetry.CacheMetrics + metrics *telemetry.ConfigMetrics + validators *validation.ValidatorFactory + recorder *audit.UsageRecorder +} + +// WithLogger sets the service logger. Defaults to slog.Default() when unset. +func WithLogger(l *slog.Logger) Option { + return func(o *serviceOptions) { o.logger = l } +} + +// WithCacheMetrics wires cache hit/miss metrics. Nil disables them. +func WithCacheMetrics(m *telemetry.CacheMetrics) Option { + return func(o *serviceOptions) { o.cacheMetrics = m } +} + +// WithMetrics wires write/version metrics. Nil disables them. +func WithMetrics(m *telemetry.ConfigMetrics) Option { + return func(o *serviceOptions) { o.metrics = m } +} + +// WithValidators wires the schema validator factory. Nil disables per-field +// validation and dependentRequired checks. +func WithValidators(v *validation.ValidatorFactory) Option { + return func(o *serviceOptions) { o.validators = v } +} + +// WithRecorder wires an audit usage recorder. Nil disables read tracking. +func WithRecorder(r *audit.UsageRecorder) Option { + return func(o *serviceOptions) { o.recorder = r } } // Service implements the ConfigService gRPC server. @@ -62,18 +86,24 @@ type Service struct { recorder *audit.UsageRecorder } -// NewService creates a new ConfigService. -func NewService(cfg ServiceConfig) *Service { +// NewService creates a new ConfigService. The four required dependencies +// (store, cache, publisher, subscriber) are positional; everything else is +// optional and may be passed via With...() options. +func NewService(store Store, cache cache.ConfigCache, publisher pubsub.Publisher, subscriber pubsub.Subscriber, opts ...Option) *Service { + o := serviceOptions{logger: slog.Default()} + for _, opt := range opts { + opt(&o) + } return &Service{ - store: cfg.Store, - cache: cfg.Cache, - publisher: cfg.Publisher, - subscriber: cfg.Subscriber, - logger: cfg.Logger, - cacheMetrics: cfg.CacheMetrics, - metrics: cfg.Metrics, - validators: cfg.Validators, - recorder: cfg.Recorder, + store: store, + cache: cache, + publisher: publisher, + subscriber: subscriber, + logger: o.logger, + cacheMetrics: o.cacheMetrics, + metrics: o.metrics, + validators: o.validators, + recorder: o.recorder, } } diff --git a/internal/config/service_test.go b/internal/config/service_test.go index edd092c5..03e21714 100644 --- a/internal/config/service_test.go +++ b/internal/config/service_test.go @@ -36,13 +36,9 @@ func newTestService() (*Service, *mockStore, *mockCache, *mockPublisher) { c := &mockCache{} pub := &mockPublisher{} sub := &mockSubscriber{} - svc := NewService(ServiceConfig{ - Store: store, - Cache: c, - Publisher: pub, - Subscriber: sub, - Logger: testLogger, - }) + svc := NewService(store, c, pub, sub, + WithLogger(testLogger), + ) return svc, store, c, pub } @@ -52,14 +48,10 @@ func newTestServiceWithValidation() (*Service, *mockStore) { pub := &mockPublisher{} sub := &mockSubscriber{} vf := validation.NewValidatorFactory(store) - svc := NewService(ServiceConfig{ - Store: store, - Cache: c, - Publisher: pub, - Subscriber: sub, - Logger: testLogger, - Validators: vf, - }) + svc := NewService(store, c, pub, sub, + WithLogger(testLogger), + WithValidators(vf), + ) return svc, store } @@ -552,12 +544,10 @@ func TestGetField_RecordsUsage(t *testing.T) { audit.WithFlushInterval(time.Hour), audit.WithLogger(testLogger), ) - svc := NewService(ServiceConfig{ - Store: store, - Cache: c, - Logger: testLogger, - Recorder: recorder, - }) + svc := NewService(store, c, nil, nil, + WithLogger(testLogger), + WithRecorder(recorder), + ) ctx := context.Background() store.On("GetLatestConfigVersion", ctx, tenantID1). @@ -587,12 +577,10 @@ func TestGetConfig_RecordsUsage(t *testing.T) { audit.WithFlushInterval(time.Hour), audit.WithLogger(testLogger), ) - svc := NewService(ServiceConfig{ - Store: store, - Cache: c, - Logger: testLogger, - Recorder: recorder, - }) + svc := NewService(store, c, nil, nil, + WithLogger(testLogger), + WithRecorder(recorder), + ) ctx := context.Background() store.On("GetLatestConfigVersion", ctx, tenantID1). @@ -630,12 +618,10 @@ func TestGetFields_RecordsUsage(t *testing.T) { audit.WithFlushInterval(time.Hour), audit.WithLogger(testLogger), ) - svc := NewService(ServiceConfig{ - Store: store, - Cache: c, - Logger: testLogger, - Recorder: recorder, - }) + svc := NewService(store, c, nil, nil, + WithLogger(testLogger), + WithRecorder(recorder), + ) ctx := context.Background() store.On("GetLatestConfigVersion", ctx, tenantID1). @@ -669,12 +655,10 @@ func TestGetConfig_CacheHit_RecordsUsage(t *testing.T) { audit.WithFlushInterval(time.Hour), audit.WithLogger(testLogger), ) - svc := NewService(ServiceConfig{ - Store: store, - Cache: c, - Logger: testLogger, - Recorder: recorder, - }) + svc := NewService(store, c, nil, nil, + WithLogger(testLogger), + WithRecorder(recorder), + ) ctx := context.Background() store.On("GetLatestConfigVersion", ctx, tenantID1). diff --git a/internal/server/memory_integration_test.go b/internal/server/memory_integration_test.go index 3d9035ad..04d7d781 100644 --- a/internal/server/memory_integration_test.go +++ b/internal/server/memory_integration_test.go @@ -54,16 +54,12 @@ func TestMemoryBackend_Integration(t *testing.T) { schemaSvc := schema.NewService(memSchema, slog.Default(), telemetry.NewSchemaMetrics(telemetry.Config{}), validatorFactory) pb.RegisterSchemaServiceServer(srv.GRPCServer(), schemaSvc) - configSvc := config.NewService(config.ServiceConfig{ - Store: memConfig, - Cache: cache.NewMemoryCache(0), - Publisher: memPubSub, - Subscriber: memPubSub, - Logger: slog.Default(), - CacheMetrics: telemetry.NewCacheMetrics(telemetry.Config{}), - Metrics: telemetry.NewConfigMetrics(telemetry.Config{}), - Validators: validatorFactory, - }) + configSvc := config.NewService(memConfig, cache.NewMemoryCache(0), memPubSub, memPubSub, + config.WithLogger(slog.Default()), + config.WithCacheMetrics(telemetry.NewCacheMetrics(telemetry.Config{})), + config.WithMetrics(telemetry.NewConfigMetrics(telemetry.Config{})), + config.WithValidators(validatorFactory), + ) pb.RegisterConfigServiceServer(srv.GRPCServer(), configSvc) auditSvc := audit.NewService(audit.NewMemoryStore(), slog.Default(), nil) diff --git a/sdk/adminclient/client.go b/sdk/adminclient/client.go index 384f2f6d..f9d99c81 100644 --- a/sdk/adminclient/client.go +++ b/sdk/adminclient/client.go @@ -16,9 +16,10 @@ type Client struct { server ServerTransport } -// New creates a new admin client using the given transport implementations. -// Any of the transports may be nil if that service is not needed; -// methods for a nil service will return [ErrServiceNotConfigured]. +// New creates a new admin client. Pass [WithSchemaTransport], +// [WithConfigTransport], [WithAuditTransport], and [WithServerTransport] for +// the services you need. Methods on services without a wired transport return +// [ErrServiceNotConfigured]. // // Example (with grpctransport): // @@ -26,7 +27,16 @@ type Client struct { // // Example (with explicit transports): // -// client := adminclient.New(schemaTransport, configTransport, auditTransport, serverTransport) -func New(schema SchemaTransport, config ConfigTransport, audit AuditTransport, server ServerTransport) *Client { - return &Client{schema: schema, config: config, audit: audit, server: server} +// client := adminclient.New( +// adminclient.WithSchemaTransport(schemaTransport), +// adminclient.WithConfigTransport(configTransport), +// adminclient.WithAuditTransport(auditTransport), +// adminclient.WithServerTransport(serverTransport), +// ) +func New(opts ...Option) *Client { + o := clientOptions{} + for _, opt := range opts { + opt(&o) + } + return &Client{schema: o.schema, config: o.config, audit: o.audit, server: o.server} } diff --git a/sdk/adminclient/client_test.go b/sdk/adminclient/client_test.go index 624e2609..0c8fc3d1 100644 --- a/sdk/adminclient/client_test.go +++ b/sdk/adminclient/client_test.go @@ -164,14 +164,14 @@ func TestNew(t *testing.T) { ms := &mockSchemaTransport{} mc := &mockConfigTransport{} ma := &mockAuditTransport{} - client := New(ms, mc, ma, nil) + client := New(WithSchemaTransport(ms), WithConfigTransport(mc), WithAuditTransport(ma)) if client == nil { t.Fatal("expected non-nil client") } } func TestNew_NilTransports(t *testing.T) { - client := New(nil, nil, nil, nil) + client := New() if client == nil { t.Fatal("expected non-nil client even with nil transports") } @@ -179,7 +179,7 @@ func TestNew_NilTransports(t *testing.T) { func TestCreateSchema_Success(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ctx := context.Background() ms.createSchemaFn = func(_ context.Context, req *CreateSchemaRequest) (*Schema, error) { @@ -203,7 +203,7 @@ func TestCreateSchema_Success(t *testing.T) { func TestGetSchema_NotFound(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ctx := context.Background() ms.getSchemaFn = func(_ context.Context, _ string, _ *int32) (*Schema, error) { @@ -218,7 +218,7 @@ func TestGetSchema_NotFound(t *testing.T) { func TestCreateTenant_Success(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ctx := context.Background() ms.createTenantFn = func(_ context.Context, req *CreateTenantRequest) (*Tenant, error) { @@ -236,7 +236,7 @@ func TestCreateTenant_Success(t *testing.T) { func TestCreateTenant_UnpublishedSchema(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ctx := context.Background() ms.createTenantFn = func(_ context.Context, _ *CreateTenantRequest) (*Tenant, error) { @@ -251,7 +251,7 @@ func TestCreateTenant_UnpublishedSchema(t *testing.T) { func TestLockUnlockField(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ctx := context.Background() ms.lockFieldFn = func(_ context.Context, _, _ string, _ []string) error { return nil } @@ -267,7 +267,7 @@ func TestLockUnlockField(t *testing.T) { func TestListConfigVersions_AutoPaginate(t *testing.T) { mc := &mockConfigTransport{} - client := New(nil, mc, nil, nil) + client := New(WithConfigTransport(mc)) ctx := context.Background() mc.listVersionsFn = func(_ context.Context, _ string, _ int32, pageToken string) (*ListVersionsResponse, error) { @@ -293,7 +293,7 @@ func TestListConfigVersions_AutoPaginate(t *testing.T) { func TestRollbackConfig_Success(t *testing.T) { mc := &mockConfigTransport{} - client := New(nil, mc, nil, nil) + client := New(WithConfigTransport(mc)) ctx := context.Background() mc.rollbackFn = func(_ context.Context, _ string, _ int32, _ string) (*Version, error) { @@ -311,7 +311,7 @@ func TestRollbackConfig_Success(t *testing.T) { func TestExportImportConfig(t *testing.T) { mc := &mockConfigTransport{} - client := New(nil, mc, nil, nil) + client := New(WithConfigTransport(mc)) ctx := context.Background() mc.exportConfigFn = func(_ context.Context, _ string, _ *int32) ([]byte, error) { @@ -341,7 +341,7 @@ func TestExportImportConfig(t *testing.T) { func TestQueryWriteLog_Success(t *testing.T) { ma := &mockAuditTransport{} - client := New(nil, nil, ma, nil) + client := New(WithAuditTransport(ma)) ctx := context.Background() ma.queryWriteLogFn = func(_ context.Context, _ *QueryWriteLogRequest) (*QueryWriteLogResponse, error) { @@ -374,7 +374,7 @@ func TestGetServerInfo_Success(t *testing.T) { }, nil }, } - client := New(nil, nil, nil, ms) + client := New(WithServerTransport(ms)) info, err := client.GetServerInfo(context.Background()) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -394,7 +394,7 @@ func TestGetServerInfo_Success(t *testing.T) { } func TestServiceNotConfigured(t *testing.T) { - client := New(nil, nil, nil, nil) + client := New() ctx := context.Background() _, err := client.GetSchema(ctx, "s1") @@ -420,7 +420,7 @@ func TestServiceNotConfigured(t *testing.T) { func TestGetLatestPublishedSchemaVersion_LatestIsPublished(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.listSchemasFn = func(_ context.Context, _ int32, _ string) (*ListSchemasResponse, error) { return &ListSchemasResponse{ @@ -442,7 +442,7 @@ func TestGetLatestPublishedSchemaVersion_LatestIsPublished(t *testing.T) { func TestGetLatestPublishedSchemaVersion_WalksBackFromDraft(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.listSchemasFn = func(_ context.Context, _ int32, _ string) (*ListSchemasResponse, error) { return &ListSchemasResponse{ @@ -474,7 +474,7 @@ func TestGetLatestPublishedSchemaVersion_WalksBackFromDraft(t *testing.T) { func TestGetLatestPublishedSchemaVersion_NoPublishedVersion(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.listSchemasFn = func(_ context.Context, _ int32, _ string) (*ListSchemasResponse, error) { return &ListSchemasResponse{ @@ -493,7 +493,7 @@ func TestGetLatestPublishedSchemaVersion_NoPublishedVersion(t *testing.T) { func TestGetLatestPublishedSchemaVersion_SchemaNameNotFound(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.listSchemasFn = func(_ context.Context, _ int32, _ string) (*ListSchemasResponse, error) { return &ListSchemasResponse{Schemas: []*Schema{{ID: "s_other", Name: "other", Published: true}}}, nil @@ -507,7 +507,7 @@ func TestGetLatestPublishedSchemaVersion_SchemaNameNotFound(t *testing.T) { func TestGetLatestPublishedSchemaVersion_ListError(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.listSchemasFn = func(_ context.Context, _ int32, _ string) (*ListSchemasResponse, error) { return nil, errors.New("rpc failed") @@ -521,7 +521,7 @@ func TestGetLatestPublishedSchemaVersion_ListError(t *testing.T) { func TestGetLatestPublishedSchemaVersion_GetSchemaVersionError(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.listSchemasFn = func(_ context.Context, _ int32, _ string) (*ListSchemasResponse, error) { return &ListSchemasResponse{Schemas: []*Schema{{ID: "s1", Name: "payments", Version: 3, Published: false}}}, nil diff --git a/sdk/adminclient/operations_test.go b/sdk/adminclient/operations_test.go index 12e5d7dd..fd90bf82 100644 --- a/sdk/adminclient/operations_test.go +++ b/sdk/adminclient/operations_test.go @@ -13,7 +13,7 @@ import ( func TestGetSchemaVersion_Success(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.getSchemaFn = func(_ context.Context, id string, version *int32) (*Schema, error) { if id != "s1" || version == nil || *version != int32(2) { @@ -33,7 +33,7 @@ func TestGetSchemaVersion_Success(t *testing.T) { func TestListSchemas_AutoPaginate(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.listSchemasFn = func(_ context.Context, _ int32, pageToken string) (*ListSchemasResponse, error) { if pageToken == "" { @@ -58,7 +58,7 @@ func TestListSchemas_AutoPaginate(t *testing.T) { func TestUpdateSchema_Success(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.updateSchemaFn = func(_ context.Context, req *UpdateSchemaRequest) (*Schema, error) { if req.ID != "s1" { @@ -84,7 +84,7 @@ func TestUpdateSchema_Success(t *testing.T) { func TestDeleteSchema_Success(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.deleteSchemaFn = func(_ context.Context, id string) error { if id != "s1" { @@ -99,7 +99,7 @@ func TestDeleteSchema_Success(t *testing.T) { func TestExportSchema_Success(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.exportSchemaFn = func(_ context.Context, _ string, _ *int32) ([]byte, error) { return []byte("spec_version: v1"), nil @@ -116,7 +116,7 @@ func TestExportSchema_Success(t *testing.T) { func TestExportSchema_SpecificVersion(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.exportSchemaFn = func(_ context.Context, id string, version *int32) ([]byte, error) { if id != "s1" || version == nil || *version != int32(2) { @@ -134,7 +134,7 @@ func TestExportSchema_SpecificVersion(t *testing.T) { func TestImportSchema_Success(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.importSchemaFn = func(_ context.Context, yamlContent []byte, autoPublish bool) (*Schema, error) { if autoPublish { @@ -154,7 +154,7 @@ func TestImportSchema_Success(t *testing.T) { func TestImportSchema_AutoPublish(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.importSchemaFn = func(_ context.Context, _ []byte, autoPublish bool) (*Schema, error) { if !autoPublish { @@ -174,7 +174,7 @@ func TestImportSchema_AutoPublish(t *testing.T) { func TestPublishSchema_AlreadyPublished(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.publishSchemaFn = func(_ context.Context, _ string, _ int32) (*Schema, error) { return nil, ErrFailedPrecondition @@ -188,7 +188,7 @@ func TestPublishSchema_AlreadyPublished(t *testing.T) { func TestPublishSchema_Success(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.publishSchemaFn = func(_ context.Context, id string, version int32) (*Schema, error) { if id != "s1" || version != int32(1) { @@ -210,7 +210,7 @@ func TestPublishSchema_Success(t *testing.T) { func TestGetTenant_Success(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.getTenantFn = func(_ context.Context, id string) (*Tenant, error) { if id != "t1" { @@ -230,7 +230,7 @@ func TestGetTenant_Success(t *testing.T) { func TestGetTenant_NotFound(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.getTenantFn = func(_ context.Context, _ string) (*Tenant, error) { return nil, ErrNotFound @@ -244,7 +244,7 @@ func TestGetTenant_NotFound(t *testing.T) { func TestListTenants_WithSchemaFilter(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.listTenantsFn = func(_ context.Context, schemaID *string, _ int32, _ string) (*ListTenantsResponse, error) { if schemaID == nil || *schemaID != "s1" { @@ -268,7 +268,7 @@ func TestListTenants_WithSchemaFilter(t *testing.T) { func TestListTenants_NoFilter(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.listTenantsFn = func(_ context.Context, schemaID *string, _ int32, _ string) (*ListTenantsResponse, error) { if schemaID != nil { @@ -285,7 +285,7 @@ func TestListTenants_NoFilter(t *testing.T) { func TestListTenants_AutoPaginate(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.listTenantsFn = func(_ context.Context, _ *string, _ int32, pageToken string) (*ListTenantsResponse, error) { if pageToken == "" { @@ -310,7 +310,7 @@ func TestListTenants_AutoPaginate(t *testing.T) { func TestUpdateTenantName(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.updateTenantFn = func(_ context.Context, req *UpdateTenantRequest) (*Tenant, error) { if req.Name == nil || *req.Name != "new-name" { @@ -333,7 +333,7 @@ func TestUpdateTenantName(t *testing.T) { func TestUpdateTenantSchema(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.updateTenantFn = func(_ context.Context, req *UpdateTenantRequest) (*Tenant, error) { if req.SchemaVersion == nil || *req.SchemaVersion != int32(2) { @@ -356,7 +356,7 @@ func TestUpdateTenantSchema(t *testing.T) { func TestUpdateTenant_BothFields(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.updateTenantFn = func(_ context.Context, req *UpdateTenantRequest) (*Tenant, error) { if req.Name == nil || *req.Name != "renamed" { @@ -381,7 +381,7 @@ func TestUpdateTenant_BothFields(t *testing.T) { func TestDeleteTenant_Success(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.deleteTenantFn = func(_ context.Context, id string) error { if id != "t1" { @@ -398,7 +398,7 @@ func TestDeleteTenant_Success(t *testing.T) { func TestListFieldLocks_Success(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.listFieldLocksFn = func(_ context.Context, tenantID string) ([]FieldLock, error) { if tenantID != "t1" { @@ -426,7 +426,7 @@ func TestListFieldLocks_Success(t *testing.T) { func TestLockField_WithValues(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.lockFieldFn = func(_ context.Context, tenantID, fieldPath string, lockedValues []string) error { if tenantID != "t1" || fieldPath != "app.env" { @@ -445,7 +445,7 @@ func TestLockField_WithValues(t *testing.T) { func TestLockField_NoValues(t *testing.T) { ms := &mockSchemaTransport{} - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) ms.lockFieldFn = func(_ context.Context, _, _ string, lockedValues []string) error { if len(lockedValues) != 0 { @@ -463,7 +463,7 @@ func TestLockField_NoValues(t *testing.T) { func TestGetFieldUsage_Success(t *testing.T) { ma := &mockAuditTransport{} - client := New(nil, nil, ma, nil) + client := New(WithAuditTransport(ma)) ma.getFieldUsageFn = func(_ context.Context, tenantID, fieldPath string, start, end *time.Time) (*UsageStats, error) { if tenantID != "t1" || fieldPath != "app.fee" { @@ -486,7 +486,7 @@ func TestGetFieldUsage_Success(t *testing.T) { func TestGetFieldUsage_WithTimeRange(t *testing.T) { ma := &mockAuditTransport{} - client := New(nil, nil, ma, nil) + client := New(WithAuditTransport(ma)) start := time.Now().Add(-time.Hour) end := time.Now() @@ -506,7 +506,7 @@ func TestGetFieldUsage_WithTimeRange(t *testing.T) { func TestGetTenantUsage_Success(t *testing.T) { ma := &mockAuditTransport{} - client := New(nil, nil, ma, nil) + client := New(WithAuditTransport(ma)) ma.getTenantUsageFn = func(_ context.Context, tenantID string, _, _ *time.Time) ([]*UsageStats, error) { if tenantID != "t1" { @@ -529,7 +529,7 @@ func TestGetTenantUsage_Success(t *testing.T) { func TestGetUnusedFields_Success(t *testing.T) { ma := &mockAuditTransport{} - client := New(nil, nil, ma, nil) + client := New(WithAuditTransport(ma)) ma.getUnusedFieldsFn = func(_ context.Context, tenantID string, _ time.Time) ([]string, error) { if tenantID != "t1" { @@ -580,7 +580,7 @@ func TestAuditFilters(t *testing.T) { func TestQueryWriteLog_AutoPaginate(t *testing.T) { ma := &mockAuditTransport{} - client := New(nil, nil, ma, nil) + client := New(WithAuditTransport(ma)) ma.queryWriteLogFn = func(_ context.Context, req *QueryWriteLogRequest) (*QueryWriteLogResponse, error) { if req.PageToken == "" { @@ -607,7 +607,7 @@ func TestQueryWriteLog_AutoPaginate(t *testing.T) { func TestGetConfigVersion_Success(t *testing.T) { mc := &mockConfigTransport{} - client := New(nil, mc, nil, nil) + client := New(WithConfigTransport(mc)) mc.getVersionFn = func(_ context.Context, tenantID string, version int32) (*Version, error) { if tenantID != "t1" || version != int32(3) { @@ -630,7 +630,7 @@ func TestGetConfigVersion_Success(t *testing.T) { func TestImportConfig_WithMode(t *testing.T) { mc := &mockConfigTransport{} - client := New(nil, mc, nil, nil) + client := New(WithConfigTransport(mc)) mc.importConfigFn = func(_ context.Context, req *ImportConfigRequest) (*Version, error) { if req.TenantID != "t1" { @@ -656,7 +656,7 @@ func TestImportConfig_WithMode(t *testing.T) { func TestImportConfig_DefaultMode(t *testing.T) { mc := &mockConfigTransport{} - client := New(nil, mc, nil, nil) + client := New(WithConfigTransport(mc)) mc.importConfigFn = func(_ context.Context, req *ImportConfigRequest) (*Version, error) { if req.Mode != ImportModeMerge { @@ -680,7 +680,7 @@ func TestTransportError_Propagated(t *testing.T) { ms.getSchemaFn = func(_ context.Context, _ string, _ *int32) (*Schema, error) { return nil, sentinel } - client := New(ms, nil, nil, nil) + client := New(WithSchemaTransport(ms)) _, err := client.GetSchema(context.Background(), "s1") if !errors.Is(err, sentinel) { @@ -691,7 +691,7 @@ func TestTransportError_Propagated(t *testing.T) { // --- Service not configured (all methods) --- func TestServiceNotConfigured_AllMethods(t *testing.T) { - client := New(nil, nil, nil, nil) + client := New() ctx := context.Background() _, err := client.GetSchemaVersion(ctx, "s1", 1) diff --git a/sdk/adminclient/options.go b/sdk/adminclient/options.go new file mode 100644 index 00000000..0045d110 --- /dev/null +++ b/sdk/adminclient/options.go @@ -0,0 +1,35 @@ +package adminclient + +// Option configures a Client. +type Option func(*clientOptions) + +type clientOptions struct { + schema SchemaTransport + config ConfigTransport + audit AuditTransport + server ServerTransport +} + +// WithSchemaTransport wires the schema transport. Methods that need it return +// [ErrServiceNotConfigured] when unset. +func WithSchemaTransport(t SchemaTransport) Option { + return func(o *clientOptions) { o.schema = t } +} + +// WithConfigTransport wires the config transport. Methods that need it return +// [ErrServiceNotConfigured] when unset. +func WithConfigTransport(t ConfigTransport) Option { + return func(o *clientOptions) { o.config = t } +} + +// WithAuditTransport wires the audit transport. Methods that need it return +// [ErrServiceNotConfigured] when unset. +func WithAuditTransport(t AuditTransport) Option { + return func(o *clientOptions) { o.audit = t } +} + +// WithServerTransport wires the server-info transport. Methods that need it +// return [ErrServiceNotConfigured] when unset. +func WithServerTransport(t ServerTransport) Option { + return func(o *clientOptions) { o.server = t } +} diff --git a/sdk/grpctransport/convenience.go b/sdk/grpctransport/convenience.go index d4fa5261..64ca8672 100644 --- a/sdk/grpctransport/convenience.go +++ b/sdk/grpctransport/convenience.go @@ -34,10 +34,10 @@ func NewConfigClient(conn grpc.ClientConnInterface, opts ...Option) *configclien func NewAdminClient(conn grpc.ClientConnInterface, opts ...Option) *adminclient.Client { cfg := buildConfig(opts) return adminclient.New( - &SchemaTransport{rpc: pb.NewSchemaServiceClient(conn), auth: cfg.auth}, - &AdminConfigTransport{rpc: pb.NewConfigServiceClient(conn), auth: cfg.auth}, - &AuditTransport{rpc: pb.NewAuditServiceClient(conn), auth: cfg.auth}, - &ServerTransport{rpc: pb.NewServerServiceClient(conn)}, + adminclient.WithSchemaTransport(&SchemaTransport{rpc: pb.NewSchemaServiceClient(conn), auth: cfg.auth}), + adminclient.WithConfigTransport(&AdminConfigTransport{rpc: pb.NewConfigServiceClient(conn), auth: cfg.auth}), + adminclient.WithAuditTransport(&AuditTransport{rpc: pb.NewAuditServiceClient(conn), auth: cfg.auth}), + adminclient.WithServerTransport(&ServerTransport{rpc: pb.NewServerServiceClient(conn)}), ) }