Files
clawbot 16649e0f2f
check / check (push) Successful in 54s
A SimpleX Chat bot that answers arithmetic (closes #1)
Remove the template's HTTP service, database and fx wiring. Add exact
arithmetic on go/parser and go/constant, a client that runs simplex-chat
as a child process and drives its WebSocket API, and the bot, which keeps
an auto-accepting address and replies to each message. The image adds the
checksum-pinned simplex-chat v7.0.2 on Ubuntu 22.04.

Model: opus-5-5
2026-09-26 22:01:51 +00:00

138 lines
3.9 KiB
Go

// Package config loads runtime configuration from the environment (and
// an optional ./.env file) via viper.
//
// The iron rule of this package: a value that is SET but cannot be
// parsed aborts startup. It is never replaced by the default. An
// operator who writes DEBUG=yes has said something specific that this
// program does not understand, and starting anyway with the default
// turns their mistake into a silent misconfiguration that only
// surfaces much later, somewhere else. Defaults apply to values that
// are ABSENT, and to nothing else.
//
// Every parse failure found in one pass is reported together, so a
// broken deployment takes one restart to diagnose rather than several.
package config
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/spf13/viper"
// spooky action at a distance!
// this populates the environment
// from a ./.env file automatically
// for development configuration.
// .env contents should be things like
// `DEBUG=true`
// (without the backticks, of course)
_ "github.com/joho/godotenv/autoload"
)
// Environment variable names. Bare names, no prefix: this matches the
// other services and keeps a compose file readable.
const (
EnvDataDir = "DATA_DIR"
EnvDebug = "DEBUG"
)
// DefaultDataDir applies when DATA_DIR is absent.
const DefaultDataDir = "./data"
// ErrInvalidConfig is the sentinel every configuration failure wraps,
// so callers can distinguish "the operator got it wrong" from "the
// machine is broken" without string matching.
var ErrInvalidConfig = errors.New("invalid configuration")
// Config is the parsed, validated runtime configuration. Every field
// is final by the time New returns: nothing re-reads the environment
// later, so there is exactly one moment at which configuration can be
// wrong, and it is before anything starts.
type Config struct {
// DataDir holds the SimpleX Chat database: the bot's profile, its
// address and its contacts. Losing it loses the address.
DataDir string
Debug bool
}
// loader parses one environment into a Config, accumulating every
// failure instead of stopping at the first, so one restart surfaces the
// whole list.
type loader struct {
v *viper.Viper
errs []error
}
func (l *loader) fail(key, raw, why string) {
l.errs = append(l.errs, fmt.Errorf(
"%w: %s=%q is %s", ErrInvalidConfig, key, raw, why,
))
}
// raw returns the trimmed value of key, and whether it was set to
// anything. Whitespace-only counts as absent: it is what an empty
// compose-file entry produces, and no key here has a meaningful blank
// value.
func (l *loader) raw(key string) (string, bool) {
s := strings.TrimSpace(l.v.GetString(key))
return s, s != ""
}
func (l *loader) str(key, def string) string {
if s, ok := l.raw(key); ok {
return s
}
return def
}
// boolean accepts what strconv.ParseBool accepts (1/t/T/TRUE/true/True
// and the false equivalents) and refuses everything else. "yes" is a
// parse failure on purpose: guessing at it is how a setting ends up the
// opposite of what the operator meant.
func (l *loader) boolean(key string, def bool) bool {
s, ok := l.raw(key)
if !ok {
return def
}
b, err := strconv.ParseBool(s)
if err != nil {
l.fail(key, s, "not a boolean (use true or false)")
return def
}
return b
}
// New parses and validates the environment. An error here aborts
// startup before the chat client is launched, so there is no partially
// configured running state to reason about.
func New() (*Config, error) {
v := viper.New()
v.AutomaticEnv()
return load(v)
}
// load is New's body against an explicit viper instance, so tests can
// drive it with a known environment instead of mutating the process's.
func load(v *viper.Viper) (*Config, error) {
l := &loader{v: v}
c := &Config{
DataDir: l.str(EnvDataDir, DefaultDataDir),
Debug: l.boolean(EnvDebug, false),
}
if len(l.errs) > 0 {
return nil, errors.Join(l.errs...)
}
return c, nil
}