diff --git a/README.md b/README.md index b4a5c21..65caf48 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Defaults : { "port": 99, } -Config : { +Given config file : { "user": "root" "secret": "confsecret" } @@ -54,6 +54,7 @@ The config files may be of the following types: - yml - json + ## Keep in mind - There is no case sensitivty, i.e. "pim", "Pim" and "PIM" are all considered the same - The names of the environmental variables must match that of the struct. It is possible to set a prefix, so that i.e. if "MYVAR_" is set as a prefix, "MYVAR_PIM" will map to the property "pim"/"Pim"/"PIM". diff --git a/config.go b/config.go index c6ae114..c7fa307 100644 --- a/config.go +++ b/config.go @@ -13,6 +13,11 @@ import ( "github.com/pkg/errors" ) +//Aliasing flag.Errorhandling for clarity and easy of use. +type ErrorHandling flag.ErrorHandling + +var osExit = os.Exit //to enable testing + var ( envs map[string]interface{} envPrefix string @@ -20,11 +25,14 @@ var ( writedefconf bool printconf bool - configErrorHandling flag.ErrorHandling + configErrorHandling ErrorHandling ) -var osExit = os.Exit //to enable testing -var ErrNotAPointer = errors.New("cfg should be pointer") +const ( + ContinueOnError = ErrorHandling(flag.ContinueOnError) // Return a descriptive error. + ExitOnError = ErrorHandling(flag.ExitOnError) // Call os.Exit(2) or for -h/-help Exit(0). + PanicOnError = ErrorHandling(flag.PanicOnError) // Call panic with a descriptive error. +) func init() { envs = make(map[string]interface{}) @@ -36,34 +44,56 @@ func init() { flagSet.Usage = Usage } -// Init sets the error handling property. -// The error handling for the config package is the same as that -// for the std flag package -func Init(errorHandling flag.ErrorHandling) { +/* +Init sets the global error handling property, as well as the error handling property for the flagset. + +The error handling for the config package is similar to that of the standard flag package; +there are three modes: Continue, Panic and Exit. + +The default mode is Continue. +*/ +func Init(errorHandling ErrorHandling) { configErrorHandling = errorHandling - flagSet.Init("config", errorHandling) + flagSet.Init("elri/config", flag.ErrorHandling(errorHandling)) } func handleError(err error) { switch configErrorHandling { - case flag.ContinueOnError: + case ContinueOnError: return - case flag.ExitOnError: + case ExitOnError: osExit(2) - case flag.PanicOnError: + case PanicOnError: panic(err) } } +/* +Set a prefix to use for all environmental variables. + +For example to different between what is used in testing and otherwise, the prefix "TEST_" could be used. +The environmental variables TEST_timeout and TEST_angle would then map to the properties 'timeout' and 'angle'. +*/ func SetEnvPrefix(prefix string) { envPrefix = prefix } +/* +Set a list of environmental variable names to check when filling out the configuration struct. + +The list can consist of variables both containing a set env prefix and not, but the environmental variable that is looked for will be that with the prefix. +That is, if the prefix is set as TEST_ and the list envVarNames is ["timeout", "TEST_angle"], the environmental variables that will be looked for are ["TEST_timeout", "TEST_angle"]. + +If the environmental variable(s) cannot be find, SetEnvsToParse will return an error containing all the names of the non-existant variables. Note that the error will only be return if +the error handling mode is set to ContinueOnError, else the function will Panic or Exit depending on the mode. +*/ func SetEnvsToParse(envVarNames []string) (err error) { for _, e := range envVarNames { eFull := e if envPrefix != "" { - eFull = envPrefix + e + if !strings.HasPrefix(eFull, envPrefix) { + eFull = envPrefix + e + } } envVar, ok := os.LookupEnv(eFull) if ok { @@ -78,10 +108,24 @@ func SetEnvsToParse(envVarNames []string) (err error) { return } +/* + Parse all the sources (flags, env vars, default config file) and store the result in the value pointer to by cfg. + + If cfg is not a pointer, SetUpConfiguration returns an ErrNotAPointer. + +*/ func SetUpConfiguration(cfg interface{}) (err error) { return setup(cfg, "") } +/* + Parse all the sources (flags, env vars, given config file, default config file) and store the result in the value pointer to by cfg. + + If cfg is not a pointer, SetUpConfigurationWithConfigFile returns an ErrNotAPointer. + + The 'filename' must either be an absolute path to the config file, exist in the current working directory, or in one of the directories given as 'dirs'. If the given file cannot be found, the other sources will still be parsed, but an ErrNoConfigFileToParse will be returned. + +*/ func SetUpConfigurationWithConfigFile(cfg interface{}, filename string, dirs ...string) (err error) { return setup(cfg, filename) } @@ -89,7 +133,8 @@ func SetUpConfigurationWithConfigFile(cfg interface{}, filename string, dirs ... func setup(cfg interface{}, filename string, dirs ...string) (err error) { //Check that cfg is pointer if reflect.ValueOf(cfg).Kind() != reflect.Ptr { - return fmt.Errorf("invalid argument: "+ErrNotAPointer.Error()+"but is %s", reflect.ValueOf(cfg).Kind()) + err = fmt.Errorf("[setup]: %w ", ErrNotAPointer) + return } // DEFAULT CONFIG FILE @@ -102,10 +147,7 @@ func setup(cfg interface{}, filename string, dirs ...string) (err error) { // GIVEN CONFIG FILE if filename != "" { - cf_err := ParseConfigFile(cfg, filename, dirs...) - if cf_err != nil { - err = addErr(err, cf_err) - } + err = ParseConfigFile(cfg, filename, dirs...) } // ENVIRONMENTAL VARIABLES @@ -143,6 +185,10 @@ func setup(cfg interface{}, filename string, dirs ...string) (err error) { osExit(0) } + if err != nil { + handleError(err) + } + return } @@ -259,23 +305,29 @@ func setFieldString(toInsert interface{}, fieldName string, fieldVal reflect.Val return } -// Creates a string given a ptr to a struct -// E.g. -// type Person struct { -// Name string -// Age int -// } -// -// mio := &Person{Name: "Mio", Age: 9} -// -// String(mio): -// name: Mio -// age: 9 +/* +Creates a string given a ptr to a struct. + +Example: + type Person struct { + Name string + Age int + } + + func printMio() { + mio := &Person{Name: "Mio", Age: 9} + fmt.Println(String(mio)) + } + +output: + name: Mio + age: 9 +*/ func String(c interface{}) string { return createString(c, true) } -// Same as String, except ignores zero values e.g. empty strings and zeroes +// Same as String(), except ignores zero values e.g. empty strings and zeroes func StringIgnoreZeroValues(c interface{}) string { return createString(c, false) } diff --git a/config_test.go b/config_test.go index c2de88f..8cf9a6e 100644 --- a/config_test.go +++ b/config_test.go @@ -411,14 +411,17 @@ func Test_ConfigFail(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - SetDefaultFile(tt.defaultFile) + err = SetDefaultFile(tt.defaultFile) + if err != nil { + err = ErrNoDefaultConfig + } if tt.cfg == nil { cfg = new(TestConfig) } else { cfg = tt.cfg } - err = SetUpConfigurationWithConfigFile(cfg, tt.configFile) + err = addErr(err, SetUpConfigurationWithConfigFile(cfg, tt.configFile)) assert.NotNil(t, err) for _, expectedErr := range tt.expectedErrors { assert.Contains(t, err.Error(), expectedErr.Error()) diff --git a/examples/complexstructs/main.go b/examples/complexstructs/main.go index fbbc8da..689178c 100644 --- a/examples/complexstructs/main.go +++ b/examples/complexstructs/main.go @@ -35,14 +35,20 @@ type Configuration struct { Bottles []Bottle `yaml:"bottles" toml:"bottles"` } -func main() { - config.SetDefaultFile("default_conf.yml") +func createConfig() *Configuration { + config.Init(config.PanicOnError) + + //The default file path must be absolut path, since config is set to PanicOnError, the setup will panic if run outside exmaples/complexstructs/ + config.SetDefaultFile("default_conf.toml") cfg := new(Configuration) - err := config.SetUpConfiguration(cfg) - if err != nil { - panic(err) - } + config.SetUpConfiguration(cfg) //disregards error here since error handling mode is set to panic + + return cfg +} + +func main() { + cfg := createConfig() fmt.Println(cfg.Welcome) fmt.Println("Now printing", cfg.Owner.Name+"'s bottles of "+cfg.Type) diff --git a/examples/complexstructs/main_test.go b/examples/complexstructs/main_test.go new file mode 100644 index 0000000..e47c10a --- /dev/null +++ b/examples/complexstructs/main_test.go @@ -0,0 +1,12 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_main(t *testing.T) { + main() + assert.True(t, true) +} diff --git a/examples/nodefaultfile/main.go b/examples/nodefaultfile/main.go new file mode 100644 index 0000000..dc54f9b --- /dev/null +++ b/examples/nodefaultfile/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/elri/config" +) + +// -- Configuration struct +type Configuration struct { + Nothing string `yaml:"nothing" toml:"nothing"` +} + +var ( + _ = flag.String("nothing", "Hello World", "does nothing") +) + +func main() { + config.ParseFlags() + + cfg := new(Configuration) + err := config.SetUpConfiguration(cfg) + if err != nil { + panic(err) + } + fmt.Println(cfg.Nothing) + +} diff --git a/examples/nodefaultfile/main_test.go b/examples/nodefaultfile/main_test.go new file mode 100644 index 0000000..e47c10a --- /dev/null +++ b/examples/nodefaultfile/main_test.go @@ -0,0 +1,12 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_main(t *testing.T) { + main() + assert.True(t, true) +} diff --git a/examples/nodefaultfile/nodefaultfile b/examples/nodefaultfile/nodefaultfile new file mode 100755 index 0000000..008aa76 Binary files /dev/null and b/examples/nodefaultfile/nodefaultfile differ diff --git a/examples/simple/main_test.go b/examples/simple/main_test.go new file mode 100644 index 0000000..e47c10a --- /dev/null +++ b/examples/simple/main_test.go @@ -0,0 +1,12 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_main(t *testing.T) { + main() + assert.True(t, true) +} diff --git a/flags.go b/flags.go index ca7bb9c..d4825c4 100644 --- a/flags.go +++ b/flags.go @@ -25,26 +25,46 @@ var ( printConfFlagName = "print-conf" ) +/* +Set config package's global FlagSet. +*/ func SetFlagSet(f *flag.FlagSet) { flagSet = f _ = flagSet.Bool(writeConfFlagName, false, "writes default configuration to default file. if default file already exists, options of overwrite, show and abort are given. ") - _ = flagSet.Bool(printConfFlagName, false, " prints configuration for current run. if combined with write-def-conf the print format is that of default file.") + _ = flagSet.Bool(printConfFlagName, false, "prints configuration for current run. if combined with write-def-conf the print format is that of default file.") } +/* +Set a list of flags to parse. As default, args is set os os.Args[1:], the command-line args. + +SetFlagSetArgs is particularly useful for testing. +*/ func SetFlagSetArgs(args []string) { flagSetArgs = args } +/* +Returns a map of all flag defaults. The key is the flag name and the value the flag's default value. +*/ func GetDefaultFlags() map[string]interface{} { return flag_defaults } +/* +Returns the Flag structure of the named flag of the global flag set, returning nil if none exists. By default, the global flag set is that of the command-line. +*/ func LookupFlag(name string) *flag.Flag { return flagSet.Lookup(name) } -//based on flag package's PrintDefaults() +/* +Usage prints a usage message documenting all defined command-line flags to the set FlagSet's output, which by default is os.Stderr. +It is based on the standard flag package's PrintDefaults() but includes two more default flags in addition to 'help': +write-def-conf (write to default config file) and print-conf (print current configuration to stdout). + +Usage is called when an error occurs while parsing flags. +*/ func Usage() { fmt.Fprintf(flagSet.Output(), "Usage of %s:\n", os.Args[0]) @@ -77,7 +97,7 @@ func Usage() { if f.Name != writeConfFlagName && f.Name != printConfFlagName { if !reflect.ValueOf(f.DefValue).IsZero() { - if IsString(f) { + if isString(f) { // put quotes on the value fmt.Fprintf(&b, " (default %q)", f.DefValue) } else { @@ -94,20 +114,28 @@ func Usage() { // Value type insert +/* +FlagValue is a wrapper for flag.Value, which stores the dynamic value of a flag, and information on if the flag has been parsed. +*/ type FlagValue struct { Value flag.Value parsed bool } +// Used for setting the value of a flag without setting flag to 'parsed' func (fv *FlagValue) defSet(val string) error { return fv.Value.Set(val) } +/* +Intercepts the standard flag package's Set(). +*/ func (fv *FlagValue) Set(val string) error { fv.parsed = true return fv.Value.Set(val) } +//Intercepts the standard flag package's String(). func (fv *FlagValue) String() string { var ret string if fv.Value != nil { @@ -116,14 +144,16 @@ func (fv *FlagValue) String() string { return ret } -func IsString(f *flag.Flag) bool { +// check if the value of type flag is string +func isString(f *flag.Flag) bool { ensureFlagValue(f) - fv := GetFlagValue(f) + fv := getFlagValue(f) val := reflect.Indirect(reflect.ValueOf(fv.Value)) return val.Kind() == reflect.String } -func GetFlagValue(f *flag.Flag) *FlagValue { +// get the FlagValue from a flag +func getFlagValue(f *flag.Flag) *FlagValue { if f.Value != nil { fv, ok := f.Value.(*FlagValue) if ok { @@ -139,6 +169,7 @@ func GetFlagValue(f *flag.Flag) *FlagValue { } +// ensure that the flag's Value has been exchanged for a FlagValue func ensureFlagValue(f *flag.Flag) (changed bool) { if f.Value == nil { err := errors.New("flag Value is nil") @@ -164,15 +195,21 @@ func ensureFlagValue(f *flag.Flag) (changed bool) { return } -// Special case for bool to enable using bool flags like switches +/* +Implements flag package's boolFlag interface. Special case to enable using bool flags like switches. +*/ type FlagValueBool struct { FlagValue } +//Intercepts the standard flag package's IsBoolFlag(). func (fvb *FlagValueBool) IsBoolFlag() bool { return true } // Flag Parsing +/* +Parses flags, stores all default flags in a list, and all parsed flags in another. +*/ func ParseFlags() error { flagSet.VisitAll(beforeParse()) err := flagSet.Parse(flagSetArgs) @@ -186,9 +223,12 @@ func ParseFlags() error { return err } +/* +Checks if a flag has been parsed. +*/ func ParsedFlag(f *flag.Flag) bool { ensureFlagValue(f) - fv := GetFlagValue(f) + fv := getFlagValue(f) if fv != nil { return fv.parsed } @@ -199,6 +239,9 @@ func addToFlagDefaults(f *flag.Flag, defVal string) { addFlagValueToMap(flag_defaults, f, defVal) } +/* +Set, or reset, the default value of a flag. +*/ func SetFlagDefault(fName, def string) error { f := LookupFlag(fName) if f == nil { @@ -207,7 +250,7 @@ func SetFlagDefault(fName, def string) error { addToFlagDefaults(f, def) ensureFlagValue(f) f.DefValue = def - fv := GetFlagValue(f) + fv := getFlagValue(f) if fv != nil { return fv.defSet(def) } @@ -215,18 +258,23 @@ func SetFlagDefault(fName, def string) error { return errors.New(errMsg) } +/* +before parsing flags: Add all flag default values to global flag default map +*/ func beforeParse() func(*flag.Flag) { return func(f *flag.Flag) { addToFlagDefaults(f, f.DefValue) } } +/* +after parsing flags: add all parsed flag's values to global flag value map +*/ func afterParse() func(*flag.Flag) { return func(f *flag.Flag) { if !flagSet.Parsed() { err := errors.New("flagSet not parsed") handleError(err) - // panic(errors.New("flagSet not parsed")) //TODO } if ParsedFlag(f) { if f.Name == printConfFlagName && f.Value.String() == "true" { @@ -246,7 +294,7 @@ func addFlagValueToMap(m map[string]interface{}, f *flag.Flag, value string) { //Get reflect.Kind of the data that's stored in the Flag ensureFlagValue(f) - fv := GetFlagValue(f) + fv := getFlagValue(f) if fv == nil { //TODO if debug log.Println("not FlagValue type: ", reflect.TypeOf(f.Value)) return diff --git a/flags_test.go b/flags_test.go index c7e4daa..6d79a70 100644 --- a/flags_test.go +++ b/flags_test.go @@ -90,9 +90,9 @@ func Test_FlagValueIsString(t *testing.T) { for k, f := range flags { if k != str { - assert.False(t, IsString(f)) + assert.False(t, isString(f)) } else { - assert.True(t, IsString(f)) + assert.True(t, isString(f)) } } } @@ -154,7 +154,7 @@ func Test_ParseBoolFLag(t *testing.T) { flagSet := testInit() fBool := flagSet.Bool(b, tt.defValue, "usage") - fmt.Println(tt.name, fBool) + //fmt.Println(tt.name, fBool) args := make([]string, 0) if tt.set { @@ -270,13 +270,13 @@ func Test_GetFlagValue(t *testing.T) { f := flagSet.Lookup(str) //Not - fv := GetFlagValue(f) + fv := getFlagValue(f) assert.Nil(t, fv) //FlagValue f.Value = &FlagValue{Value: f.Value} - fv = GetFlagValue(f) + fv = getFlagValue(f) assert.NotNil(t, fv) //FlagValueBool @@ -284,7 +284,7 @@ func Test_GetFlagValue(t *testing.T) { fvb.Value = fv.Value f.Value = fvb - fv = GetFlagValue(f) + fv = getFlagValue(f) assert.NotNil(t, fv) } @@ -295,7 +295,7 @@ func Test_ParsedFlag(t *testing.T) { assert.False(t, ParsedFlag(f)) ensureFlagValue(f) - fv := GetFlagValue(f) + fv := getFlagValue(f) fv.parsed = true assert.True(t, ParsedFlag(f)) @@ -504,9 +504,9 @@ func Test_IsString(t *testing.T) { iFlag := LookupFlag(i) strFlag := LookupFlag(str) - assert.False(t, IsString(iFlag)) - assert.True(t, IsString(strFlag)) - assert.False(t, IsString(bFlag)) + assert.False(t, isString(iFlag)) + assert.True(t, isString(strFlag)) + assert.False(t, isString(bFlag)) } diff --git a/parse.go b/parse.go index 023f84c..167448b 100644 --- a/parse.go +++ b/parse.go @@ -17,10 +17,12 @@ import ( "gopkg.in/yaml.v2" ) -var ( - defaultFile = "" -) +var defaultFile = "" +/* +Set default file. fpath must be absolute path. If the file cannot be opened, the function will return an error. Note that the error will only be return if +the error handling mode is set to ContinueOnError, else the function will Panic or Exit depending on the mode. +*/ func SetDefaultFile(fpath string) (err error) { defaultFile = fpath @@ -42,12 +44,17 @@ func GetDefaultFile() string { var ( ErrNoDefaultConfig = errors.New("no default config file to parse") ErrFailedToParseDefaultConfig = fmt.Errorf("failed to parse default config (%s)", defaultFile) + ErrNotAPointer = errors.New("argument to must be a pointer") ErrInvalidConfigFile = errors.New("unsupported or invalid file") ErrInvalidFormat = errors.New("invalid format of file") ErrNoConfigFileToParse = errors.New("no file given to parse") - ErrNoFileFound = syscall.Errno(2) //errors.New("could not find file") + ErrNoFileFound = syscall.Errno(2) // "could not find file" ) +/* +Compounds errors. Used rather than errors.Wrap since there's no hierarchy in the errors; + errors can stack up one another without one being dependant on one another. +*/ func addErr(prev error, add error) error { if prev == nil { return errors.New(add.Error()) @@ -55,10 +62,14 @@ func addErr(prev error, add error) error { return errors.New(prev.Error() + ", " + add.Error()) } -// cfg must be a pointer +/* +Parse the default config fiĺe into the value pointed to by cfg. Returns error regardless of error handling mode. + +If cfg is not a pointer, ParseDefaultConfigFile returns an ErrNotAPointer. +*/ func ParseDefaultConfigFile(cfg interface{}) (err error) { - if reflect.TypeOf(cfg).Kind() != reflect.Ptr { - err = errors.New("struct given to ParseDefaultConfigFile must be of type pointer") + if reflect.TypeOf(cfg).Kind() != reflect.Ptr { //TODO: to a struct, map or list? + err = fmt.Errorf("[ParseDefaultConfigFile]: %w ", ErrNotAPointer) return } @@ -82,10 +93,16 @@ func ParseDefaultConfigFile(cfg interface{}) (err error) { return } -// cfg must be a pointer +/* +Parse the given config fiĺe into the value pointed to by cfg. Returns error regardless of error handling scheme. + +If cfg is not a pointer, ParseConfigFile returns an ErrNotAPointer. + +The 'filename' must either be an absolute path to the config file, exist in the current working directory, or in one of the directories given as 'dirs'. If the given file cannot be found, ParseConfig file returns an ErrNoConfigFileToParse. +*/ func ParseConfigFile(cfg interface{}, filename string, dirs ...string) (err error) { if reflect.TypeOf(cfg).Kind() != reflect.Ptr { - err = errors.New("struct given to ParseDefaultConfigFile must be of type pointer") + err = fmt.Errorf("[ParseConfigFile]: %w ", ErrNotAPointer) return } @@ -95,7 +112,7 @@ func ParseConfigFile(cfg interface{}, filename string, dirs ...string) (err erro } // Parse default file first -- it's ok if it fails - err = ParseDefaultConfigFile(cfg) + ParseDefaultConfigFile(cfg) // If not found as is, check through relevant directories found := true @@ -155,10 +172,9 @@ func decode(cfg interface{}, f *os.File, filename string) (err error) { return } -// cfg must be a pointer func writeToDefaultFile(cfg interface{}) (err error) { if defaultFile == "" { - fmt.Println("WARNING! No default file path set") + fmt.Println("WARNING! Trying to write to default file but no default file path set") osExit(1) } else { var write bool diff --git a/parse_test.go b/parse_test.go index 92a528c..23603eb 100644 --- a/parse_test.go +++ b/parse_test.go @@ -325,7 +325,7 @@ func Test_encode(t *testing.T) { _, err = encode(tt.cfg, tt.filename) if tt.expectedError == nil { err = ParseConfigFile(parsed, tt.filename) - assert.Equal(t, ErrNoDefaultConfig, err) + assert.Nil(t, err) } else { assert.NotNil(t, err) assert.Contains(t, err.Error(), tt.expectedError.Error())