Scaffolds the server module and everything Phase 0 of PLAN.md calls for: a binary that boots, serves its discovery endpoints and nothing else yet. - config: one YAML file plus per-field WARP_* environment overrides, unknown fields rejected, validation that reports every problem at once - logging and metrics: slog, a Prometheus registry, per-route HTTP instrumentation labelled by pattern rather than path - httputil: the Matrix error envelope, error-returning handlers, CORS, request logging, panic recovery, and a router over http.ServeMux - endpoints: /_matrix/client/versions, /.well-known/matrix/client, /.well-known/matrix/server (gated), /healthz, /metrics, and M_UNRECOGNIZED for everything else - storage: driver registry, shared database/sql base, checksummed migration runner, sqlite and postgres drivers, pocketbase stub - settings: server_settings table behind a cached service, so the federation and registration switches change without a restart - federation: the gate that makes federation a runtime mode - cmd/warp: serve, migrate, generate-keys, version - Makefile, golangci-lint v2, GitHub Actions CI (Linux and Windows tests, race on Linux, storage tests against PostgreSQL, lint, boot smoke test), Dockerfile, example config and docs Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
2.5 KiB
Go
106 lines
2.5 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// durationType is compared against to tell a Duration from a plain int64.
|
|
var durationType = reflect.TypeOf(time.Duration(0))
|
|
|
|
// ApplyEnv overwrites every field carrying an `env` tag whose variable is set
|
|
// to a non-empty value. lookup is usually os.Getenv; tests pass a fake.
|
|
//
|
|
// Supported kinds: string, bool, all ints, time.Duration and []string
|
|
// (comma-separated, whitespace trimmed, empty elements dropped).
|
|
func ApplyEnv(cfg *Config, lookup func(string) string) error {
|
|
return applyEnv(reflect.ValueOf(cfg).Elem(), lookup)
|
|
}
|
|
|
|
func applyEnv(v reflect.Value, lookup func(string) string) error {
|
|
t := v.Type()
|
|
for i := range t.NumField() {
|
|
field := t.Field(i)
|
|
if !field.IsExported() {
|
|
continue
|
|
}
|
|
fv := v.Field(i)
|
|
|
|
if fv.Kind() == reflect.Struct && field.Type != durationType {
|
|
if err := applyEnv(fv, lookup); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
name := field.Tag.Get("env")
|
|
if name == "" {
|
|
continue
|
|
}
|
|
raw := strings.TrimSpace(lookup(name))
|
|
if raw == "" {
|
|
continue
|
|
}
|
|
if err := setFromString(fv, raw); err != nil {
|
|
return fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func setFromString(fv reflect.Value, raw string) error {
|
|
if fv.Type() == durationType {
|
|
d, err := time.ParseDuration(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid duration %q", raw)
|
|
}
|
|
fv.SetInt(int64(d))
|
|
return nil
|
|
}
|
|
|
|
switch fv.Kind() {
|
|
case reflect.String:
|
|
fv.SetString(raw)
|
|
case reflect.Bool:
|
|
b, err := strconv.ParseBool(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid boolean %q (want true or false)", raw)
|
|
}
|
|
fv.SetBool(b)
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
n, err := strconv.ParseInt(raw, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid integer %q", raw)
|
|
}
|
|
fv.SetInt(n)
|
|
case reflect.Slice:
|
|
if fv.Type().Elem().Kind() != reflect.String {
|
|
return fmt.Errorf("unsupported slice type %s", fv.Type())
|
|
}
|
|
fv.Set(reflect.ValueOf(splitList(raw)))
|
|
default:
|
|
return fmt.Errorf("unsupported field type %s", fv.Type())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// splitList parses a comma-separated env value. A lone "-" clears the list,
|
|
// which is the only way to turn a YAML list back into an empty one from the
|
|
// environment.
|
|
func splitList(raw string) []string {
|
|
if raw == "-" {
|
|
return []string{}
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
if p = strings.TrimSpace(p); p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|