diff --git a/coverage_test.go b/coverage_test.go index 8e33943..8c6adfe 100644 --- a/coverage_test.go +++ b/coverage_test.go @@ -290,7 +290,7 @@ func TestCoverage_ScopedStore_Bad_GroupsClosedStore(t *testing.T) { var err error _, err = scopedStore.Groups("") require.Error(t, err) - assert.Contains(t, err.Error(), "store.Groups") + assert.Contains(t, err.Error(), "store.ScopedStore.Groups") } func TestCoverage_ScopedStore_Bad_GroupsSeqRowsError(t *testing.T) { diff --git a/scope.go b/scope.go index 7cafbfa..7a40721 100644 --- a/scope.go +++ b/scope.go @@ -49,6 +49,25 @@ type ScopedStoreConfig struct { Quota QuotaConfig } +// Usage example: `if err := (store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}).Validate(); err != nil { return }` +func (config ScopedStoreConfig) Validate() error { + if !validNamespace.MatchString(config.Namespace) { + return core.E( + "store.ScopedStoreConfig.Validate", + core.Sprintf("namespace %q is invalid; use names like %q or %q", config.Namespace, "tenant-a", "tenant-42"), + nil, + ) + } + if config.Quota.MaxKeys < 0 || config.Quota.MaxGroups < 0 { + return core.E( + "store.ScopedStoreConfig.Validate", + core.Sprintf("quota values must be zero or positive; got MaxKeys=%d MaxGroups=%d", config.Quota.MaxKeys, config.Quota.MaxGroups), + nil, + ) + } + return nil +} + type scopedWatcherBinding struct { backingStore *Store underlyingEvents <-chan Event @@ -84,20 +103,13 @@ func NewScoped(storeInstance *Store, namespace string) *ScopedStore { // Usage example: `scopedStore, err := store.NewScopedConfigured(storeInstance, store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}); if err != nil { return }` func NewScopedConfigured(storeInstance *Store, config ScopedStoreConfig) (*ScopedStore, error) { + if storeInstance == nil { + return nil, core.E("store.NewScopedConfigured", "store instance is nil", nil) + } + if err := config.Validate(); err != nil { + return nil, core.E("store.NewScopedConfigured", "validate config", err) + } scopedStore := NewScoped(storeInstance, config.Namespace) - if scopedStore == nil { - if storeInstance == nil { - return nil, core.E("store.NewScopedConfigured", "store instance is nil", nil) - } - return nil, core.E("store.NewScopedConfigured", core.Sprintf("namespace %q is invalid; use names like %q or %q", config.Namespace, "tenant-a", "tenant-42"), nil) - } - if config.Quota.MaxKeys < 0 || config.Quota.MaxGroups < 0 { - return nil, core.E( - "store.NewScopedConfigured", - core.Sprintf("quota values must be zero or positive; got MaxKeys=%d MaxGroups=%d", config.Quota.MaxKeys, config.Quota.MaxGroups), - nil, - ) - } scopedStore.MaxKeys = config.Quota.MaxKeys scopedStore.MaxGroups = config.Quota.MaxGroups return scopedStore, nil @@ -267,7 +279,7 @@ func (scopedStore *ScopedStore) CountAll(groupPrefix ...string) (int, error) { // Usage example: `groupNames, err := scopedStore.Groups("config")` // Usage example: `groupNames, err := scopedStore.Groups()` func (scopedStore *ScopedStore) Groups(groupPrefix ...string) ([]string, error) { - backingStore, err := scopedStore.resolvedStore("store.Groups") + backingStore, err := scopedStore.resolvedStore("store.ScopedStore.Groups") if err != nil { return nil, err } diff --git a/scope_test.go b/scope_test.go index 1dcd642..1b72c88 100644 --- a/scope_test.go +++ b/scope_test.go @@ -126,6 +126,29 @@ func TestScope_NewScopedConfigured_Bad_InvalidNamespace(t *testing.T) { assert.Contains(t, err.Error(), "namespace") } +func TestScope_ScopedStoreConfig_Good_Validate(t *testing.T) { + err := (ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: 4, MaxGroups: 2}, + }).Validate() + require.NoError(t, err) +} + +func TestScope_ScopedStoreConfig_Bad_InvalidNamespace(t *testing.T) { + err := (ScopedStoreConfig{Namespace: "tenant_a"}).Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "namespace") +} + +func TestScope_ScopedStoreConfig_Bad_NegativeQuota(t *testing.T) { + err := (ScopedStoreConfig{ + Namespace: "tenant-a", + Quota: QuotaConfig{MaxKeys: -1}, + }).Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "quota values must be zero or positive") +} + // --------------------------------------------------------------------------- // ScopedStore — basic CRUD // --------------------------------------------------------------------------- diff --git a/store.go b/store.go index d796704..fba77ce 100644 --- a/store.go +++ b/store.go @@ -43,6 +43,21 @@ type StoreConfig struct { PurgeInterval time.Duration } +// Usage example: `if err := (store.StoreConfig{DatabasePath: ":memory:", PurgeInterval: 30 * time.Second}).Validate(); err != nil { return }` +func (config StoreConfig) Validate() error { + if config.Journal != (JournalConfiguration{}) && !config.Journal.isConfigured() { + return core.E( + "store.StoreConfig.Validate", + "journal configuration must include endpoint URL, organisation, and bucket name", + nil, + ) + } + if config.PurgeInterval < 0 { + return core.E("store.StoreConfig.Validate", "purge interval must be zero or positive", nil) + } + return nil +} + // Usage example: `config := storeInstance.JournalConfiguration(); fmt.Println(config.EndpointURL, config.Organisation, config.BucketName)` // The values are copied into the store and used as journal metadata. type JournalConfiguration struct { @@ -199,15 +214,16 @@ func NewConfigured(config StoreConfig) (*Store, error) { } func openConfiguredStore(operation string, config StoreConfig) (*Store, error) { + if err := config.Validate(); err != nil { + return nil, core.E(operation, "validate config", err) + } + storeInstance, err := openSQLiteStore(operation, config.DatabasePath) if err != nil { return nil, err } if config.Journal != (JournalConfiguration{}) { - if !config.Journal.isConfigured() { - return nil, core.E(operation, "journal configuration must include endpoint URL, organisation, and bucket name", nil) - } storeInstance.journalConfiguration = journalConfiguration{ endpointURL: config.Journal.EndpointURL, organisation: config.Journal.Organisation, diff --git a/store_test.go b/store_test.go index eeffe5b..e220a09 100644 --- a/store_test.go +++ b/store_test.go @@ -148,6 +148,38 @@ func TestStore_NewConfigured_Bad_PartialJournalConfiguration(t *testing.T) { assert.Contains(t, err.Error(), "journal configuration must include endpoint URL, organisation, and bucket name") } +func TestStore_StoreConfig_Good_Validate(t *testing.T) { + err := (StoreConfig{ + DatabasePath: ":memory:", + Journal: JournalConfiguration{ + EndpointURL: "http://127.0.0.1:8086", + Organisation: "core", + BucketName: "events", + }, + PurgeInterval: 20 * time.Millisecond, + }).Validate() + require.NoError(t, err) +} + +func TestStore_StoreConfig_Bad_NegativePurgeInterval(t *testing.T) { + err := (StoreConfig{ + DatabasePath: ":memory:", + PurgeInterval: -time.Second, + }).Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), "purge interval must be zero or positive") +} + +func TestStore_NewConfigured_Bad_NegativePurgeInterval(t *testing.T) { + _, err := NewConfigured(StoreConfig{ + DatabasePath: ":memory:", + PurgeInterval: -time.Second, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "validate config") + assert.Contains(t, err.Error(), "purge interval must be zero or positive") +} + func TestStore_Config_Good(t *testing.T) { storeInstance, err := NewConfigured(StoreConfig{ DatabasePath: ":memory:",