From c9e93802ebdcbce184137e66b77c8133040da9f4 Mon Sep 17 00:00:00 2001 From: "Arthur K." Date: Sun, 1 Feb 2026 13:14:34 +0300 Subject: [PATCH] chore: use go generate to create Validate and Merge funcs on RoleConfigs --- .gitignore | 1 + Makefile | 10 +- go.mod | 7 +- go.sum | 12 +- internal/codegen/roleconfig/main.go | 196 ++++++++++++++++++++++++++++ internal/config/dns.go | 18 +-- internal/config/host.go | 72 +--------- internal/config/master.go | 54 +------- internal/config/node.go | 64 +-------- internal/config/role.go | 2 +- 10 files changed, 236 insertions(+), 200 deletions(-) create mode 100644 internal/codegen/roleconfig/main.go diff --git a/.gitignore b/.gitignore index 84c048a..42014fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /build/ +*_gen.go diff --git a/Makefile b/Makefile index f8232c0..a18b22c 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,15 @@ - all: hivemind hivemind-musl -hivemind: +codegen: + go generate ./... + +hivemind: codegen go build -o build/hivemind ./cmd/hivemind -hivemind-musl: +hivemind-musl: codegen CC=musl-gcc go build \ -ldflags="-linkmode external -extldflags '-static'" \ -o build/hivemind-musl \ ./cmd/hivemind -.phony: all hivemind hivemind-musl +.phony: all codegen hivemind hivemind-musl diff --git a/go.mod b/go.mod index 53b5b0a..81f11a3 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,16 @@ module git.wzray.com/homelab/hivemind go 1.25.5 require ( - github.com/rs/zerolog v1.34.0 github.com/BurntSushi/toml v1.6.0 + github.com/rs/zerolog v1.34.0 + golang.org/x/tools v0.41.0 ) require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/errors v0.9.1 // indirect - golang.org/x/sys v0.39.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect ) diff --git a/go.sum b/go.sum index 8a3e287..a04e00f 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -14,8 +16,14 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= diff --git a/internal/codegen/roleconfig/main.go b/internal/codegen/roleconfig/main.go new file mode 100644 index 0000000..5d1a485 --- /dev/null +++ b/internal/codegen/roleconfig/main.go @@ -0,0 +1,196 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "go/ast" + "go/token" + "go/types" + "io" + "os" + "reflect" + "strings" + + "golang.org/x/tools/go/packages" + "golang.org/x/tools/imports" +) + +const ( + defaultMerge = ` + if s.%[1]s != %[2]s { + s.%[1]s = other.%[1]s + } + ` + defaultValidator = ` + if s.%[1]s == %[2]s { + return errors.New("missing %[3]s") + } + ` + ipValidator = ` + if net.ParseIP(s.%[1]s) == nil { + return fmt.Errorf("invalid %[3]s: %%q", s.%[1]s) + } + ` + positiveValidator = ` + if s.%[1]s < 1 { + return fmt.Errorf("invalid %[3]s: %%q", s.%[1]s) + } + ` +) + +var ( + structName string + packageName string + fileName string +) + +type Validator struct { + code string +} + +type Field struct { + name string + prettyName string + defaultValue string + validators []string +} + +func NewField(name, prettyName, defaultValue, tag string) *Field { + validators := make([]string, 0, 2) + + switch tag { + case "": + validators = append(validators, defaultValidator) + case "ip": + validators = append(validators, defaultValidator, ipValidator) + case "positive": + validators = append(validators, positiveValidator) + case "no": + break + default: + panic(fmt.Sprintf("invalid tag %v", tag)) + } + + return &Field{ + name: name, + prettyName: prettyName, + defaultValue: defaultValue, + validators: validators, + } +} + +func init() { + namePtr := flag.String("name", "", "name of the struct") + flag.Parse() + if namePtr == nil || *namePtr == "" { + fmt.Fprintln(os.Stderr, "Invalid name") + os.Exit(1) + } + structName = *namePtr + + packageName = os.Getenv("GOPACKAGE") + + fileName = fmt.Sprintf("%s_gen.go", strings.TrimSuffix(os.Getenv("GOFILE"), ".go")) +} + +func defaultValue(t types.Type) string { + b, ok := t.Underlying().(*types.Basic) + if !ok { + return "nil" + } + + switch b.Kind() { + case types.Int, types.Int8, types.Int16, types.Int32, types.Int64, + types.Uint, types.Uint8, types.Uint16, types.Uint32, types.Uint64: + return "0" + case types.String: + return `""` + case types.Bool: + return "false" + default: + return "nil" + } +} + +type Package struct { + fields []*Field + imports map[string]struct{} +} + +func enumerateFields(root *types.Struct) []*Field { + fields := make([]*Field, 0, 16) + + for i := range root.NumFields() { + field := root.Field(i) + tag := reflect.StructTag(root.Tag(i)) + + extraValidators := tag.Get("gen") + prettyName := tag.Get("toml") + if embed, ok := field.Type().Underlying().(*types.Struct); ok { + fields = append(fields, enumerateFields(embed)...) + } else { + fields = append(fields, NewField(field.Name(), prettyName, defaultValue(field.Type()), extraValidators)) + } + } + + return fields +} + +func writeMerge(w io.Writer, name string, fields []*Field) { + fmt.Fprintf(w, "func (s *%[1]s) Merge(other %[1]s) {", name) + for _, f := range fields { + fmt.Fprintf(w, defaultMerge, f.name, f.defaultValue) + } + fmt.Fprintf(w, "}\n\n") +} + +func writeValidators(w io.Writer, name string, fields []*Field) { + fmt.Fprintf(w, "func (s %s) Validate() error {", name) + for _, f := range fields { + for _, v := range f.validators { + fmt.Fprintf(w, v, f.name, f.defaultValue, f.prettyName) + } + } + fmt.Fprintf(w, "\nreturn nil\n}\n\n") +} + +func main() { + cfg := &packages.Config{ + Mode: packages.NeedName | + packages.NeedTypes | + packages.NeedTypesInfo | + packages.NeedSyntax | + packages.NeedFiles, + } + pkgs, _ := packages.Load(cfg, ".") + config := *pkgs[0] + for _, v := range config.Syntax { + ast.Inspect(v, func(node ast.Node) bool { + decl, ok := node.(*ast.GenDecl) + if !ok || decl.Tok != token.TYPE { + return true + } + + for _, spec := range decl.Specs { + ts := spec.(*ast.TypeSpec) + if ts.Name.String() != structName { + continue + } + + t := config.TypesInfo.TypeOf(ts.Type.(*ast.StructType)).(*types.Struct) + fields := enumerateFields(t) + + buf := bytes.NewBuffer(make([]byte, 0)) + fmt.Fprintf(buf, "// Code generated by roleconfig; DO NOT EDIT.\n\npackage %s\n", packageName) + writeValidators(buf, structName, fields) + writeMerge(buf, structName, fields) + x, _ := imports.Process(fileName, buf.Bytes(), nil) + os.WriteFile(fileName, x, 0644) + + break + } + return false + }) + } +} diff --git a/internal/config/dns.go b/internal/config/dns.go index 88bc735..664dcbc 100644 --- a/internal/config/dns.go +++ b/internal/config/dns.go @@ -1,20 +1,8 @@ package config +//go:generate -command roleconfig go run git.wzray.com/homelab/hivemind/internal/codegen/roleconfig +//go:generate roleconfig -name DnsConfig type DnsConfig struct { - UseSystemd bool `toml:"use_systemd"` + UseSystemd bool `toml:"use_systemd" gen:"no"` baseRoleConfig } - -func (c DnsConfig) Validate() error { - return nil -} - -func (c *DnsConfig) Merge(other DnsConfig) { - if other.set { - c.set = other.set - } - - if other.UseSystemd { - c.UseSystemd = other.UseSystemd - } -} diff --git a/internal/config/host.go b/internal/config/host.go index 9a302ef..ac7aeb7 100644 --- a/internal/config/host.go +++ b/internal/config/host.go @@ -1,79 +1,13 @@ package config -import ( - "errors" - "fmt" - "net" -) - +//go:generate -command roleconfig go run git.wzray.com/homelab/hivemind/internal/codegen/roleconfig +//go:generate roleconfig -name HostConfig type HostConfig struct { Domain string `toml:"domain"` - IpAddress string `toml:"ip"` + IpAddress string `toml:"ip" gen:"ip"` LocalAddress string `toml:"local_address"` InternalEntrypoint string `toml:"internal_entrypoint"` ExternalEntrypoint string `toml:"external_entrypoint"` ListenAddress string `toml:"listen_address"` baseRoleConfig } - -func (c HostConfig) Validate() error { - if c.Domain == "" { - return errors.New("missing domain") - } - - if c.IpAddress == "" { - return errors.New("missing ip") - } - - if net.ParseIP(c.IpAddress) == nil { - return fmt.Errorf("invalid ip: %q", c.IpAddress) - } - - if c.LocalAddress == "" { - return errors.New("missing local_address") - } - - if c.InternalEntrypoint == "" { - return errors.New("missing internal_entrypoint") - } - - if c.ExternalEntrypoint == "" { - return errors.New("missing external_entrypoint") - } - - if c.ListenAddress == "" { - return errors.New("missing listen_address") - } - - return nil -} - -func (c *HostConfig) Merge(other HostConfig) { - if other.set { - c.set = other.set - } - - if other.Domain != "" { - c.Domain = other.Domain - } - - if other.IpAddress != "" { - c.IpAddress = other.IpAddress - } - - if other.LocalAddress != "" { - c.LocalAddress = other.LocalAddress - } - - if other.InternalEntrypoint != "" { - c.InternalEntrypoint = other.InternalEntrypoint - } - - if other.ExternalEntrypoint != "" { - c.ExternalEntrypoint = other.ExternalEntrypoint - } - - if other.ListenAddress != "" { - c.ListenAddress = other.ListenAddress - } -} diff --git a/internal/config/master.go b/internal/config/master.go index fb4b266..09d7cf3 100644 --- a/internal/config/master.go +++ b/internal/config/master.go @@ -1,54 +1,12 @@ package config -import "errors" - +//go:generate -command roleconfig go run git.wzray.com/homelab/hivemind/internal/codegen/roleconfig +//go:generate roleconfig -name MasterConfig type MasterConfig struct { - ObserverInterval int `toml:"observer_interval"` - BackoffSeconds int `toml:"backoff_seconds"` - BackoffCount int `toml:"backoff_count"` - NodeTimeout int `toml:"node_timeout"` + ObserverInterval int `toml:"observer_interval" gen:"positive"` + BackoffSeconds int `toml:"backoff_seconds" gen:"positive"` + BackoffCount int `toml:"backoff_count" gen:"positive"` + NodeTimeout int `toml:"node_timeout" gen:"positive"` baseRoleConfig } - -func (c MasterConfig) Validate() error { - if c.ObserverInterval < 1 { - return errors.New("invalid observer_interval") - } - - if c.BackoffSeconds < 1 { - return errors.New("invalid backoff_seconds") - } - - if c.BackoffCount < 1 { - return errors.New("invalid backoff_count") - } - - if c.NodeTimeout < 1 { - return errors.New("invalid node_timeout") - } - - return nil -} - -func (c *MasterConfig) Merge(other MasterConfig) { - if other.set { - c.set = true - } - - if other.ObserverInterval != 0 { - c.ObserverInterval = other.ObserverInterval - } - - if other.BackoffSeconds != 0 { - c.BackoffSeconds = other.BackoffSeconds - } - - if other.BackoffCount != 0 { - c.BackoffCount = other.BackoffCount - } - - if other.NodeTimeout != 0 { - c.NodeTimeout = other.NodeTimeout - } -} diff --git a/internal/config/node.go b/internal/config/node.go index 0ee3da0..318742b 100644 --- a/internal/config/node.go +++ b/internal/config/node.go @@ -1,9 +1,7 @@ package config import ( - "errors" "fmt" - "net" "strings" ) @@ -28,68 +26,16 @@ func (l *LogLevel) UnmarshalText(data []byte) error { } } +//go:generate -command roleconfig go run git.wzray.com/homelab/hivemind/internal/codegen/roleconfig +//go:generate roleconfig -name NodeConfig type NodeConfig struct { Hostname string `toml:"hostname"` Address string `toml:"address"` Port int `toml:"port"` - KeepaliveInterval int `toml:"keepalive_interval"` + KeepaliveInterval int `toml:"keepalive_interval" gen:"positive"` LogLevel LogLevel `toml:"log_level"` - BootstrapMaster string `toml:"bootstrap_master"` - ListenOn string `toml:"listen_on"` -} - -func (c NodeConfig) Validate() error { - if c.Address == "" { - return errors.New("missing address") - } - - if c.Hostname == "" { - return errors.New("missing hostname") - } - - if c.KeepaliveInterval < 1 { - return errors.New("invalid keepalive_interval") - } - - if c.ListenOn == "" { - return errors.New("missing listen_on") - } - - if net.ParseIP(c.ListenOn) == nil { - return fmt.Errorf("invalid listen_on: %v", c.ListenOn) - } - - return nil -} - -func (c *NodeConfig) Merge(other NodeConfig) { - if other.Hostname != "" { - c.Hostname = other.Hostname - } - - if other.Address != "" { - c.Address = other.Address - } - - if other.BootstrapMaster != "" { - c.BootstrapMaster = other.BootstrapMaster - } - - if other.ListenOn != "" { - c.ListenOn = other.ListenOn - } - - if other.Port != 0 { - c.Port = other.Port - } - - if other.KeepaliveInterval != 0 { - c.KeepaliveInterval = other.KeepaliveInterval - } - - if other.LogLevel != "" { - c.LogLevel = other.LogLevel - } + BootstrapMaster string `toml:"bootstrap_master" gen:"no"` + ListenOn string `toml:"listen_on" gen:"ip"` } diff --git a/internal/config/role.go b/internal/config/role.go index 4d7069d..7928542 100644 --- a/internal/config/role.go +++ b/internal/config/role.go @@ -1,5 +1,5 @@ package config type baseRoleConfig struct { - set bool + set bool `gen:"no"` }