diff options
| author | Jeff Carr <[email protected]> | 2025-10-20 05:50:38 -0500 |
|---|---|---|
| committer | Jeff Carr <[email protected]> | 2025-10-20 05:50:38 -0500 |
| commit | 8a24584262a90956e012cee7b03c8c2b9f4b794c (patch) | |
| tree | 8b7d0afd04e717d7c07da5eb2894debd74ec2307 | |
| parent | b6e93c08d601a7a6c27a0fdcdf98f6cb7dc9ccd8 (diff) | |
reworking this to make it more sane. hopefully.
| -rw-r--r-- | Makefile | 6 | ||||
| -rw-r--r-- | README.md | 63 | ||||
| -rw-r--r-- | autogenpbLookup.go (renamed from lookupPB.go) | 23 | ||||
| -rw-r--r-- | autogenpbName.go | 111 | ||||
| -rw-r--r-- | config.Load.go | 81 | ||||
| -rw-r--r-- | config.Panic.go | 37 | ||||
| -rw-r--r-- | config.Save.go | 35 | ||||
| -rw-r--r-- | config.proto | 19 | ||||
| -rw-r--r-- | doc.go | 18 | ||||
| -rw-r--r-- | init.go | 1 | ||||
| -rw-r--r-- | loadConfig.go | 59 | ||||
| -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
@@ -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 @@ -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 } @@ -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 @@ -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 +} @@ -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) } |
