Fixed lint issues and missing documentation
This commit is contained in:
1 parent
7fd321df97
commit
6e6dec68c0
34 files changed
+346
-156
No files matched your search
@@ -33,10 +33,10 @@ is kept.
|
||||
| Key | Type | Default | Notes |
|
||||
|---|---|---|---|
|
||||
| `logging.level` | string | `warn` | One of `debug`, `info`, `warn`, `error` |
|
||||
| `logging.file` | string | | Log file for `fetch service` (unless `--foreground`) |
|
||||
| `logging.file` | string | | Log file for `fetch service` (unless `--foreground`); other commands log to the terminal |
|
||||
| `sources.<source>.enabled` | bool | `false` | For every source |
|
||||
| `sources.<source>.interval` | duration | `10m` | For every source; e.g. `30s`, `1h` |
|
||||
| `sources.find_a_tender.api_root` | string | URL of the Find A Tender service API. Currently `https://www.find-tender.service.gov.uk/api/1.0` | |
|
||||
| `sources.find_a_tender.api_root` | string | `https://www.find-tender.service.gov.uk/api/1.0` | URL of the Find a Tender API |
|
||||
| `sources.fts_mock.api_root` | string | | URL of the mock FTS server (see `internal/mock`) |
|
||||
| `sources.sleeper.block_shutdown` | bool | `false` | Test source: if true, its sleep ignores shutdown |
|
||||
|
||||
@@ -73,14 +73,20 @@ per key:
|
||||
`section.key`. An embedded struct has its fields promoted, as Go does;
|
||||
every source embeds `JobSearchConfigSource` for `enabled` and `interval`.
|
||||
- `time.Duration` fields report `reflect.String` from `TypeOf`, accept either a
|
||||
`time.Duration` or a duration string in `SetValue`, and are written as
|
||||
strings (`"10m0s"`) by `Map`, `JSON` and `YAML`.
|
||||
`time.Duration` or a duration string in `SetValue`, and are given out as
|
||||
strings (`"10m0s"`) by `Map`, `Flatten` and `ValueOf`, and so by `JSON`,
|
||||
`YAML` and `String` too.
|
||||
- A section can reject values by implementing `validate(key, value) error` on
|
||||
its pointer type. `JobSearchConfigLogging` does this for `level`.
|
||||
|
||||
Each section type has the same small set of methods, each a one-line call into
|
||||
`section.go`: `Map`, `Flatten`, `TypeOf`, `ValueOf` and `SetValue`, plus
|
||||
`Keys` on some.
|
||||
Every section implements the `Section` interface defined in `section.go`:
|
||||
`Map`, `Flatten`, `TypeOf`, `ValueOf` and `SetValue`, each a one-line call into
|
||||
`section.go` (some sections also have `Keys`). The methods sit under a
|
||||
`// ---------- Section methods (see Section) ----------` banner in each
|
||||
section's file. `TestSections` walks `JobSearchConfig` and checks every
|
||||
section, including embedded ones, implements `Section` and that its
|
||||
`Flatten` keys match its `cfg` tag, so a new section is checked without being
|
||||
listed anywhere.
|
||||
|
||||
|
||||
## Adding a key or section
|
||||
@@ -90,11 +96,14 @@ else is needed for it to be loaded, printed, and read or written by the
|
||||
|
||||
To add a new section:
|
||||
1. Create a `conf_*.go` file with the section struct, a `JSCK*` constant for
|
||||
its key, and the standard methods (copy an existing section).
|
||||
its key, and an implementation of `Section` (copy an existing section's
|
||||
methods).
|
||||
2. Add the struct as a tagged field of its parent: `JobSearchConfig` in
|
||||
`config.go`, or `JobSearchConfigSources` in `conf_sources.go` for a source,
|
||||
etc. The field's `cfg` tag must match the `JSCK*` constant.
|
||||
|
||||
Nothing in `section.go` needs changing.
|
||||
|
||||
|
||||
## API
|
||||
- `Init(cfgFile, &conf)`: load the configuration, as described above.
|
||||
@@ -119,7 +128,7 @@ To add a new section:
|
||||
| `config.go` | `JobSearchConfig`, defaults, Viper loading, text output |
|
||||
| `load.go` | `Init`, `.env` and config file loading |
|
||||
| `modify.go` | `StoreValue` and `PurgeValue` |
|
||||
| `section.go` | Struct tag reflection shared by every section |
|
||||
| `section.go` | The `Section` interface and the struct tag reflection shared by every section |
|
||||
| `conf_logging.go` | `logging` section and log level helpers |
|
||||
| `conf_sources.go` | `sources` section and the shared source fields |
|
||||
| `conf_source_*.go` | One file per source section |
|
||||
|
||||
@@ -6,18 +6,17 @@ import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// JSCKLogging is the logging section's configuration key.
|
||||
const JSCKLogging string = "logging"
|
||||
|
||||
// JobSearchConfigLogging 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 {
|
||||
Level string `cfg:"level" default:"warn"`
|
||||
File string `cfg:"file"`
|
||||
}
|
||||
|
||||
/*
|
||||
* General section functions
|
||||
* See related methods in section.go
|
||||
*/
|
||||
func (c *JobSearchConfigLogging) Keys() []string { return keysOf(c) }
|
||||
// ---------- Section methods (see Section) ----------
|
||||
|
||||
func (c JobSearchConfigLogging) Map() map[string]any { return mapOf(c) }
|
||||
|
||||
@@ -37,8 +36,12 @@ func (c *JobSearchConfigLogging) SetValue(key string, value any) error {
|
||||
return setValue(c, key, value)
|
||||
}
|
||||
|
||||
/* Section specific functions */
|
||||
// Checks values before they are stored (see validator)
|
||||
// Keys returns the keys directly in the section.
|
||||
func (c *JobSearchConfigLogging) 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 {
|
||||
switch key {
|
||||
case "level":
|
||||
@@ -52,7 +55,8 @@ func (c *JobSearchConfigLogging) validate(key string, value any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Utility methods for converting level strings to slog levels
|
||||
// ConfigLevelToLogLevel converts a configured level name to an slog level. An
|
||||
// unknown name gives 99, above every real level, so nothing is logged.
|
||||
func ConfigLevelToLogLevel(level string) slog.Level {
|
||||
switch level {
|
||||
case "debug":
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestSourceSection(t *testing.T) {
|
||||
if got := c.Map(); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Map = %v; want %v", got, want)
|
||||
}
|
||||
wantFlat := map[string]any{"enabled": true, "interval": time.Minute}
|
||||
wantFlat := map[string]any{"enabled": true, "interval": "1m0s"}
|
||||
if got := c.Flatten(); !reflect.DeepEqual(got, wantFlat) {
|
||||
t.Errorf("Flatten = %v; want %v (no prefix)", got, wantFlat)
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func TestSourceSection(t *testing.T) {
|
||||
if k, err := c.TypeOf("enabled"); err != nil || k != reflect.Bool {
|
||||
t.Errorf("TypeOf(enabled) = %v, %v; want Bool", k, err)
|
||||
}
|
||||
if v, err := c.ValueOf("interval"); err != nil || v != time.Minute {
|
||||
if v, err := c.ValueOf("interval"); err != nil || v != "1m0s" {
|
||||
t.Errorf("ValueOf(interval) = %v, %v", v, err)
|
||||
}
|
||||
if err := c.SetValue("interval", "5s"); err != nil || c.Interval != 5*time.Second {
|
||||
@@ -180,19 +180,10 @@ func TestSourceSection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// sourceSection is the method set shared by every concrete source section.
|
||||
type sourceSection interface {
|
||||
Map() map[string]any
|
||||
Flatten() map[string]any
|
||||
TypeOf(string) (reflect.Kind, error)
|
||||
ValueOf(string) (any, error)
|
||||
SetValue(string, any) error
|
||||
}
|
||||
|
||||
func TestConcreteSourceSections(t *testing.T) {
|
||||
tests := []struct {
|
||||
prefix string
|
||||
section sourceSection
|
||||
section Section
|
||||
apiRoot bool
|
||||
blockShutdown bool
|
||||
}{
|
||||
@@ -220,7 +211,7 @@ func TestConcreteSourceSections(t *testing.T) {
|
||||
wantMap := map[string]any{"enabled": true, "interval": "2m0s"}
|
||||
wantFlat := map[string]any{
|
||||
tt.prefix + ".enabled": true,
|
||||
tt.prefix + ".interval": 2 * time.Minute,
|
||||
tt.prefix + ".interval": "2m0s",
|
||||
}
|
||||
if tt.apiRoot {
|
||||
if err := s.SetValue("api_root", "http://localhost:8080"); err != nil {
|
||||
|
||||
@@ -2,17 +2,18 @@ package config
|
||||
|
||||
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
|
||||
// OCDS API at APIRoot.
|
||||
type JobSearchConfigSourceFindATender struct {
|
||||
JobSearchConfigSource
|
||||
APIRoot string `cfg:"api_root" default:"https://www.find-tender.service.gov.uk/api/1.0"`
|
||||
}
|
||||
|
||||
/*
|
||||
* General section functions
|
||||
* See related methods in section.go
|
||||
*/
|
||||
// ---------- Section methods (see Section) ----------
|
||||
|
||||
func (c JobSearchConfigSourceFindATender) Map() map[string]any { return mapOf(c) }
|
||||
|
||||
func (c JobSearchConfigSourceFindATender) Flatten() map[string]any {
|
||||
|
||||
@@ -2,17 +2,18 @@ package config
|
||||
|
||||
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
|
||||
// server (cmd/mock-fts) at APIRoot.
|
||||
type JobSearchConfigSourceFTSMock struct {
|
||||
JobSearchConfigSource
|
||||
APIRoot string `cfg:"api_root"`
|
||||
}
|
||||
|
||||
/*
|
||||
* General section functions
|
||||
* See related methods in section.go
|
||||
*/
|
||||
// ---------- Section methods (see Section) ----------
|
||||
|
||||
func (c JobSearchConfigSourceFTSMock) Map() map[string]any { return mapOf(c) }
|
||||
|
||||
func (c JobSearchConfigSourceFTSMock) Flatten() map[string]any {
|
||||
|
||||
@@ -2,17 +2,19 @@ package config
|
||||
|
||||
import "reflect"
|
||||
|
||||
// JSCKSourceSleeper is the sleeper source's configuration key.
|
||||
const JSCKSourceSleeper string = "sleeper"
|
||||
|
||||
// JobSearchConfigSourceSleeper 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 {
|
||||
JobSearchConfigSource
|
||||
BlockShutdown bool `cfg:"block_shutdown"`
|
||||
}
|
||||
|
||||
/*
|
||||
* General section functions
|
||||
* See related methods in section.go
|
||||
*/
|
||||
// ---------- Section methods (see Section) ----------
|
||||
|
||||
func (c JobSearchConfigSourceSleeper) Map() map[string]any { return mapOf(c) }
|
||||
|
||||
func (c JobSearchConfigSourceSleeper) Flatten() map[string]any {
|
||||
|
||||
@@ -5,19 +5,19 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// JSCKSources is the sources section's configuration key.
|
||||
const JSCKSources string = "sources"
|
||||
|
||||
// Each field's cfg tag must match the section's JSCK* constant.
|
||||
// JobSearchConfigSources 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"`
|
||||
}
|
||||
|
||||
/*
|
||||
* General section functions
|
||||
* See related methods in section.go
|
||||
*/
|
||||
// ---------- Section methods (see Section) ----------
|
||||
|
||||
func (c JobSearchConfigSources) Map() map[string]any { return mapOf(c) }
|
||||
|
||||
func (c JobSearchConfigSources) Flatten() map[string]any {
|
||||
@@ -36,19 +36,16 @@ func (c *JobSearchConfigSources) SetValue(key string, value any) error {
|
||||
return setValue(c, key, value)
|
||||
}
|
||||
|
||||
/*
|
||||
*** Generic source definition, embedded in each source ***
|
||||
*/
|
||||
// ---------- Generic Source definition ----------
|
||||
|
||||
// JobSearchConfigSource holds the settings every source has, and is embedded
|
||||
// in each source's section. Interval is how often an enabled source fetches.
|
||||
type JobSearchConfigSource struct {
|
||||
Enabled bool `cfg:"enabled"`
|
||||
Interval time.Duration `cfg:"interval" default:"10m"`
|
||||
}
|
||||
|
||||
/*
|
||||
* General section functions
|
||||
* See related methods in section.go
|
||||
*/
|
||||
func (c *JobSearchConfigSource) Keys() []string { return keysOf(c) }
|
||||
// ---------- Section methods for Source (see Section) ----------
|
||||
|
||||
func (c JobSearchConfigSource) Map() map[string]any { return mapOf(c) }
|
||||
|
||||
@@ -67,3 +64,6 @@ func (c JobSearchConfigSource) ValueOf(key string) (any, error) {
|
||||
func (c *JobSearchConfigSource) SetValue(key string, value any) error {
|
||||
return setValue(c, key, value)
|
||||
}
|
||||
|
||||
// Keys returns the keys directly in the section.
|
||||
func (c *JobSearchConfigSource) Keys() []string { return keysOf(c) }
|
||||
@@ -11,16 +11,23 @@ import (
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// EnvPrefix prefixes environment variables that override configuration keys,
|
||||
// e.g. JOBSEARCH_LOGGING_LEVEL for logging.level.
|
||||
const EnvPrefix string = "JOBSEARCH"
|
||||
|
||||
// EtcConfigPath is searched for config.* when no config file is given, before
|
||||
// the working directory.
|
||||
const EtcConfigPath string = "/etc/job-search"
|
||||
|
||||
// Each field's cfg tag must match the section's JSCK* constant.
|
||||
// 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"`
|
||||
Sources JobSearchConfigSources `cfg:"sources"`
|
||||
}
|
||||
|
||||
// Default configuration, taken from the default struct tags
|
||||
// LoadDefaultConfig sets Viper's defaults from the configuration's default
|
||||
// struct tags.
|
||||
func LoadDefaultConfig() {
|
||||
var conf JobSearchConfig
|
||||
if err := applyDefaults(&conf); err != nil {
|
||||
@@ -29,6 +36,8 @@ func LoadDefaultConfig() {
|
||||
LoadToViper(conf)
|
||||
}
|
||||
|
||||
// ---------- Section methods (see Section) ----------
|
||||
|
||||
func (c JobSearchConfig) Map() map[string]any { return mapOf(c) }
|
||||
|
||||
func (c JobSearchConfig) Flatten() map[string]any { return flattenOf("", c) }
|
||||
@@ -45,7 +54,8 @@ func (c *JobSearchConfig) SetValue(key string, value any) error {
|
||||
return setValue(c, key, value)
|
||||
}
|
||||
|
||||
// Utility method for loading a configuration from Viper
|
||||
// LoadFromViper copies every key Viper knows that conf also has into conf.
|
||||
// Unknown keys are skipped; invalid values are logged and ignored.
|
||||
func LoadFromViper(conf *JobSearchConfig) {
|
||||
keys := viper.AllKeys()
|
||||
for idx := range keys {
|
||||
@@ -71,7 +81,7 @@ func LoadFromViper(conf *JobSearchConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
// Utility method for loading a configuration into Viper as defaults
|
||||
// LoadToViper sets every key of conf as a Viper default.
|
||||
func LoadToViper(conf JobSearchConfig) {
|
||||
flatMap := conf.Flatten()
|
||||
for key := range flatMap {
|
||||
@@ -79,7 +89,8 @@ func LoadToViper(conf JobSearchConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
// Converts configuration to a string in flat dotted format
|
||||
// String formats the configuration one "key = value <type>" line per key, with
|
||||
// dotted keys.
|
||||
func (conf JobSearchConfig) String() string {
|
||||
var sb strings.Builder
|
||||
for k, v := range conf.Flatten() {
|
||||
@@ -89,14 +100,14 @@ func (conf JobSearchConfig) String() string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Converts configuration to JSON
|
||||
// JSON formats the configuration as indented JSON.
|
||||
func (conf JobSearchConfig) JSON() string {
|
||||
confMap := conf.Map()
|
||||
jbytes, _ := json.MarshalIndent(confMap, "", " ")
|
||||
return string(jbytes)
|
||||
}
|
||||
|
||||
// Converts configuration to YAML
|
||||
// YAML formats the configuration as YAML.
|
||||
func (conf JobSearchConfig) YAML() string {
|
||||
confMap := conf.Map()
|
||||
ybytes, _ := yaml.Marshal(confMap)
|
||||
|
||||
@@ -34,10 +34,18 @@ func TestKeysRoundTrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Every section has the full method set, and its Flatten keys (which use the
|
||||
// section's JSCK* constant) line up with its cfg tag in the parent.
|
||||
// sectionType is the Section interface type.
|
||||
var sectionType = reflect.TypeOf((*Section)(nil)).Elem()
|
||||
|
||||
// Every section, including embedded ones, implements Section, and its
|
||||
// Flatten keys (which use the section's JSCK* constant) line up with its cfg
|
||||
// tag in the parent. Sections are found by walking JobSearchConfig, so a new
|
||||
// section is checked without being listed anywhere.
|
||||
func TestSections(t *testing.T) {
|
||||
var conf JobSearchConfig
|
||||
if !reflect.PointerTo(reflect.TypeOf(conf)).Implements(sectionType) {
|
||||
t.Error("*JobSearchConfig does not implement Section")
|
||||
}
|
||||
all := conf.Flatten()
|
||||
checkSections(t, all, "", reflect.ValueOf(&conf).Elem())
|
||||
}
|
||||
@@ -49,11 +57,10 @@ func checkSections(t *testing.T, all map[string]any, prefix string, section refl
|
||||
}
|
||||
fv := section.FieldByIndex(f.index)
|
||||
name := fv.Type().Name()
|
||||
for _, m := range []string{"Map", "Flatten", "TypeOf", "ValueOf", "SetValue"} {
|
||||
if !fv.Addr().MethodByName(m).IsValid() {
|
||||
t.Errorf("%s is missing method %s", name, m)
|
||||
}
|
||||
if !fv.Addr().Type().Implements(sectionType) {
|
||||
t.Errorf("*%s does not implement Section", name)
|
||||
}
|
||||
checkEmbedded(t, fv.Type())
|
||||
if flatten := fv.MethodByName("Flatten"); flatten.IsValid() {
|
||||
keys := flatten.Call(nil)[0].Interface().(map[string]any)
|
||||
for k := range keys {
|
||||
@@ -66,3 +73,19 @@ func checkSections(t *testing.T, all map[string]any, prefix string, section refl
|
||||
checkSections(t, all, joinKey(prefix, f.key), fv)
|
||||
}
|
||||
}
|
||||
|
||||
// checkEmbedded checks that structs embedded in a section, such as
|
||||
// JobSearchConfigSource, implement Section too.
|
||||
func checkEmbedded(t *testing.T, section reflect.Type) {
|
||||
for i := 0; i < section.NumField(); i++ {
|
||||
sf := section.Field(i)
|
||||
if !sf.Anonymous || sf.Type.Kind() != reflect.Struct {
|
||||
continue
|
||||
}
|
||||
if !reflect.PointerTo(sf.Type).Implements(sectionType) {
|
||||
t.Errorf("*%s (embedded in %s) does not implement Section",
|
||||
sf.Type.Name(), section.Name())
|
||||
}
|
||||
checkEmbedded(t, sf.Type)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Debug: Prints detected environment variables
|
||||
// printEnv logs the JOBSEARCH* environment variables at debug level.
|
||||
func printEnv() {
|
||||
slog.Debug("Environment:")
|
||||
env := os.Environ()
|
||||
@@ -24,7 +24,8 @@ func printEnv() {
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Prints loaded configuration
|
||||
// printLoadedConfig logs every key Viper has loaded, with its value and type,
|
||||
// at debug level.
|
||||
func printLoadedConfig() {
|
||||
slog.Debug("Loaded config:")
|
||||
keys := viper.AllKeys()
|
||||
@@ -37,7 +38,7 @@ func printLoadedConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Debug: Prints parsed configuration
|
||||
// printConfig logs conf at debug level.
|
||||
func printConfig(conf JobSearchConfig) {
|
||||
slog.Debug("Config:")
|
||||
conf.PrintLog(slog.LevelDebug)
|
||||
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Reads in dotenv file for configuration
|
||||
// loadEnv loads environment variables from a .env file in the working
|
||||
// directory.
|
||||
func loadEnv() error {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
return fmt.Errorf("invalid .env file: %w", err)
|
||||
@@ -17,7 +18,9 @@ func loadEnv() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reads in config file and ENV variables if set to form a configuration.
|
||||
// loadConfig sets up Viper with the defaults, cfgFile (or config.* found in
|
||||
// EtcConfigPath or the working directory) and JOBSEARCH_* environment
|
||||
// variables. A missing config file is only a warning.
|
||||
func loadConfig(cfgFile string) error {
|
||||
if cfgFile != "" {
|
||||
// Use config file from the flag.
|
||||
@@ -54,7 +57,9 @@ func loadConfig(cfgFile string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Initilises environment file configuration and general app configuration
|
||||
// Init loads the .env file and the configuration (see loadConfig) into conf.
|
||||
// A debug or error log level already set in conf, from --debug or --quiet,
|
||||
// is kept over the loaded one.
|
||||
func Init(cfgFile string, conf *JobSearchConfig) error {
|
||||
if err := loadEnv(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -5,22 +5,23 @@ import (
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// Prints configuration to stdout in flat dotted format
|
||||
// PrintTerminal prints the configuration to stdout as String formats it.
|
||||
func (conf JobSearchConfig) PrintTerminal() {
|
||||
fmt.Print(conf.String() + "\n")
|
||||
}
|
||||
|
||||
// Prints configuration to stdout as JSON
|
||||
// PrintJson prints the configuration to stdout as JSON.
|
||||
func (conf JobSearchConfig) PrintJson() {
|
||||
fmt.Print(conf.JSON() + "\n")
|
||||
}
|
||||
|
||||
// Prints configuration to stdout as YAML
|
||||
// PrintYaml prints the configuration to stdout as YAML.
|
||||
func (conf JobSearchConfig) PrintYaml() {
|
||||
fmt.Print(conf.YAML() + "\n")
|
||||
}
|
||||
|
||||
// Prints configuration to logrus log at debug, info (default) or error level
|
||||
// PrintLog logs each configuration key and value at atLevel: debug, warn,
|
||||
// error, or info for any other level.
|
||||
func (conf JobSearchConfig) PrintLog(atLevel slog.Level) {
|
||||
var logFn func(args ...interface{})
|
||||
|
||||
|
||||
@@ -36,6 +36,32 @@ var (
|
||||
timeType = reflect.TypeOf(time.Time{})
|
||||
)
|
||||
|
||||
// Section is the method set of every configuration section, including
|
||||
// JobSearchConfig itself, implemented on the section's pointer type. Keys are
|
||||
// dotted paths relative to the section, e.g. "sleeper.interval" in
|
||||
// JobSearchConfigSources. Durations are given out as strings such as "10m0s",
|
||||
// matching TypeOf's reflect.String for them. TestSections checks every
|
||||
// section, found by walking JobSearchConfig, implements it.
|
||||
type Section interface {
|
||||
// Map returns the section as a map of keys to values, with nested
|
||||
// sections as nested maps.
|
||||
Map() map[string]any
|
||||
// Flatten returns the section as a flat map of dotted keys, each
|
||||
// prefixed with the section's own key (its JSCK* constant), so a
|
||||
// section's keys match how its parent flattens it. JobSearchConfig and
|
||||
// the embedded JobSearchConfigSource have no prefix.
|
||||
Flatten() map[string]any
|
||||
// TypeOf returns the kind of the value at key, which is how it is read
|
||||
// from Viper, or an error for an unknown key.
|
||||
TypeOf(key string) (reflect.Kind, error)
|
||||
// ValueOf returns the value at key, or an error for an unknown key.
|
||||
ValueOf(key string) (any, error)
|
||||
// SetValue stores value at key. value may be of the field's type or a
|
||||
// string to parse (e.g. "10m", "true", "42"). The section's validation,
|
||||
// if any, runs first; on error the field is unchanged.
|
||||
SetValue(key string, value any) error
|
||||
}
|
||||
|
||||
// validator is implemented by sections that need to reject some values.
|
||||
// key is the field's own key within the section; value has already been
|
||||
// converted to the field's type.
|
||||
@@ -43,6 +69,8 @@ type validator interface {
|
||||
validate(key string, value any) error
|
||||
}
|
||||
|
||||
// field is a section struct field that is part of the configuration (see
|
||||
// fieldsOf).
|
||||
type field struct {
|
||||
key string
|
||||
index []int // path for reflect.Value.FieldByIndex
|
||||
@@ -136,16 +164,15 @@ func mapOf(v any) map[string]any {
|
||||
return mapOfValue(structValue(v))
|
||||
}
|
||||
|
||||
// mapOfValue is mapOf for a section struct value.
|
||||
func mapOfValue(section reflect.Value) map[string]any {
|
||||
m := map[string]any{}
|
||||
for _, f := range fieldsOf(section.Type()) {
|
||||
fv := section.FieldByIndex(f.index)
|
||||
if f.section {
|
||||
m[f.key] = mapOfValue(fv)
|
||||
} else if f.typ == durationType {
|
||||
m[f.key] = fv.Interface().(time.Duration).String()
|
||||
} else {
|
||||
m[f.key] = fv.Interface()
|
||||
m[f.key] = exportValue(fv)
|
||||
}
|
||||
}
|
||||
return m
|
||||
@@ -159,6 +186,8 @@ func flattenOf(prefix string, v any) map[string]any {
|
||||
return m
|
||||
}
|
||||
|
||||
// flattenInto adds section's keys to m, dotted and prefixed with prefix (if
|
||||
// non-empty), descending into nested sections.
|
||||
func flattenInto(m map[string]any, prefix string, section reflect.Value) {
|
||||
for _, f := range fieldsOf(section.Type()) {
|
||||
fv := section.FieldByIndex(f.index)
|
||||
@@ -166,11 +195,21 @@ func flattenInto(m map[string]any, prefix string, section reflect.Value) {
|
||||
if f.section {
|
||||
flattenInto(m, key, fv)
|
||||
} else {
|
||||
m[key] = fv.Interface()
|
||||
m[key] = exportValue(fv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// exportValue returns a field's value as Map, Flatten and ValueOf give it
|
||||
// out: durations become strings such as "1m30s", so values agree with kindOf
|
||||
// and can be passed back to SetValue; everything else is unchanged.
|
||||
func exportValue(fv reflect.Value) any {
|
||||
if fv.Type() == durationType {
|
||||
return time.Duration(fv.Int()).String()
|
||||
}
|
||||
return fv.Interface()
|
||||
}
|
||||
|
||||
// kindOf is the kind reported by TypeOf, i.e. how the value is read from
|
||||
// Viper. Durations are read as strings and parsed by SetValue.
|
||||
func kindOf(t reflect.Type) reflect.Kind {
|
||||
@@ -180,6 +219,8 @@ func kindOf(t reflect.Type) reflect.Kind {
|
||||
return t.Kind()
|
||||
}
|
||||
|
||||
// typeOf returns the kind of the dotted key in section v (see kindOf), or
|
||||
// errUnknownKey.
|
||||
func typeOf(v any, key string) (reflect.Kind, error) {
|
||||
_, f, err := lookup(structValue(v), key)
|
||||
if err != nil {
|
||||
@@ -188,12 +229,13 @@ func typeOf(v any, key string) (reflect.Kind, error) {
|
||||
return kindOf(f.typ), nil
|
||||
}
|
||||
|
||||
// valueOf returns the value of the dotted key in section v, or errUnknownKey.
|
||||
func valueOf(v any, key string) (any, error) {
|
||||
section, f, err := lookup(structValue(v), key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return section.FieldByIndex(f.index).Interface(), nil
|
||||
return exportValue(section.FieldByIndex(f.index)), nil
|
||||
}
|
||||
|
||||
// setValue sets a dotted key on the section that ptr points to.
|
||||
|
||||
@@ -33,7 +33,7 @@ type testSection struct {
|
||||
NoTag string
|
||||
Skipped string `cfg:"-"`
|
||||
Empty string `cfg:""`
|
||||
unexport string `cfg:"unexported"`
|
||||
unexport string `cfg:"unexported"` //nolint:unused // checks unexported fields are ignored
|
||||
}
|
||||
|
||||
type testValidated struct {
|
||||
@@ -85,6 +85,9 @@ func TestMapOf(t *testing.T) {
|
||||
if got := m["on"]; got != true {
|
||||
t.Errorf("m[on] (promoted) = %#v", got)
|
||||
}
|
||||
if got := m["dur"]; got != "1m30s" {
|
||||
t.Errorf("m[dur] = %#v; want the duration as a string", got)
|
||||
}
|
||||
inner, ok := m["inner"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("m[inner] = %#v; want nested map", m["inner"])
|
||||
@@ -102,12 +105,15 @@ func TestMapOf(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFlattenOfPrefix(t *testing.T) {
|
||||
m := flattenOf("p", testSection{})
|
||||
m := flattenOf("p", testSection{Dur: 90 * time.Second})
|
||||
for _, k := range []string{"p.str", "p.on", "p.inner.name"} {
|
||||
if _, ok := m[k]; !ok {
|
||||
t.Errorf("flattenOf missing %q in %v", k, m)
|
||||
}
|
||||
}
|
||||
if got := m["p.dur"]; got != "1m30s" {
|
||||
t.Errorf("m[p.dur] = %#v; want the duration as a string", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructValuePanics(t *testing.T) {
|
||||
@@ -220,12 +226,12 @@ func TestSetValue(t *testing.T) {
|
||||
{"uint16", "65535", uint16(65535)},
|
||||
{"float", "2.25", 2.25},
|
||||
{"float32", "0.5", float32(0.5)},
|
||||
{"dur", "1h30m", 90 * time.Minute},
|
||||
{"dur", "1h30m", "1h30m0s"}, // durations are given out as strings
|
||||
{"inner.name", "nested", "nested"},
|
||||
// values of the field's type are used as-is
|
||||
{"int", 5, 5},
|
||||
{"on", true, true},
|
||||
{"dur", 3 * time.Second, 3 * time.Second},
|
||||
{"dur", 3 * time.Second, "3s"},
|
||||
{"slice", []string{"a", "b"}, []string{"a", "b"}},
|
||||
{"when", time.Unix(0, 0).UTC(), time.Unix(0, 0).UTC()},
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user