refactor: add AX config validation helpers
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-04 15:47:56 +00:00
parent 32e7413bf4
commit cae3c32d51
6 changed files with 150 additions and 26 deletions

View file

@ -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}
}

View file

@ -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"`)
}

View file

@ -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
}

View file

@ -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",

View file

@ -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 != "" &&

View file

@ -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) {