summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Carr <[email protected]>2025-10-20 05:50:38 -0500
committerJeff Carr <[email protected]>2025-10-20 05:50:38 -0500
commit8a24584262a90956e012cee7b03c8c2b9f4b794c (patch)
tree8b7d0afd04e717d7c07da5eb2894debd74ec2307
parentb6e93c08d601a7a6c27a0fdcdf98f6cb7dc9ccd8 (diff)
reworking this to make it more sane. hopefully.
-rw-r--r--Makefile6
-rw-r--r--README.md63
-rw-r--r--autogenpbLookup.go (renamed from lookupPB.go)23
-rw-r--r--autogenpbName.go111
-rw-r--r--config.Load.go81
-rw-r--r--config.Panic.go37
-rw-r--r--config.Save.go35
-rw-r--r--config.proto19
-rw-r--r--doc.go18
-rw-r--r--init.go1
-rw-r--r--loadConfig.go59
-rw-r--r--loadRaw.go (renamed from load.go)13
-rw-r--r--saveRaw.go (renamed from save.go)0
13 files changed, 411 insertions, 55 deletions
diff --git a/Makefile b/Makefile
index 54f5b85..1013fb0 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-all: config.pb.go goimports vet
+all: autogenpb goimports vet
@echo This GO code passes the compile checks
# fixes your numbers if you move things around
@@ -9,8 +9,8 @@ proto-renumber: clean
autogenpb --renumber --proto config.proto
make goimports vet
-config.pb.go: config.proto
- make generate
+autogenpb:
+ autogenpb --proto config.proto
generate: clean
go mod init
diff --git a/README.md b/README.md
index 67dbb17..8529fb0 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,51 @@
-// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
+* Theory of config
-common config file handling for protobuf defined config files
-intended to be super simple so the code you need to write is simple.
+ * Make the defaults the same everywhere
-Enables Load functions:
+ * There are two types of config files:
-// loads ~/.config/myapp/trees.text
-cfg := new(MyPB)
-err := config.ConfigLoad(cfg, "myapp", "trees")
+ 1) A super simple ENV key:value file (config.proto defined in this package)
+ 2) A application defined <appname>.proto (.proto wherever you want to put it)
-Enables Save functions:
-err := cfg.Save() // it automatically knows where to save
+* 1) Basic application config.proto information
-### Errors ####
+ the application config.proto is loaded from, in order:
-if errors.Is(err, config.VersionMismatch) {
- // protobuf structure changed
-}
-if errors.Is(err, config.ErrEmpty) {
- // config file was empty
-}
-if errors.Is(err, config.ErrNotExist) {
- // config file didn't exist (yes, this is the os.ExistErr)
-}
+ /usr/share/doc/appname/config.text
+ /etc/default/appname
+ ~/.config/appname/config.text
+
+ These files ONLY HAVE ENV LIKE KEY/VALUE Pairs
+
+* 2) application specific protobuf files
+
+ Application config files are always located in the same place
+ with the same name by default.
+
+ For the application "forge" using forge.proto
+
+ ~/.config/forge/forge.text
+
+ To override these defaults, set the values in the 1) protobuf
+
+* notes from the code
+
+```
+// Because you followed autogenpb's advice (you did follow it right?) you now
+// win the automation lottery.
+//
+// for this .proto file, GetProtobufName(pb) returns "repos"
+// Then, ConfigLoad(), ConfigSave(), CacheLoad() and CacheSave()
+// all do exactly what is expected:
+//
+// Automatically work with the files:
+// ~/.config/<appname>/repos.pb
+// or
+// ~/.cache/<appname/repos.pb
+//
+// message Repos {
+// string uuid = 1;
+// string version = 2;
+// repeated Repo repos = 3;
+```
diff --git a/lookupPB.go b/autogenpbLookup.go
index 9fb73a2..910b1b4 100644
--- a/lookupPB.go
+++ b/autogenpbLookup.go
@@ -66,7 +66,7 @@ func GetFilename(pb proto.Message) (string, error) {
fieldName = protoreflect.Name("filename")
fieldDescriptor = descriptor.Fields().ByName(fieldName)
// if fieldDescriptor == nil {
- // panic(".proto file: try 'Filename', not 'filename'? or maybe nomutex if pb.Marshal() fails")
+ // die(".proto file: try 'Filename', not 'filename'? or maybe nomutex if pb.Marshal() fails")
// }
}
@@ -163,3 +163,24 @@ func printStrings(pb proto.Message) {
}
}
}
+
+// GetProtoFile returns the name of the .proto file that defined the message.
+func GetProtoFile(pb proto.Message) (string, error) {
+ // 1. Get the protoreflect.Message interface from the message.
+ msg := pb.ProtoReflect()
+
+ // 2. Get the message's descriptor.
+ descriptor := msg.Descriptor()
+
+ // 3. Get the descriptor for the file that contains this message.
+ fileDescriptor := descriptor.ParentFile()
+ if fileDescriptor == nil {
+ return "", fmt.Errorf("could not get file descriptor from message")
+ }
+
+ // 4. Get the path (filename) from the file descriptor.
+ // This is the value you're looking for, e.g., "repo.proto".
+ filename := fileDescriptor.Path()
+
+ return filename, nil
+}
diff --git a/autogenpbName.go b/autogenpbName.go
new file mode 100644
index 0000000..f2e492c
--- /dev/null
+++ b/autogenpbName.go
@@ -0,0 +1,111 @@
+package config
+
+import (
+ "fmt"
+
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protoreflect"
+)
+
+// Because you followed autogenpb's advice (you did follow it right?) you now
+// win the automation lottery.
+//
+// for this .proto file, GetProtobufName(pb) returns "repos"
+// Then, ConfigLoad(), ConfigSave(), CacheLoad() and CacheSave()
+// all do exactly what is expected:
+//
+// Automatically work with the files:
+//
+// ~/.config/<appname>/repos.pb
+//
+// or
+//
+// ~/.cache/<appname/repos.pb
+//
+// message Repos {
+// string uuid = 1;
+// string version = 2;
+// repeated Repo repos = 3;
+func GetProtobufName(pb proto.Message) (string, error) {
+ // 1. Get the protoreflect.Message interface.
+ msg := pb.ProtoReflect()
+
+ // 2. Get the message's descriptor.
+ descriptor := msg.Descriptor()
+
+ // 3. Get all the field descriptors for the message.
+ fields := descriptor.Fields()
+
+ // find string "uuid"
+ fieldNumber := protoreflect.FieldNumber(1)
+ fieldDescriptor := fields.ByNumber(fieldNumber)
+
+ if fieldDescriptor == nil {
+ return "", fmt.Errorf("message %q has no field with number 1", descriptor.FullName())
+ }
+ if fieldDescriptor.Name() != "uuid" {
+ return "", fmt.Errorf("message %q field(1) != 'uuid' is (%s)", descriptor.FullName(), fieldDescriptor.Name())
+ }
+
+ // find string "version"
+ fieldNumber = protoreflect.FieldNumber(2)
+ fieldDescriptor = fields.ByNumber(fieldNumber)
+
+ if fieldDescriptor == nil {
+ return "", fmt.Errorf("message %q has no field(2)", descriptor.FullName())
+ }
+ if fieldDescriptor.Name() != "version" {
+ return "", fmt.Errorf("message %q field(2) != 'version' is (%s)", descriptor.FullName(), fieldDescriptor.Name())
+ }
+
+ // 4. Find the field descriptor specifically by its number (3).
+ // This is the crucial step.
+ fieldNumber = protoreflect.FieldNumber(3)
+ fieldDescriptor = fields.ByNumber(fieldNumber)
+
+ // 5. Add some safety checks.
+ if fieldDescriptor == nil {
+ return "", fmt.Errorf("message %q has no field with number 3", descriptor.FullName())
+ }
+
+ // Optional but recommended: verify it's a repeated field as you expect.
+ // IsList() is the reflection equivalent of `repeated`.
+ if !fieldDescriptor.IsList() {
+ return "", fmt.Errorf("field with number 3 (%q) is not a repeated field", fieldDescriptor.Name())
+ }
+
+ // 6. Get the name from the descriptor and return it.
+ // The name is of type protoreflect.Name, so we cast it to a string.
+ return string(fieldDescriptor.Name()), nil
+}
+
+func GetThirdFieldName(pb proto.Message) (string, error) {
+ // 1. Get the protoreflect.Message interface.
+ msg := pb.ProtoReflect()
+
+ // 2. Get the message's descriptor.
+ descriptor := msg.Descriptor()
+
+ // 3. Get all the field descriptors for the message.
+ fields := descriptor.Fields()
+
+ // 4. Find the field descriptor specifically by its number (3).
+ // This is the crucial step.
+ fieldNumber := protoreflect.FieldNumber(3)
+ fieldDescriptor := fields.ByNumber(fieldNumber)
+
+ // 5. Add some safety checks.
+ if fieldDescriptor == nil {
+ return "", fmt.Errorf("message %q has no field with number 3", descriptor.FullName())
+ }
+
+ // Optional but recommended: verify it's a repeated field as you expect.
+ // IsList() is the reflection equivalent of `repeated`.
+ if !fieldDescriptor.IsList() {
+ return "", fmt.Errorf("field with number 3 (%q) is not a repeated field", fieldDescriptor.Name())
+ }
+
+ // 6. Get the name from the descriptor and return it.
+ // The name is of type protoreflect.Name, so we cast it to a string.
+ return string(fieldDescriptor.Name()), nil
+}
diff --git a/config.Load.go b/config.Load.go
new file mode 100644
index 0000000..b960508
--- /dev/null
+++ b/config.Load.go
@@ -0,0 +1,81 @@
+package config
+
+import (
+ "errors"
+ "os"
+ "os/user"
+ "path/filepath"
+)
+
+// loads your applications config file from
+// ~/.config/<appname>/config.text
+func (pb *Config) Load() error {
+ appname, err := GetAppname() // already configured by your application
+ if err != nil {
+ return err
+ }
+
+ configdir, err := getConfigDir()
+ if err != nil {
+ return err
+ }
+
+ filename := filepath.Join(configdir, appname+".text")
+ _, err = SetFilename(pb, filename)
+ if err != nil {
+ return err
+ }
+
+ saveMu.Lock()
+ defer saveMu.Unlock()
+ err = loadTEXT(pb, filename)
+ return err
+}
+
+func GetAppname() (string, error) {
+ if APPNAME != "" {
+ return APPNAME, nil
+ }
+ return "", errors.New("your application must setup config.Init()")
+}
+
+func GetUsername() string {
+ if Get("username") != "" {
+ return Get("username")
+ }
+ usr, _ := user.Current()
+ if usr.Username != "" {
+ return usr.Username
+ }
+ return "notsure" // OS Idiocracy
+}
+
+func getCacheDir() (string, error) {
+ if Get("cacheDir") != "" {
+ return Get("cacheDir"), nil
+ }
+
+ cacheDir, _ := os.UserCacheDir()
+
+ appname, err := GetAppname() // application should have already configured this
+ if err != nil {
+ return cacheDir, err
+ }
+
+ return filepath.Join(cacheDir, appname), nil
+}
+
+func getConfigDir() (string, error) {
+ if Get("configDir") != "" {
+ return Get("configDir"), nil
+ }
+
+ configDir, _ := os.UserConfigDir()
+
+ appname, err := GetAppname() // application should have already configured this
+ if err != nil {
+ return configDir, err
+ }
+
+ return filepath.Join(configDir, appname), nil
+}
diff --git a/config.Panic.go b/config.Panic.go
new file mode 100644
index 0000000..45d233e
--- /dev/null
+++ b/config.Panic.go
@@ -0,0 +1,37 @@
+package config
+
+import "google.golang.org/protobuf/proto"
+
+func GetPanic(flag string) string {
+ saveMu.Lock()
+ defer saveMu.Unlock()
+ if configPB == nil {
+ configPanic(flag)
+ }
+ found := configPB.FindByKey(flag)
+ if found == nil {
+ configPanic(flag)
+ }
+ return found.Value
+}
+
+func configPanic(varname string) {
+ saveMu.Lock()
+ defer saveMu.Unlock()
+ if configPB == nil {
+ panic("config file is nil")
+ }
+ panic("config name '" + varname + "' not found")
+}
+
+// should this be a function?
+func LoadPanicPB(pb proto.Message) error {
+ fullname, err := GetFilename(pb)
+ if err != nil {
+ panic("config.LoadPB() err")
+ }
+ if fullname == "" {
+ panic("config.LoadPB() got blank filename = ''")
+ }
+ return LoadPB(pb)
+}
diff --git a/config.Save.go b/config.Save.go
index c5aaeb7..1f1736f 100644
--- a/config.Save.go
+++ b/config.Save.go
@@ -3,6 +3,7 @@ package config
import (
"os"
"path/filepath"
+ "strings"
"sync"
)
@@ -38,33 +39,37 @@ func Get(flag string) string {
if configPB == nil {
return ""
}
- found := configPB.FindByKey(flag)
- if found == nil {
+ c := findByLower(flag)
+ if c == nil {
return ""
}
- return found.Value
+
+ return c.Value
+}
+
+func findByLower(lookingFor string) *Config {
+ for c := range configPB.IterAll() {
+ if strings.ToLower(c.Key) == strings.ToLower(lookingFor) {
+ return c
+ }
+ }
+ return nil
}
-func GetPanic(flag string) string {
+func True(flag string) bool {
saveMu.Lock()
defer saveMu.Unlock()
if configPB == nil {
- configPanic(flag)
+ return false
}
found := configPB.FindByKey(flag)
if found == nil {
- configPanic(flag)
+ return false
}
- return found.Value
-}
-
-func configPanic(varname string) {
- saveMu.Lock()
- defer saveMu.Unlock()
- if configPB == nil {
- panic("config file is nil")
+ if strings.ToLower(found.Value) == "true" {
+ return true
}
- panic("config name '" + varname + "' not found")
+ return false
}
func Set(key string, newValue string) error {
diff --git a/config.proto b/config.proto
index 2716967..9803223 100644
--- a/config.proto
+++ b/config.proto
@@ -6,17 +6,14 @@ package config;
import "google/protobuf/timestamp.proto"; // Import the well-known type for Timestamp
-message Config { //
- string key = 1; // config key name `autogenpb:unique` `autogenpb:sort`
- string value = 2; // config value name
- google.protobuf.Timestamp ctime = 3; // create time of the patch
- map<string, string> vals = 4; // a simple map
+message Config { //
+ string key = 1; // config key name `autogenpb:unique` `autogenpb:sort`
+ string value = 2; // config value name
}
-message Configs { // `autogenpb:marshal` `autogenpb:nomutex`
- string uuid = 1; // `autogenpb:uuid:3135d0f9-82a9-40b6-8aa1-b683ebe7bedd`
- string version = 2; // `autogenpb:version:v0.0.2 go.wit.com/lib/config`
- repeated Config configs = 3;
- string filename = 4; // can store where the filename is so that saves can be automated
- map<string, string> flags = 5; // a simple map
+message Configs { // `autogenpb:marshal` `autogenpb:nomutex`
+ string uuid = 1; // `autogenpb:uuid:3135d0f9-82a9-40b6-8aa1-b683ebe7bedd`
+ string version = 2; // `autogenpb:version:v0.0.2 go.wit.com/lib/config`
+ repeated Config configs = 3;
+ string filename = 4; // can store where the filename is so that saves can be automated
}
diff --git a/doc.go b/doc.go
new file mode 100644
index 0000000..63ba20a
--- /dev/null
+++ b/doc.go
@@ -0,0 +1,18 @@
+// Because you followed autogenpb's advice (you did follow it right?) you now
+// win the automation lottery.
+//
+// for this .proto file, GetProtobufName(pb) returns "repos"
+// Then, ConfigLoad(), ConfigSave(), CacheLoad() and CacheSave()
+// all do exactly what is expected:
+//
+// Automatically work with the files:
+// ~/.config/<appname>/repos.pb
+// or
+// ~/.cache/<appname/repos.pb
+//
+// message Repos {
+// string uuid = 1;
+// string version = 2;
+// repeated Repo repos = 3;
+
+package config
diff --git a/init.go b/init.go
index 93ebeab..a2c146a 100644
--- a/init.go
+++ b/init.go
@@ -46,7 +46,6 @@ func Init(appname, version, buildtime string, fromargv []string) error {
return err
}
fmt.Println("config.Init()", err)
- // panic("config")
return err
}
diff --git a/loadConfig.go b/loadConfig.go
new file mode 100644
index 0000000..0c90a17
--- /dev/null
+++ b/loadConfig.go
@@ -0,0 +1,59 @@
+package config
+
+import (
+ "errors"
+ "strings"
+
+ "go.wit.com/log"
+ "google.golang.org/protobuf/proto"
+)
+
+// loads foo.proto from ~/.config/<appname>/foo.text
+func ConfigLoad(pb proto.Message) error {
+ appname, err := GetAppname() // already configured by your application
+ if err != nil {
+ return err
+ }
+ protoname, err := GetProtobufName(pb) // defined in the foo.proto file
+ if err != nil {
+ return err
+ }
+
+ curfilename, err := GetFilename(pb)
+ if err == nil {
+ return err
+ }
+
+ // Get ~/.config/appname/protoname.text
+ fullname := GetConfigFilename(appname, protoname)
+
+ if err = loadTEXT(pb, fullname); err == nil {
+ // If the config is old or broken, this sets the filename
+ if curfilename != fullname {
+ _, err := SetFilename(pb, fullname)
+ if err != nil {
+ log.Info("FILENAME COULD NOT BE SET old=", curfilename)
+ log.Info("FILENAME COULD NOT BE SET new=", fullname)
+ return errors.Join(err, errors.New("something is wrong in lib/config"))
+ }
+ }
+ return nil
+ } else {
+ if strings.HasSuffix(fullname, ".text") {
+ fulljson := fullname + ".json"
+ // If the config is old or broken, this sets the filename
+ if err := loadJSON(pb, fulljson); err == nil {
+ if curfilename != fullname {
+ _, err := SetFilename(pb, fullname)
+ if err != nil {
+ log.Info("FILENAME COULD NOT BE SET old=", curfilename)
+ log.Info("FILENAME COULD NOT BE SET new=", fullname)
+ return errors.Join(err, errors.New("something is wrong in lib/config"))
+ }
+ }
+ return nil
+ }
+ }
+ }
+ return ErrMarshal
+}
diff --git a/load.go b/loadRaw.go
index 62b9017..c09b3f5 100644
--- a/load.go
+++ b/loadRaw.go
@@ -29,7 +29,7 @@ var ErrMarshal error = fmt.Errorf("protobuf parse error")
// - Full path to the config file. usually: ~/.config/<appname>
// - []byte : the contents of the file
// - error on read
-func ConfigLoad(pb proto.Message, appname string, protoname string) error {
+func ConfigLoadRaw(pb proto.Message, appname string, protoname string) error {
// Get ~/.config/appname/protoname.text
fullname := GetConfigFilename(appname, protoname)
@@ -131,7 +131,7 @@ func LoadVersionCheckPB(pb proto.Message) (string, string, error) {
}
// verify 'version' for .pb files
- // application will panic if they don't match
+ // application should die if they don't match
var worked bool
newver, err = GetString(pb, "version")
if err != nil {
@@ -166,10 +166,13 @@ func LoadVersionCheckPB(pb proto.Message) (string, string, error) {
return newver, pbver, nil
}
-// uses the version to panic. This is needed because loading binary
+// uses the version to die. This is needed because loading binary
// protobuf files with rearranged messages is indeterminate
func LoadPB(pb proto.Message) error {
fullname, err := GetFilename(pb)
+ if fullname == "" {
+ panic("config.LoadPB() got blank filename = ''")
+ }
if err != nil {
return err
}
@@ -184,11 +187,11 @@ func LoadPB(pb proto.Message) error {
fmt.Println("")
fmt.Println("Your protobuf file is old and can not be loaded")
fmt.Println("your application must decide how to handle this (delete or fix)")
- fmt.Println("always panic here. application is broken")
+ fmt.Println("always die here. application is broken")
fmt.Println("You must delete or convert the file", fullname)
fmt.Println("")
// probably should ALWAYS PANIC HERE
- // upon further study, always panic here is better than not
+ // upon further study, always die here is better than not
s := fmt.Sprintf("protobuf version wrong. delete or fix %s", fullname)
panic(s)
}
diff --git a/save.go b/saveRaw.go
index d257649..d257649 100644
--- a/save.go
+++ b/saveRaw.go