refactor: add AX config validation helpers
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
32e7413bf4
commit
cae3c32d51
6 changed files with 150 additions and 26 deletions
44
compact.go
44
compact.go
|
|
@ -26,6 +26,31 @@ type CompactOptions struct {
|
|||
Format string
|
||||
}
|
||||
|
||||
// Usage example: `normalisedOptions := (store.CompactOptions{Before: time.Now().Add(-30 * 24 * time.Hour)}).Normalised()`
|
||||
func (compactOptions CompactOptions) Normalised() CompactOptions {
|
||||
if compactOptions.Output == "" {
|
||||
compactOptions.Output = defaultArchiveOutputDirectory
|
||||
}
|
||||
if compactOptions.Format == "" {
|
||||
compactOptions.Format = "gzip"
|
||||
}
|
||||
return compactOptions
|
||||
}
|
||||
|
||||
// Usage example: `if err := (store.CompactOptions{Before: time.Now().Add(-30 * 24 * time.Hour), Format: "gzip"}).Validate(); err != nil { return }`
|
||||
func (compactOptions CompactOptions) Validate() error {
|
||||
switch compactOptions.Format {
|
||||
case "", "gzip", "zstd":
|
||||
return nil
|
||||
default:
|
||||
return core.E(
|
||||
"store.CompactOptions.Validate",
|
||||
core.Concat(`format must be "gzip" or "zstd"; got `, compactOptions.Format),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type compactArchiveEntry struct {
|
||||
journalEntryID int64
|
||||
journalBucketName string
|
||||
|
|
@ -44,20 +69,13 @@ func (storeInstance *Store) Compact(options CompactOptions) core.Result {
|
|||
return core.Result{Value: core.E("store.Compact", "ensure journal schema", err), OK: false}
|
||||
}
|
||||
|
||||
outputDirectory := options.Output
|
||||
if outputDirectory == "" {
|
||||
outputDirectory = defaultArchiveOutputDirectory
|
||||
}
|
||||
format := options.Format
|
||||
if format == "" {
|
||||
format = "gzip"
|
||||
}
|
||||
if format != "gzip" && format != "zstd" {
|
||||
return core.Result{Value: core.E("store.Compact", core.Concat("unsupported archive format: ", format), nil), OK: false}
|
||||
options = options.Normalised()
|
||||
if err := options.Validate(); err != nil {
|
||||
return core.Result{Value: core.E("store.Compact", "validate options", err), OK: false}
|
||||
}
|
||||
|
||||
filesystem := (&core.Fs{}).NewUnrestricted()
|
||||
if result := filesystem.EnsureDir(outputDirectory); !result.OK {
|
||||
if result := filesystem.EnsureDir(options.Output); !result.OK {
|
||||
return core.Result{Value: core.E("store.Compact", "ensure archive directory", result.Value.(error)), OK: false}
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +110,7 @@ func (storeInstance *Store) Compact(options CompactOptions) core.Result {
|
|||
return core.Result{Value: "", OK: true}
|
||||
}
|
||||
|
||||
outputPath := compactOutputPath(outputDirectory, format)
|
||||
outputPath := compactOutputPath(options.Output, options.Format)
|
||||
archiveFileResult := filesystem.Create(outputPath)
|
||||
if !archiveFileResult.OK {
|
||||
return core.Result{Value: core.E("store.Compact", "create archive file", archiveFileResult.Value.(error)), OK: false}
|
||||
|
|
@ -109,7 +127,7 @@ func (storeInstance *Store) Compact(options CompactOptions) core.Result {
|
|||
}
|
||||
}()
|
||||
|
||||
writer, err := archiveWriter(file, format)
|
||||
writer, err := archiveWriter(file, options.Format)
|
||||
if err != nil {
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,3 +183,29 @@ func TestCompact_Compact_Good_DeterministicOrderingForSameTimestamp(t *testing.T
|
|||
require.True(t, unmarshalResult.OK, "archive line unmarshal failed: %v", unmarshalResult.Value)
|
||||
assert.Equal(t, "session-a", secondArchivedRow["measurement"])
|
||||
}
|
||||
|
||||
func TestCompact_CompactOptions_Good_Normalised(t *testing.T) {
|
||||
options := (CompactOptions{
|
||||
Before: time.Now().Add(-24 * time.Hour),
|
||||
}).Normalised()
|
||||
|
||||
assert.Equal(t, defaultArchiveOutputDirectory, options.Output)
|
||||
assert.Equal(t, "gzip", options.Format)
|
||||
}
|
||||
|
||||
func TestCompact_CompactOptions_Good_Validate(t *testing.T) {
|
||||
err := (CompactOptions{
|
||||
Before: time.Now().Add(-24 * time.Hour),
|
||||
Format: "zstd",
|
||||
}).Validate()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCompact_CompactOptions_Bad_ValidateUnsupportedFormat(t *testing.T) {
|
||||
err := (CompactOptions{
|
||||
Before: time.Now().Add(-24 * time.Hour),
|
||||
Format: "zip",
|
||||
}).Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), `format must be "gzip" or "zstd"`)
|
||||
}
|
||||
|
|
|
|||
20
scope.go
20
scope.go
|
|
@ -22,6 +22,18 @@ type QuotaConfig struct {
|
|||
MaxGroups int
|
||||
}
|
||||
|
||||
// Usage example: `if err := (store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}).Validate(); err != nil { return }`
|
||||
func (quotaConfig QuotaConfig) Validate() error {
|
||||
if quotaConfig.MaxKeys < 0 || quotaConfig.MaxGroups < 0 {
|
||||
return core.E(
|
||||
"store.QuotaConfig.Validate",
|
||||
core.Sprintf("quota values must be zero or positive; got MaxKeys=%d MaxGroups=%d", quotaConfig.MaxKeys, quotaConfig.MaxGroups),
|
||||
nil,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Usage example: `scopedStore, err := store.NewScopedConfigured(storeInstance, store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}); if err != nil { return }; _ = scopedStore.Set("colour", "blue")`
|
||||
// ScopedStore keeps one namespace isolated behind helpers such as Set and
|
||||
// GetFrom so callers do not repeat the `tenant-a:` prefix manually.
|
||||
|
|
@ -61,12 +73,8 @@ func (scopedConfig ScopedStoreConfig) Validate() error {
|
|||
nil,
|
||||
)
|
||||
}
|
||||
if scopedConfig.Quota.MaxKeys < 0 || scopedConfig.Quota.MaxGroups < 0 {
|
||||
return core.E(
|
||||
"store.ScopedStoreConfig.Validate",
|
||||
core.Sprintf("quota values must be zero or positive; got MaxKeys=%d MaxGroups=%d", scopedConfig.Quota.MaxKeys, scopedConfig.Quota.MaxGroups),
|
||||
nil,
|
||||
)
|
||||
if err := scopedConfig.Quota.Validate(); err != nil {
|
||||
return core.E("store.ScopedStoreConfig.Validate", "quota", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,17 @@ func TestScope_NewScopedConfigured_Bad_InvalidNamespace(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "namespace")
|
||||
}
|
||||
|
||||
func TestScope_QuotaConfig_Good_Validate(t *testing.T) {
|
||||
err := (QuotaConfig{MaxKeys: 4, MaxGroups: 2}).Validate()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestScope_QuotaConfig_Bad_ValidateNegativeValue(t *testing.T) {
|
||||
err := (QuotaConfig{MaxKeys: -1, MaxGroups: 2}).Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "quota values must be zero or positive")
|
||||
}
|
||||
|
||||
func TestScope_ScopedStoreConfig_Good_Validate(t *testing.T) {
|
||||
err := (ScopedStoreConfig{
|
||||
Namespace: "tenant-a",
|
||||
|
|
|
|||
36
store.go
36
store.go
|
|
@ -55,12 +55,10 @@ func (storeConfig StoreConfig) Validate() error {
|
|||
nil,
|
||||
)
|
||||
}
|
||||
if storeConfig.Journal != (JournalConfiguration{}) && !storeConfig.Journal.isConfigured() {
|
||||
return core.E(
|
||||
"store.StoreConfig.Validate",
|
||||
"journal configuration must include endpoint URL, organisation, and bucket name",
|
||||
nil,
|
||||
)
|
||||
if storeConfig.Journal != (JournalConfiguration{}) {
|
||||
if err := storeConfig.Journal.Validate(); err != nil {
|
||||
return core.E("store.StoreConfig.Validate", "journal config", err)
|
||||
}
|
||||
}
|
||||
if storeConfig.PurgeInterval < 0 {
|
||||
return core.E("store.StoreConfig.Validate", "purge interval must be zero or positive", nil)
|
||||
|
|
@ -79,6 +77,32 @@ type JournalConfiguration struct {
|
|||
BucketName string
|
||||
}
|
||||
|
||||
// Usage example: `if err := (store.JournalConfiguration{EndpointURL: "http://127.0.0.1:8086", Organisation: "core", BucketName: "events"}).Validate(); err != nil { return }`
|
||||
func (journalConfig JournalConfiguration) Validate() error {
|
||||
switch {
|
||||
case journalConfig.EndpointURL == "":
|
||||
return core.E(
|
||||
"store.JournalConfiguration.Validate",
|
||||
`endpoint URL is empty; use values like "http://127.0.0.1:8086"`,
|
||||
nil,
|
||||
)
|
||||
case journalConfig.Organisation == "":
|
||||
return core.E(
|
||||
"store.JournalConfiguration.Validate",
|
||||
`organisation is empty; use values like "core"`,
|
||||
nil,
|
||||
)
|
||||
case journalConfig.BucketName == "":
|
||||
return core.E(
|
||||
"store.JournalConfiguration.Validate",
|
||||
`bucket name is empty; use values like "events"`,
|
||||
nil,
|
||||
)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (journalConfig JournalConfiguration) isConfigured() bool {
|
||||
return journalConfig.EndpointURL != "" &&
|
||||
journalConfig.Organisation != "" &&
|
||||
|
|
|
|||
|
|
@ -141,6 +141,42 @@ func TestStore_JournalConfiguration_Good(t *testing.T) {
|
|||
}, config)
|
||||
}
|
||||
|
||||
func TestStore_JournalConfiguration_Good_Validate(t *testing.T) {
|
||||
err := (JournalConfiguration{
|
||||
EndpointURL: "http://127.0.0.1:8086",
|
||||
Organisation: "core",
|
||||
BucketName: "events",
|
||||
}).Validate()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestStore_JournalConfiguration_Bad_ValidateMissingEndpointURL(t *testing.T) {
|
||||
err := (JournalConfiguration{
|
||||
Organisation: "core",
|
||||
BucketName: "events",
|
||||
}).Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "endpoint URL is empty")
|
||||
}
|
||||
|
||||
func TestStore_JournalConfiguration_Bad_ValidateMissingOrganisation(t *testing.T) {
|
||||
err := (JournalConfiguration{
|
||||
EndpointURL: "http://127.0.0.1:8086",
|
||||
BucketName: "events",
|
||||
}).Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "organisation is empty")
|
||||
}
|
||||
|
||||
func TestStore_JournalConfiguration_Bad_ValidateMissingBucketName(t *testing.T) {
|
||||
err := (JournalConfiguration{
|
||||
EndpointURL: "http://127.0.0.1:8086",
|
||||
Organisation: "core",
|
||||
}).Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "bucket name is empty")
|
||||
}
|
||||
|
||||
func TestStore_JournalConfigured_Good(t *testing.T) {
|
||||
storeInstance, err := New(":memory:", WithJournal("http://127.0.0.1:8086", "core", "events"))
|
||||
require.NoError(t, err)
|
||||
|
|
@ -165,7 +201,8 @@ func TestStore_NewConfigured_Bad_PartialJournalConfiguration(t *testing.T) {
|
|||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "journal configuration must include endpoint URL, organisation, and bucket name")
|
||||
assert.Contains(t, err.Error(), "journal config")
|
||||
assert.Contains(t, err.Error(), "bucket name is empty")
|
||||
}
|
||||
|
||||
func TestStore_StoreConfig_Good_Validate(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue