Files
job-search/internal/config/modify.go
T
bwilliams c9f42b62ad Added websocket hub for handling websocket clients, with a wsserve command
which runs a plain TCP server for receiving websocket hub clients, with
server config, CSRF and rate limit middleware.

- internal/websocket: Hub pushing messages to WebSocket clients (ServeHTTP,
  example.com/ws upgrade with optional CSRF check) and plain TCP clients (ServeTCP,
  newline-delimited JSON envelopes); ping/pong keepalive, per-client message
  rate limit, slow consumer handling, graceful Shutdown, debug logging of
  messages sent and received
- internal/websocket/tcpserver: standalone TCP server for a hub, with accept
  back-off, a per-IP connection limit (server.websocket.connection_limit) and
  graceful shutdown
- internal/cli/wsserve: `wsserve` command running the TCP server, with
  --listen/--host/--port overrides
- internal/middleware: keyed token bucket rate limiter, real client IP from
  trusted proxies
- internal/httpx: RFC 9457 problem responses, request IDs in context
- internal/config: server section (host, port, shutdown timeout, rate limiter
  idle TTL, websocket); util.ByteSize settings (e.g. "32KiB"); lists of
  sections as single keys; empty lists stored as nil; isExplicitlySet for
  printLoadedConfig; JobSearchConfigSource renamed to JSCSourcesSource;
  TestSections checks every section's methods
- internal/util: ByteSize, parsed and formatted with go-humanize
2026-10-14 15:55:25 +01:00

118 lines
3.3 KiB
Go

package config
import (
"errors"
"fmt"
"reflect"
"strconv"
"github.com/spf13/viper"
)
// StoreValue parses value as the type of key, checks conf accepts it, sets it
// in conf and Viper and writes it to the config file Viper is using. Only the
// file's own settings are written back, not defaults or environment
// variables. It returns the stored value (as ValueOf gives it) and its kind.
func StoreValue(
conf *JobSearchConfig, key string, value string,
) (any, reflect.Kind, error) {
return storeValue(conf, key, value)
}
// storeValue is StoreValue for any section, so key types that
// JobSearchConfig doesn't have yet can be tested.
func storeValue(s Section, key string, value string) (any, reflect.Kind, error) {
confType, err := s.TypeOf(key)
if err != nil {
return nil, confType, err
}
// Check the value is of the right kind first, for a clearer error than
// SetValue gives.
if _, err := parseValue(confType, value); err != nil {
return nil, confType, fmt.Errorf(
"config value type not accepted, expected <%s>", confType,
)
}
// SetValue parses the string into the field's own type (e.g. int8,
// float32 or time.Duration) and rejects values the configuration itself
// would not accept, e.g. an unknown log level or an unparseable duration.
if err := s.SetValue(key, value); err != nil {
return nil, confType, err
}
confValue, err := s.ValueOf(key)
if err != nil {
return nil, confType, err
}
viper.Set(key, confValue)
file, err := readUsedConfigFile()
if err != nil {
return nil, confType, err
}
file.Set(key, confValue)
if err := file.WriteConfig(); err != nil {
return nil, confType, err
}
return confValue, confType, nil
}
// parseValue parses value as a value of kind, returning
// errors.ErrUnsupported for a kind it can't parse.
func parseValue(kind reflect.Kind, value string) (any, error) {
switch kind {
case reflect.Bool:
return strconv.ParseBool(value)
case reflect.String:
return value, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.ParseInt(value, 10, 64)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return strconv.ParseUint(value, 10, 64)
case reflect.Float32, reflect.Float64:
return strconv.ParseFloat(value, 64)
case reflect.Slice:
return splitList(value), nil
}
return nil, errors.ErrUnsupported
}
// PurgeValue rewrites the config file Viper is using without key. The
// file's other settings are kept as they are; defaults and environment
// variables are not written to it.
func PurgeValue(conf *JobSearchConfig, key string) error {
if _, err := conf.TypeOf(key); err != nil {
return err
}
file, err := readUsedConfigFile()
if err != nil {
return err
}
out := viper.New()
out.SetConfigFile(file.ConfigFileUsed())
for _, k := range file.AllKeys() {
if k != key {
out.Set(k, file.Get(k))
}
}
return out.WriteConfig()
}
// readUsedConfigFile reads the config file Viper is using into a new Viper, so
// it holds only the file's settings.
func readUsedConfigFile() (*viper.Viper, error) {
path := viper.ConfigFileUsed()
if path == "" {
return nil, errors.New("no configuration file in use")
}
file := viper.New()
file.SetConfigFile(path)
if err := file.ReadInConfig(); err != nil {
return nil, err
}
return file, nil
}