diff --git a/internal/cli/options.go b/internal/cli/options.go index 3d23aa5..a942acf 100644 --- a/internal/cli/options.go +++ b/internal/cli/options.go @@ -41,7 +41,7 @@ func (o *Options) RunSetup( // Check for debug level or quiet supression via args conf := &config.JobSearchConfig{ - Logging: config.JobSearchConfigLogging{}, + Logging: config.JSCLogging{}, } if o.debug { conf.Logging.Level = "debug" diff --git a/internal/config/conf_database.go b/internal/config/conf_database.go index 04465a4..54b9ecb 100644 --- a/internal/config/conf_database.go +++ b/internal/config/conf_database.go @@ -5,31 +5,31 @@ import "reflect" // JSCKDatabase is the database section's configuration key. const JSCKDatabase string = "database" -// JobSearchConfigDatabase configures the SQLite database. File is the path to +// JSCDatabase configures the SQLite database. File is the path to // the database file. -type JobSearchConfigDatabase struct { +type JSCDatabase struct { File string `cfg:"file"` } // ---------- Section methods (see Section) ---------- // Keys returns the keys directly in the section. -func (c *JobSearchConfigDatabase) Keys() []string { return keysOf(c) } +func (c *JSCDatabase) Keys() []string { return keysOf(c) } -func (c JobSearchConfigDatabase) Map() map[string]any { return mapOf(c) } +func (c JSCDatabase) Map() map[string]any { return mapOf(c) } -func (c JobSearchConfigDatabase) Flatten() map[string]any { +func (c JSCDatabase) Flatten() map[string]any { return flattenOf(JSCKDatabase, c) } -func (c JobSearchConfigDatabase) TypeOf(key string) (reflect.Kind, error) { +func (c JSCDatabase) TypeOf(key string) (reflect.Kind, error) { return typeOf(c, key) } -func (c JobSearchConfigDatabase) ValueOf(key string) (any, error) { +func (c JSCDatabase) ValueOf(key string) (any, error) { return valueOf(c, key) } -func (c *JobSearchConfigDatabase) SetValue(key string, value any) error { +func (c *JSCDatabase) SetValue(key string, value any) error { return setValue(c, key, value) } diff --git a/internal/config/conf_logging.go b/internal/config/conf_logging.go index 7cfb3ed..f6b5538 100644 --- a/internal/config/conf_logging.go +++ b/internal/config/conf_logging.go @@ -9,40 +9,40 @@ import ( // JSCKLogging is the logging section's configuration key. const JSCKLogging string = "logging" -// JobSearchConfigLogging configures logging. Level is one of debug, info, +// JSCLogging configures logging. Level is one of debug, info, // warn or error. File, if set, is where commands that log to a file write. -type JobSearchConfigLogging struct { +type JSCLogging struct { Level string `cfg:"level" default:"warn"` File string `cfg:"file"` } // ---------- Section methods (see Section) ---------- -func (c JobSearchConfigLogging) Map() map[string]any { return mapOf(c) } +func (c JSCLogging) Map() map[string]any { return mapOf(c) } -func (c JobSearchConfigLogging) Flatten() map[string]any { +func (c JSCLogging) Flatten() map[string]any { return flattenOf(JSCKLogging, c) } -func (c JobSearchConfigLogging) TypeOf(key string) (reflect.Kind, error) { +func (c JSCLogging) TypeOf(key string) (reflect.Kind, error) { return typeOf(c, key) } -func (c JobSearchConfigLogging) ValueOf(key string) (any, error) { +func (c JSCLogging) ValueOf(key string) (any, error) { return valueOf(c, key) } -func (c *JobSearchConfigLogging) SetValue(key string, value any) error { +func (c *JSCLogging) SetValue(key string, value any) error { return setValue(c, key, value) } // Keys returns the keys directly in the section. -func (c *JobSearchConfigLogging) Keys() []string { return keysOf(c) } +func (c *JSCLogging) Keys() []string { return keysOf(c) } // ---------- Section specific methods ---------- // validate checks values before they are stored (see validator). -func (c *JobSearchConfigLogging) validate(key string, value any) error { +func (c *JSCLogging) validate(key string, value any) error { switch key { case "level": switch value.(string) { @@ -74,6 +74,6 @@ func ConfigLevelToLogLevel(level string) slog.Level { // LogLevel returns the configured level as an slog level. An unknown level // gives 99, above every real level, so nothing is logged. -func (c *JobSearchConfigLogging) LogLevel() slog.Level { +func (c *JSCLogging) LogLevel() slog.Level { return ConfigLevelToLogLevel(c.Level) } diff --git a/internal/config/conf_sections_test.go b/internal/config/conf_sections_test.go index 084fa7a..601deb4 100644 --- a/internal/config/conf_sections_test.go +++ b/internal/config/conf_sections_test.go @@ -62,7 +62,7 @@ func TestDefaults(t *testing.T) { } func TestLoggingSection(t *testing.T) { - c := JobSearchConfigLogging{Level: "info", File: "/tmp/x.log"} + c := JSCLogging{Level: "info", File: "/tmp/x.log"} if got, want := c.Keys(), []string{"level", "file"}; !reflect.DeepEqual(got, want) { t.Errorf("Keys = %v; want %v", got, want) @@ -100,7 +100,7 @@ func TestLoggingSection(t *testing.T) { } func TestLoggingLevelValidation(t *testing.T) { - var c JobSearchConfigLogging + var c JSCLogging for _, lvl := range []string{"debug", "info", "warn", "error"} { if err := c.SetValue("level", lvl); err != nil || c.Level != lvl { t.Errorf("SetValue(level, %q) = %v, Level = %q", lvl, err, c.Level) @@ -142,7 +142,7 @@ func TestConfigLevelToLogLevel(t *testing.T) { if got := ConfigLevelToLogLevel(in); got != want { t.Errorf("ConfigLevelToLogLevel(%q) = %v; want %v", in, got, want) } - c := JobSearchConfigLogging{Level: in} + c := JSCLogging{Level: in} if got := c.LogLevel(); got != want { t.Errorf("LogLevel() with %q = %v; want %v", in, got, want) } @@ -187,9 +187,9 @@ func TestConcreteSourceSections(t *testing.T) { apiRoot bool blockShutdown bool }{ - {JSCKSourceFindATender, &JobSearchConfigSourceFindATender{}, true, false}, - {JSCKSourceFTSMock, &JobSearchConfigSourceFTSMock{}, true, false}, - {JSCKSourceSleeper, &JobSearchConfigSourceSleeper{}, false, true}, + {JSCKSourceFindATender, &JSCSourceFindATender{}, true, false}, + {JSCKSourceFTSMock, &JSCSourceFTSMock{}, true, false}, + {JSCKSourceSleeper, &JSCSourceSleeper{}, false, true}, } for _, tt := range tests { t.Run(tt.prefix, func(t *testing.T) { @@ -252,7 +252,7 @@ func TestConcreteSourceSections(t *testing.T) { } func TestSourcesSection(t *testing.T) { - var c JobSearchConfigSources + var c JSCSources if err := c.SetValue("fts_mock.api_root", "http://mock"); err != nil { t.Fatalf("SetValue: %v", err) } @@ -457,7 +457,7 @@ func TestLoadFromViperInvalidValues(t *testing.T) { } func TestDatabaseSection(t *testing.T) { - c := JobSearchConfigDatabase{File: "./jobsearch.db"} + c := JSCDatabase{File: "./jobsearch.db"} if got, want := c.Keys(), []string{"file"}; !reflect.DeepEqual(got, want) { t.Errorf("Keys = %v; want %v", got, want) @@ -501,6 +501,8 @@ func TestViperValue(t *testing.T) { viper.Set("s", "text") viper.Set("i", "42") // strings are converted viper.Set("b", "true") + viper.Set("env_list", "a, b") // e.g. from an environment variable + viper.Set("file_list", []any{"a", "b"}) // e.g. from a YAML list for _, tt := range []struct { key string @@ -510,13 +512,15 @@ func TestViperValue(t *testing.T) { {"s", reflect.String, "text"}, {"i", reflect.Int, 42}, {"b", reflect.Bool, true}, + {"env_list", reflect.Slice, "a, b"}, // split by SetValue + {"file_list", reflect.Slice, []string{"a", "b"}}, } { got, ok := viperValue(tt.key, tt.kind) - if !ok || got != tt.want { + if !ok || !reflect.DeepEqual(got, tt.want) { t.Errorf("viperValue(%q, %v) = %#v, %v; want %#v", tt.key, tt.kind, got, ok, tt.want) } } - for _, kind := range []reflect.Kind{reflect.Float64, reflect.Int8, reflect.Slice} { + for _, kind := range []reflect.Kind{reflect.Float64, reflect.Int8, reflect.Map} { if got, ok := viperValue("i", kind); ok { t.Errorf("viperValue(i, %v) = %#v, true; want unhandled", kind, got) } diff --git a/internal/config/conf_source_findATender.go b/internal/config/conf_source_findATender.go index 70ebd4b..7cf038e 100644 --- a/internal/config/conf_source_findATender.go +++ b/internal/config/conf_source_findATender.go @@ -5,29 +5,29 @@ import "reflect" // JSCKSourceFindATender is the Find a Tender source's configuration key. const JSCKSourceFindATender string = "find_a_tender" -// JobSearchConfigSourceFindATender configures fetching from the Find a Tender +// JSCSourceFindATender configures fetching from the Find a Tender // OCDS API at APIRoot. -type JobSearchConfigSourceFindATender struct { +type JSCSourceFindATender struct { JobSearchConfigSource APIRoot string `cfg:"api_root" default:"https://www.find-tender.service.gov.uk/api/1.0"` } // ---------- Section methods (see Section) ---------- -func (c JobSearchConfigSourceFindATender) Map() map[string]any { return mapOf(c) } +func (c JSCSourceFindATender) Map() map[string]any { return mapOf(c) } -func (c JobSearchConfigSourceFindATender) Flatten() map[string]any { +func (c JSCSourceFindATender) Flatten() map[string]any { return flattenOf(JSCKSourceFindATender, c) } -func (c JobSearchConfigSourceFindATender) TypeOf(key string) (reflect.Kind, error) { +func (c JSCSourceFindATender) TypeOf(key string) (reflect.Kind, error) { return typeOf(c, key) } -func (c JobSearchConfigSourceFindATender) ValueOf(key string) (any, error) { +func (c JSCSourceFindATender) ValueOf(key string) (any, error) { return valueOf(c, key) } -func (c *JobSearchConfigSourceFindATender) SetValue(key string, value any) error { +func (c *JSCSourceFindATender) SetValue(key string, value any) error { return setValue(c, key, value) } diff --git a/internal/config/conf_source_ftsMock.go b/internal/config/conf_source_ftsMock.go index 1113a85..93b146f 100644 --- a/internal/config/conf_source_ftsMock.go +++ b/internal/config/conf_source_ftsMock.go @@ -5,29 +5,29 @@ import "reflect" // JSCKSourceFTSMock is the Find a Tender mock source's configuration key. const JSCKSourceFTSMock string = "fts_mock" -// JobSearchConfigSourceFTSMock configures fetching from a Find a Tender mock +// JSCSourceFTSMock configures fetching from a Find a Tender mock // server (cmd/mock-fts) at APIRoot. -type JobSearchConfigSourceFTSMock struct { +type JSCSourceFTSMock struct { JobSearchConfigSource APIRoot string `cfg:"api_root"` } // ---------- Section methods (see Section) ---------- -func (c JobSearchConfigSourceFTSMock) Map() map[string]any { return mapOf(c) } +func (c JSCSourceFTSMock) Map() map[string]any { return mapOf(c) } -func (c JobSearchConfigSourceFTSMock) Flatten() map[string]any { +func (c JSCSourceFTSMock) Flatten() map[string]any { return flattenOf(JSCKSourceFTSMock, c) } -func (c JobSearchConfigSourceFTSMock) TypeOf(key string) (reflect.Kind, error) { +func (c JSCSourceFTSMock) TypeOf(key string) (reflect.Kind, error) { return typeOf(c, key) } -func (c JobSearchConfigSourceFTSMock) ValueOf(key string) (any, error) { +func (c JSCSourceFTSMock) ValueOf(key string) (any, error) { return valueOf(c, key) } -func (c *JobSearchConfigSourceFTSMock) SetValue(key string, value any) error { +func (c *JSCSourceFTSMock) SetValue(key string, value any) error { return setValue(c, key, value) } diff --git a/internal/config/conf_source_sleeper.go b/internal/config/conf_source_sleeper.go index 84f5d5d..383d90e 100644 --- a/internal/config/conf_source_sleeper.go +++ b/internal/config/conf_source_sleeper.go @@ -5,30 +5,30 @@ import "reflect" // JSCKSourceSleeper is the sleeper source's configuration key. const JSCKSourceSleeper string = "sleeper" -// JobSearchConfigSourceSleeper configures the sleeper, a test source that +// JSCSourceSleeper configures the sleeper, a test source that // simulates work by sleeping. With BlockShutdown its work ignores // cancellation, to test shutdown waiting on a non-cooperative task. -type JobSearchConfigSourceSleeper struct { +type JSCSourceSleeper struct { JobSearchConfigSource BlockShutdown bool `cfg:"block_shutdown"` } // ---------- Section methods (see Section) ---------- -func (c JobSearchConfigSourceSleeper) Map() map[string]any { return mapOf(c) } +func (c JSCSourceSleeper) Map() map[string]any { return mapOf(c) } -func (c JobSearchConfigSourceSleeper) Flatten() map[string]any { +func (c JSCSourceSleeper) Flatten() map[string]any { return flattenOf(JSCKSourceSleeper, c) } -func (c JobSearchConfigSourceSleeper) TypeOf(key string) (reflect.Kind, error) { +func (c JSCSourceSleeper) TypeOf(key string) (reflect.Kind, error) { return typeOf(c, key) } -func (c JobSearchConfigSourceSleeper) ValueOf(key string) (any, error) { +func (c JSCSourceSleeper) ValueOf(key string) (any, error) { return valueOf(c, key) } -func (c *JobSearchConfigSourceSleeper) SetValue(key string, value any) error { +func (c *JSCSourceSleeper) SetValue(key string, value any) error { return setValue(c, key, value) } diff --git a/internal/config/conf_sources.go b/internal/config/conf_sources.go index 7be92eb..1eebfee 100644 --- a/internal/config/conf_sources.go +++ b/internal/config/conf_sources.go @@ -8,35 +8,35 @@ import ( // JSCKSources is the sources section's configuration key. const JSCKSources string = "sources" -// JobSearchConfigSources configures each source of opportunities, one field +// JSCSources configures each source of opportunities, one field // per source. Each field's cfg tag must match the section's JSCK* constant. -type JobSearchConfigSources struct { - FindATender JobSearchConfigSourceFindATender `cfg:"find_a_tender"` - FTSMock JobSearchConfigSourceFTSMock `cfg:"fts_mock"` - Sleeper JobSearchConfigSourceSleeper `cfg:"sleeper"` +type JSCSources struct { + FindATender JSCSourceFindATender `cfg:"find_a_tender"` + FTSMock JSCSourceFTSMock `cfg:"fts_mock"` + Sleeper JSCSourceSleeper `cfg:"sleeper"` } // ---------- Section methods (see Section) ---------- -func (c JobSearchConfigSources) Map() map[string]any { return mapOf(c) } +func (c JSCSources) Map() map[string]any { return mapOf(c) } -func (c JobSearchConfigSources) Flatten() map[string]any { +func (c JSCSources) Flatten() map[string]any { return flattenOf(JSCKSources, c) } -func (c JobSearchConfigSources) TypeOf(key string) (reflect.Kind, error) { +func (c JSCSources) TypeOf(key string) (reflect.Kind, error) { return typeOf(c, key) } -func (c JobSearchConfigSources) ValueOf(key string) (any, error) { +func (c JSCSources) ValueOf(key string) (any, error) { return valueOf(c, key) } -func (c *JobSearchConfigSources) SetValue(key string, value any) error { +func (c *JSCSources) SetValue(key string, value any) error { return setValue(c, key, value) } -// ---------- Generic Source definition ---------- +// ========== Generic source ========== // JobSearchConfigSource holds the settings every source has, and is embedded // in each source's section. Interval is how often an enabled source fetches. @@ -45,7 +45,7 @@ type JobSearchConfigSource struct { Interval time.Duration `cfg:"interval" default:"10m"` } -// ---------- Section methods for Source (see Section) ---------- +// ---------- Section methods (see Section) ---------- func (c JobSearchConfigSource) Map() map[string]any { return mapOf(c) } diff --git a/internal/config/config.go b/internal/config/config.go index 4a1fa47..771e995 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,9 +24,9 @@ const EtcConfigPath string = "/etc/job-search" // JobSearchConfig is the application's whole configuration, one field per // section. Each field's cfg tag must match the section's JSCK* constant. type JobSearchConfig struct { - Logging JobSearchConfigLogging `cfg:"logging"` - Database JobSearchConfigDatabase `cfg:"database"` - Sources JobSearchConfigSources `cfg:"sources"` + Database JSCDatabase `cfg:"database"` + Logging JSCLogging `cfg:"logging"` + Sources JSCSources `cfg:"sources"` } // LoadDefaultConfig sets Viper's defaults from the configuration's default diff --git a/internal/config/load_test.go b/internal/config/load_test.go index 24876d4..86761f1 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -106,7 +106,7 @@ func TestInitKeepsCommandLineLevel(t *testing.T) { initDir(t, []string{}, "logging:\n level: info\n") logs := captureLogs(t, slog.LevelDebug) - conf := JobSearchConfig{Logging: JobSearchConfigLogging{Level: level}} + conf := JobSearchConfig{Logging: JSCLogging{Level: level}} if err := Init("", &conf); err != nil { t.Fatal(err) } diff --git a/internal/findATender/api_test.go b/internal/findATender/api_test.go index f908ff4..b302938 100644 --- a/internal/findATender/api_test.go +++ b/internal/findATender/api_test.go @@ -21,7 +21,7 @@ import ( ) func testConfig(level string) *config.JobSearchConfig { - return &config.JobSearchConfig{Logging: config.JobSearchConfigLogging{Level: level}} + return &config.JobSearchConfig{Logging: config.JSCLogging{Level: level}} } // quietLogs discards slog output for the duration of the test, or sends it