From 8a24584262a90956e012cee7b03c8c2b9f4b794c Mon Sep 17 00:00:00 2001 From: Jeff Carr Date: Mon, 20 Oct 2025 05:50:38 -0500 Subject: reworking this to make it more sane. hopefully. --- Makefile | 6 +- README.md | 63 +++++++--- autogenpbLookup.go | 186 ++++++++++++++++++++++++++++ autogenpbName.go | 111 +++++++++++++++++ config.Load.go | 81 +++++++++++++ config.Panic.go | 37 ++++++ config.Save.go | 35 +++--- config.proto | 19 ++- doc.go | 18 +++ init.go | 1 - load.go | 348 ---------------------------------------------------- loadConfig.go | 59 +++++++++ loadRaw.go | 351 +++++++++++++++++++++++++++++++++++++++++++++++++++++ lookupPB.go | 165 ------------------------- save.go | 162 ------------------------- saveRaw.go | 162 +++++++++++++++++++++++++ 16 files changed, 1080 insertions(+), 724 deletions(-) create mode 100644 autogenpbLookup.go create mode 100644 autogenpbName.go create mode 100644 config.Load.go create mode 100644 config.Panic.go create mode 100644 doc.go delete mode 100644 load.go create mode 100644 loadConfig.go create mode 100644 loadRaw.go delete mode 100644 lookupPB.go delete mode 100644 save.go create mode 100644 saveRaw.go 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 .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//repos.pb +// or +// ~/.cache//repos.pb +// +// or +// +// ~/.cache//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 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 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//repos.pb +// or +// ~/.cache// -func Load(appname string) ([]byte, string) { -} -*/ - -var ErrEmpty error = fmt.Errorf("config file was empty") -var ErrMarshal error = fmt.Errorf("protobuf parse error") - -// returns: -// - Full path to the config file. usually: ~/.config/ -// - []byte : the contents of the file -// - error on read -func ConfigLoad(pb proto.Message, appname string, protoname string) error { - // Get ~/.config/appname/protoname.text - fullname := GetConfigFilename(appname, protoname) - - var pbFilenameSupport bool - var err error - curfilename, err := GetFilename(pb) - if err == nil { - pbFilenameSupport = true - } else { - // this .proto doesn't have the Filename variable/message - } - - if err = loadTEXT(pb, fullname); err == nil { - if pbFilenameSupport { - // 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) - panic("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 pbFilenameSupport { - 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) - panic("something is wrong in lib/config") - } - } - } - return nil - } - } - } - return ErrMarshal -} - -// returns the default constructed filename: -// ~/.config/appname/protoname.text -func GetConfigFilename(appname string, protoname string) string { - var err error - configDir, err := os.UserConfigDir() - if err != nil { - // todo: get something better than /tmp/ if anyone cares - return filepath.Join("/tmp", appname, protoname+".text") - } - return filepath.Join(configDir, appname, protoname+".text") -} - -// loads from the users .cache dir -// if the .proto file version changes, automatically delete the .pb -// file. This is important to avoid marshalling garbage data -// .cache files are treated as such, a "cache" file. don't keep important -// things in here. argv stores the information here for autodelete -func LoadCache(pb proto.Message, appname string, protoname string) error { - cacheDir, _ := os.UserCacheDir() - fullpath := filepath.Join(cacheDir, appname) - os.MkdirAll(fullpath, os.ModePerm) - fullname := filepath.Join(fullpath, protoname+".pb") - _, err := SetFilename(pb, fullname) - if err != nil { - pb = nil - os.Remove(fullname) - return err - } - newver, curver, err := LoadVersionCheckPB(pb) - if err != nil { - pb = nil - os.Remove(fullname) - return err - } - _, _ = newver, curver - return nil -} - -func LoadVersionCheckPB(pb proto.Message) (string, string, error) { - var newver string - var pbver string - var err error - - fullname, err := GetFilename(pb) - if err != nil { - return newver, pbver, err - } - // text is supposed to be "easy". Don't verify 'version' - if strings.HasSuffix(fullname, ".text") { - err = loadTEXT(pb, fullname) - return newver, pbver, err - } - - // verify 'version' for .pb files - // application will panic if they don't match - var worked bool - newver, err = GetString(pb, "version") - if err != nil { - return newver, pbver, err - } - // maybe don't really verify .json files (?) - // doing it for now anyway. maybe just return an error - if strings.HasSuffix(fullname, ".json") { - if err = loadJSON(pb, fullname); err != nil { - return newver, pbver, err - } - worked = true - } - if strings.HasSuffix(fullname, ".pb") { - if err = loadPB(pb, fullname); err != nil { - return newver, pbver, err - } - _, err = SetFilename(pb, fullname) - if err != nil { - return newver, pbver, err - } - worked = true - } - if !worked { - return newver, pbver, fmt.Errorf("unknown filetype '%s'", fullname) - } - // get the version from the current PB saved on disk - pbver, _ = GetString(pb, "version") - if newver != pbver { - return newver, pbver, VersionMismatch - } - return newver, pbver, nil -} - -// uses the version to panic. This is needed because loading binary -// protobuf files with rearranged messages is indeterminate -func LoadPB(pb proto.Message) error { - fullname, err := GetFilename(pb) - if err != nil { - return err - } - // this code needs work - newver, pbver, err := LoadVersionCheckPB(pb) - if errors.Is(err, os.ErrNotExist) { - return err - } - if errors.Is(err, VersionMismatch) || (newver != pbver) { - fmt.Println("") - fmt.Printf("VERSION new '%s' != cur PB '%s'\n", newver, pbver) - 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("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 - s := fmt.Sprintf("protobuf version wrong. delete or fix %s", fullname) - panic(s) - } - if err != nil { - // return to let the application figure this out - return err - } - return nil -} - -func LoadFromFilename(pb proto.Message, fullname string) error { - return LoadFile(pb, fullname) -} - -func LoadFile(pb proto.Message, fullname string) error { - if strings.HasSuffix(fullname, ".text") { - return loadTEXT(pb, fullname) - } - if strings.HasSuffix(fullname, ".json") { - return loadJSON(pb, fullname) - } - if strings.HasSuffix(fullname, ".pb") { - return loadPB(pb, fullname) - } - - return fmt.Errorf("unknown filetype '%s'", fullname) -} - -func loadPB(pb proto.Message, fullname string) error { - data, err := loadFile(fullname) - if err != nil { - // set pb.Filename that was attempted - return err - } - - if err = proto.Unmarshal(data, pb); err != nil { - return err - } - - return nil -} - -func LoadConfigPB(pb proto.Message, appname string, protoname string) (string, error) { - var fullname string - if strings.HasPrefix(appname, "/") { - fullname = filepath.Join(appname, protoname+".pb") - } else { - configDir, err := os.UserConfigDir() - if err != nil { - return "", err - } - - fullname = filepath.Join(configDir, appname, protoname+".pb") - } - - data, err := loadFile(fullname) - if err != nil { - return fullname, err - } - - // Unmarshal() - if err = proto.Unmarshal(data, pb); err != nil { - return fullname, err - } - - return fullname, nil -} - -func loadTEXT(pb proto.Message, fullname string) error { - var data []byte - var err error - SetFilename(pb, fullname) - if data, err = loadFile(fullname); err != nil { - return err - } - - // don't even bother with Marshal() - if data == nil { - return ErrEmpty // file is empty - } - - // Unmarshal() - if err = prototext.Unmarshal(data, pb); err != nil { - return ErrMarshal - } - - if fn, err := GetFilename(pb); err != nil { - if fn != fullname { - SetFilename(pb, fullname) - } - } - return nil -} - -// json files are backup Marshal() data in case .text Unmarshal() fails -// they always should have the ".text" filename in them -func loadJSON(pb proto.Message, fullname string) error { - var data []byte - var err error - if data, err = loadFile(fullname); err != nil { - return err - } - - // don't even bother with Marshal() - if data == nil { - return ErrEmpty // file is empty - } - - // Unmarshal() - if err = protojson.Unmarshal(data, pb); err != nil { - return ErrMarshal - } - - if fn, err := GetFilename(pb); err != nil { - if fn != fullname { - SetFilename(pb, fullname) - } - } - return nil -} - -/* left this here to remind myself just how dumb I can be -// dumb but simple to read logic -func missingConfig(fullname string) error { - data1, err1 := os.ReadFile(fullname) - if !errors.Is(err1, os.ErrNotExist) { - return err1 - } - - data2, err2 := os.ReadFile(fullname + ".json") - if !errors.Is(err2, os.ErrNotExist) { - return err2 - } - if errors.Is(err1, os.ErrNotExist) && errors.Is(err2, os.ErrNotExist) { - return os.ErrNotExist - } - if (len(data1) == 0) && (len(data2) == 0) { - return ErrEmpty - } - return nil -} -*/ - -func loadFile(fullname string) ([]byte, error) { - data, err := os.ReadFile(fullname) - if errors.Is(err, os.ErrNotExist) { - // if file does not exist, just return nil. this - return nil, err - } - if err != nil { - return nil, err - } - if len(data) == 0 { - return data, ErrEmpty - } - return data, nil -} 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//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/loadRaw.go b/loadRaw.go new file mode 100644 index 0000000..c09b3f5 --- /dev/null +++ b/loadRaw.go @@ -0,0 +1,351 @@ +package config + +// functions to import and export the protobuf +// data to and from config files + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "go.wit.com/log" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" +) + +/* +// loads a file from ~/.config// +func Load(appname string) ([]byte, string) { +} +*/ + +var ErrEmpty error = fmt.Errorf("config file was empty") +var ErrMarshal error = fmt.Errorf("protobuf parse error") + +// returns: +// - Full path to the config file. usually: ~/.config/ +// - []byte : the contents of the file +// - error on read +func ConfigLoadRaw(pb proto.Message, appname string, protoname string) error { + // Get ~/.config/appname/protoname.text + fullname := GetConfigFilename(appname, protoname) + + var pbFilenameSupport bool + var err error + curfilename, err := GetFilename(pb) + if err == nil { + pbFilenameSupport = true + } else { + // this .proto doesn't have the Filename variable/message + } + + if err = loadTEXT(pb, fullname); err == nil { + if pbFilenameSupport { + // 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) + panic("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 pbFilenameSupport { + 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) + panic("something is wrong in lib/config") + } + } + } + return nil + } + } + } + return ErrMarshal +} + +// returns the default constructed filename: +// ~/.config/appname/protoname.text +func GetConfigFilename(appname string, protoname string) string { + var err error + configDir, err := os.UserConfigDir() + if err != nil { + // todo: get something better than /tmp/ if anyone cares + return filepath.Join("/tmp", appname, protoname+".text") + } + return filepath.Join(configDir, appname, protoname+".text") +} + +// loads from the users .cache dir +// if the .proto file version changes, automatically delete the .pb +// file. This is important to avoid marshalling garbage data +// .cache files are treated as such, a "cache" file. don't keep important +// things in here. argv stores the information here for autodelete +func LoadCache(pb proto.Message, appname string, protoname string) error { + cacheDir, _ := os.UserCacheDir() + fullpath := filepath.Join(cacheDir, appname) + os.MkdirAll(fullpath, os.ModePerm) + fullname := filepath.Join(fullpath, protoname+".pb") + _, err := SetFilename(pb, fullname) + if err != nil { + pb = nil + os.Remove(fullname) + return err + } + newver, curver, err := LoadVersionCheckPB(pb) + if err != nil { + pb = nil + os.Remove(fullname) + return err + } + _, _ = newver, curver + return nil +} + +func LoadVersionCheckPB(pb proto.Message) (string, string, error) { + var newver string + var pbver string + var err error + + fullname, err := GetFilename(pb) + if err != nil { + return newver, pbver, err + } + // text is supposed to be "easy". Don't verify 'version' + if strings.HasSuffix(fullname, ".text") { + err = loadTEXT(pb, fullname) + return newver, pbver, err + } + + // verify 'version' for .pb files + // application should die if they don't match + var worked bool + newver, err = GetString(pb, "version") + if err != nil { + return newver, pbver, err + } + // maybe don't really verify .json files (?) + // doing it for now anyway. maybe just return an error + if strings.HasSuffix(fullname, ".json") { + if err = loadJSON(pb, fullname); err != nil { + return newver, pbver, err + } + worked = true + } + if strings.HasSuffix(fullname, ".pb") { + if err = loadPB(pb, fullname); err != nil { + return newver, pbver, err + } + _, err = SetFilename(pb, fullname) + if err != nil { + return newver, pbver, err + } + worked = true + } + if !worked { + return newver, pbver, fmt.Errorf("unknown filetype '%s'", fullname) + } + // get the version from the current PB saved on disk + pbver, _ = GetString(pb, "version") + if newver != pbver { + return newver, pbver, VersionMismatch + } + return newver, pbver, nil +} + +// 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 + } + // this code needs work + newver, pbver, err := LoadVersionCheckPB(pb) + if errors.Is(err, os.ErrNotExist) { + return err + } + if errors.Is(err, VersionMismatch) || (newver != pbver) { + fmt.Println("") + fmt.Printf("VERSION new '%s' != cur PB '%s'\n", newver, pbver) + 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 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 die here is better than not + s := fmt.Sprintf("protobuf version wrong. delete or fix %s", fullname) + panic(s) + } + if err != nil { + // return to let the application figure this out + return err + } + return nil +} + +func LoadFromFilename(pb proto.Message, fullname string) error { + return LoadFile(pb, fullname) +} + +func LoadFile(pb proto.Message, fullname string) error { + if strings.HasSuffix(fullname, ".text") { + return loadTEXT(pb, fullname) + } + if strings.HasSuffix(fullname, ".json") { + return loadJSON(pb, fullname) + } + if strings.HasSuffix(fullname, ".pb") { + return loadPB(pb, fullname) + } + + return fmt.Errorf("unknown filetype '%s'", fullname) +} + +func loadPB(pb proto.Message, fullname string) error { + data, err := loadFile(fullname) + if err != nil { + // set pb.Filename that was attempted + return err + } + + if err = proto.Unmarshal(data, pb); err != nil { + return err + } + + return nil +} + +func LoadConfigPB(pb proto.Message, appname string, protoname string) (string, error) { + var fullname string + if strings.HasPrefix(appname, "/") { + fullname = filepath.Join(appname, protoname+".pb") + } else { + configDir, err := os.UserConfigDir() + if err != nil { + return "", err + } + + fullname = filepath.Join(configDir, appname, protoname+".pb") + } + + data, err := loadFile(fullname) + if err != nil { + return fullname, err + } + + // Unmarshal() + if err = proto.Unmarshal(data, pb); err != nil { + return fullname, err + } + + return fullname, nil +} + +func loadTEXT(pb proto.Message, fullname string) error { + var data []byte + var err error + SetFilename(pb, fullname) + if data, err = loadFile(fullname); err != nil { + return err + } + + // don't even bother with Marshal() + if data == nil { + return ErrEmpty // file is empty + } + + // Unmarshal() + if err = prototext.Unmarshal(data, pb); err != nil { + return ErrMarshal + } + + if fn, err := GetFilename(pb); err != nil { + if fn != fullname { + SetFilename(pb, fullname) + } + } + return nil +} + +// json files are backup Marshal() data in case .text Unmarshal() fails +// they always should have the ".text" filename in them +func loadJSON(pb proto.Message, fullname string) error { + var data []byte + var err error + if data, err = loadFile(fullname); err != nil { + return err + } + + // don't even bother with Marshal() + if data == nil { + return ErrEmpty // file is empty + } + + // Unmarshal() + if err = protojson.Unmarshal(data, pb); err != nil { + return ErrMarshal + } + + if fn, err := GetFilename(pb); err != nil { + if fn != fullname { + SetFilename(pb, fullname) + } + } + return nil +} + +/* left this here to remind myself just how dumb I can be +// dumb but simple to read logic +func missingConfig(fullname string) error { + data1, err1 := os.ReadFile(fullname) + if !errors.Is(err1, os.ErrNotExist) { + return err1 + } + + data2, err2 := os.ReadFile(fullname + ".json") + if !errors.Is(err2, os.ErrNotExist) { + return err2 + } + if errors.Is(err1, os.ErrNotExist) && errors.Is(err2, os.ErrNotExist) { + return os.ErrNotExist + } + if (len(data1) == 0) && (len(data2) == 0) { + return ErrEmpty + } + return nil +} +*/ + +func loadFile(fullname string) ([]byte, error) { + data, err := os.ReadFile(fullname) + if errors.Is(err, os.ErrNotExist) { + // if file does not exist, just return nil. this + return nil, err + } + if err != nil { + return nil, err + } + if len(data) == 0 { + return data, ErrEmpty + } + return data, nil +} diff --git a/lookupPB.go b/lookupPB.go deleted file mode 100644 index 9fb73a2..0000000 --- a/lookupPB.go +++ /dev/null @@ -1,165 +0,0 @@ -package config - -import ( - "errors" - "fmt" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" -) - -var ErrProtoNoVarName error = errors.New("name not in .proto") -var ErrProtoVarNotString error = errors.New("name exists but is not a string") - -// Gemini AI can help author some pretty good protobuf code. -// I never remember the syntax for 'reflect' on these things. - -// sets "Filename" if it exists in the protobuf -func SetFilename(pb proto.Message, filename string) (bool, error) { - msg := pb.ProtoReflect() // This is the entry point to the reflection API. - - descriptor := msg.Descriptor() // Get the message's descriptor, which contains metadata about its fields. - - fieldName := protoreflect.Name("Filename") - fieldDescriptor := descriptor.Fields().ByName(fieldName) - - if fieldDescriptor == nil { - fieldName = protoreflect.Name("filename") - fieldDescriptor = descriptor.Fields().ByName(fieldName) - } - - if fieldDescriptor == nil { - return false, fmt.Errorf("fieldDescriptor == nil") - } - - if fieldDescriptor.Kind() != protoreflect.StringKind { - // The field exists but is not a string, so we can't return it as one. - return false, fmt.Errorf("The field exists but is not a string") - } - - valueToSet := protoreflect.ValueOfString(filename) - - // 6. If the field exists and is a string, get its value. - // The value is returned as a protoreflect.Value. - msg.Set(fieldDescriptor, valueToSet) - - // 7. Convert the protoreflect.Value to a native Go string. - return true, nil -} - -// this will try both "filename" and "Filename" -func GetFilename(pb proto.Message) (string, error) { - // 1. Get the protoreflect.Message interface from the message. - // This is the entry point to the reflection API. - msg := pb.ProtoReflect() - - // 2. Get the message's descriptor, which contains metadata about its fields. - descriptor := msg.Descriptor() - - // 3. Find the specific field descriptor by its protobuf name ("Filename"). - // Note: The field name must match the name in the .proto file. - fieldName := protoreflect.Name("Filename") - fieldDescriptor := descriptor.Fields().ByName(fieldName) - - // try upper case - if fieldDescriptor == nil { - 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") - // } - } - - // 4. Check if the field was found. If not, return false. - if fieldDescriptor == nil { - return "", ErrProtoNoVarName - } - - // 5. (Optional but recommended) Verify the field is a string type. - if fieldDescriptor.Kind() != protoreflect.StringKind { - // The field exists but is not a string, so we can't return it as one. - return "", ErrProtoVarNotString - } - - // 6. If the field exists and is a string, get its value. - // The value is returned as a protoreflect.Value. - value := msg.Get(fieldDescriptor) - - // 7. Convert the protoreflect.Value to a native Go string. - return value.String(), nil -} - -// this will try both "filename" and "Filename" -func GetString(pb proto.Message, varname string) (string, error) { - // 1. Get the protoreflect.Message interface from the message. - // This is the entry point to the reflection API. - msg := pb.ProtoReflect() - - // 2. Get the message's descriptor, which contains metadata about its fields. - descriptor := msg.Descriptor() - - // 3. Find the specific field descriptor by its protobuf name ("Filename"). - // Note: The field name must match the name in the .proto file. - fieldName := protoreflect.Name(varname) - fieldDescriptor := descriptor.Fields().ByName(fieldName) - - // 4. Check if the field was found. If not, return false. - if fieldDescriptor == nil { - return "", ErrProtoNoVarName - } - - // 5. (Optional but recommended) Verify the field is a string type. - if fieldDescriptor.Kind() != protoreflect.StringKind { - // The field exists but is not a string, so we can't return it as one. - return "", ErrProtoVarNotString - } - - // 6. If the field exists and is a string, get its value. - // The value is returned as a protoreflect.Value. - value := msg.Get(fieldDescriptor) - - // 7. Convert the protoreflect.Value to a native Go string. - return value.String(), nil -} - -// don't do this. use prototext.Format(pb) -// duh. I'm dumb. I literally wasted my time + was being lazy so I -// just asked asking Gemini AI to make some function for this -// when, for years, I use prototext.Format() all over the place -func printStrings(pb proto.Message) { - // 1. Get the protoreflect.Message interface. - msg := pb.ProtoReflect() - - // It's good practice to check if the message is valid. - if !msg.IsValid() { - fmt.Printf("Error: Provided protobuf message is not valid.") - return - } - - // 2. Get the message's descriptor. - descriptor := msg.Descriptor() - fmt.Printf("--- Listing String Fields in [%s] ---\n", descriptor.FullName()) - - // 3. Get the collection of all field descriptors for this message. - fields := descriptor.Fields() - - // 4. Iterate over all the fields. - for i := 0; i < fields.Len(); i++ { - // Get the descriptor for the field at the current index. - fieldDescriptor := fields.Get(i) - - // 5. Check if the field's kind is a string. - if fieldDescriptor.Kind() == protoreflect.StringKind { - // 6. If it is a string, get its name and value. - fieldName := fieldDescriptor.Name() - value := msg.Get(fieldDescriptor).String() - - // 7. Print the formatted result. - // We add a check to see if the field is populated. An empty string - // is a valid value, but you might only want to see set fields. - if msg.Has(fieldDescriptor) { - fmt.Printf(" %s: \"%s\"\n", fieldName, value) - } - } - } -} diff --git a/save.go b/save.go deleted file mode 100644 index d257649..0000000 --- a/save.go +++ /dev/null @@ -1,162 +0,0 @@ -package config - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/encoding/prototext" - "google.golang.org/protobuf/proto" -) - -var ErrProtoFilename error = fmt.Errorf("proto does not have Filename") - -func ConfigSave(pb proto.Message) error { - return saveTEXT(pb, "") -} - -// writes the protobuf to disk -// uses the already configured Filename -func SavePB(pb proto.Message) error { - fullname, err := GetFilename(pb) - if err != nil { - return err - } - return SaveToFilename(pb, fullname) -} - -// writes the protobuf to disk (sets Filename if PB has 'Filename') -func SaveToFilename(pb proto.Message, fullname string) error { - basedir, _ := filepath.Split(fullname) - if err := os.MkdirAll(basedir, os.ModePerm); err != nil { - return err - } - if strings.HasSuffix(fullname, ".pb") { - return saveProto(pb, fullname) - } - if strings.HasSuffix(fullname, ".text") { - return saveTEXT(pb, "") - } - if strings.HasSuffix(fullname, ".json") { - return saveJSON(pb) - } - return fmt.Errorf("unknown filetype '%s'", fullname) -} - -func saveProto(pb proto.Message, fullname string) error { - if !strings.HasSuffix(fullname, ".pb") { - // todo: append .text here? - return fmt.Errorf("%s needs to end in '.pb'", fullname) - } - - dir, name := filepath.Split(fullname) - if name == "" { - return fmt.Errorf("filename was blank") - } - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return err - } - - data, err := proto.Marshal(pb) - if err != nil { - return err - } - - return configWrite(fullname, data) -} - -func ConfigSaveWithHeader(pb proto.Message, header string) error { - var final error - if err := saveTEXT(pb, header); err != nil { - final = err - } - if err := saveJSON(pb); err != nil { - final = err - } - - /* - if strings.HasSuffix(fullname, ".text") { - fullname = strings.TrimSuffix(fullname, ".text") - fullname += ".json" - if err := configJSON(fullname, pb); err != nil { - final = err - } - } - */ - return final -} - -func saveTEXT(pb proto.Message, header string) error { - // get pb.Filename if it is there in the .proto file - fullname, err := GetFilename(pb) - if err != nil { - return err - } - fullname = strings.TrimSpace(fullname) - if fullname == "" { - return fmt.Errorf("saveTEXT() pb.Filename was blank") - } - - if !strings.HasSuffix(fullname, ".text") { - // todo: append .text here? - return fmt.Errorf("not .text file: %s", fullname) - } - - dir, name := filepath.Split(fullname) - if name == "" { - return fmt.Errorf("filename was blank") - } - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return err - } - - data := prototext.Format(pb) - - err = configWrite(fullname, []byte(data)) - if err != nil { - return err - } - return nil -} - -func saveJSON(pb proto.Message) error { - // get pb.Filename if it is there in the .proto file - fullname, err := GetFilename(pb) - if err != nil { - return err - } - if !strings.HasSuffix(fullname, ".text") { - // todo: append .text here? - return fmt.Errorf("not .text file: %s", fullname) - } - - dir, name := filepath.Split(fullname) - if name == "" { - return fmt.Errorf("filename was blank") - } - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return err - } - - data := protojson.Format(pb) - - fullname += ".json" - - err = configWrite(fullname, []byte(data)) - if err != nil { - return err - } - return nil -} - -func configWrite(fullname string, data []byte) error { - cfgfile, err := os.OpenFile(fullname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - defer cfgfile.Close() - if err != nil { - return err - } - _, err = cfgfile.Write(data) - return err -} diff --git a/saveRaw.go b/saveRaw.go new file mode 100644 index 0000000..d257649 --- /dev/null +++ b/saveRaw.go @@ -0,0 +1,162 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" +) + +var ErrProtoFilename error = fmt.Errorf("proto does not have Filename") + +func ConfigSave(pb proto.Message) error { + return saveTEXT(pb, "") +} + +// writes the protobuf to disk +// uses the already configured Filename +func SavePB(pb proto.Message) error { + fullname, err := GetFilename(pb) + if err != nil { + return err + } + return SaveToFilename(pb, fullname) +} + +// writes the protobuf to disk (sets Filename if PB has 'Filename') +func SaveToFilename(pb proto.Message, fullname string) error { + basedir, _ := filepath.Split(fullname) + if err := os.MkdirAll(basedir, os.ModePerm); err != nil { + return err + } + if strings.HasSuffix(fullname, ".pb") { + return saveProto(pb, fullname) + } + if strings.HasSuffix(fullname, ".text") { + return saveTEXT(pb, "") + } + if strings.HasSuffix(fullname, ".json") { + return saveJSON(pb) + } + return fmt.Errorf("unknown filetype '%s'", fullname) +} + +func saveProto(pb proto.Message, fullname string) error { + if !strings.HasSuffix(fullname, ".pb") { + // todo: append .text here? + return fmt.Errorf("%s needs to end in '.pb'", fullname) + } + + dir, name := filepath.Split(fullname) + if name == "" { + return fmt.Errorf("filename was blank") + } + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + + data, err := proto.Marshal(pb) + if err != nil { + return err + } + + return configWrite(fullname, data) +} + +func ConfigSaveWithHeader(pb proto.Message, header string) error { + var final error + if err := saveTEXT(pb, header); err != nil { + final = err + } + if err := saveJSON(pb); err != nil { + final = err + } + + /* + if strings.HasSuffix(fullname, ".text") { + fullname = strings.TrimSuffix(fullname, ".text") + fullname += ".json" + if err := configJSON(fullname, pb); err != nil { + final = err + } + } + */ + return final +} + +func saveTEXT(pb proto.Message, header string) error { + // get pb.Filename if it is there in the .proto file + fullname, err := GetFilename(pb) + if err != nil { + return err + } + fullname = strings.TrimSpace(fullname) + if fullname == "" { + return fmt.Errorf("saveTEXT() pb.Filename was blank") + } + + if !strings.HasSuffix(fullname, ".text") { + // todo: append .text here? + return fmt.Errorf("not .text file: %s", fullname) + } + + dir, name := filepath.Split(fullname) + if name == "" { + return fmt.Errorf("filename was blank") + } + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + + data := prototext.Format(pb) + + err = configWrite(fullname, []byte(data)) + if err != nil { + return err + } + return nil +} + +func saveJSON(pb proto.Message) error { + // get pb.Filename if it is there in the .proto file + fullname, err := GetFilename(pb) + if err != nil { + return err + } + if !strings.HasSuffix(fullname, ".text") { + // todo: append .text here? + return fmt.Errorf("not .text file: %s", fullname) + } + + dir, name := filepath.Split(fullname) + if name == "" { + return fmt.Errorf("filename was blank") + } + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + + data := protojson.Format(pb) + + fullname += ".json" + + err = configWrite(fullname, []byte(data)) + if err != nil { + return err + } + return nil +} + +func configWrite(fullname string, data []byte) error { + cfgfile, err := os.OpenFile(fullname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + defer cfgfile.Close() + if err != nil { + return err + } + _, err = cfgfile.Write(data) + return err +} -- cgit v1.2.3