package conf import ( "errors" "fmt" "net/url" "os" "reflect" "strings" ) // ErrInvalidStruct indicates that a configuration struct is not the correct type. var ErrInvalidStruct = errors.New("configuration must be a struct pointer") // A FieldError occurs when an error occurs updating an individual field // in the provided struct value. type FieldError struct { fieldName string typeName string value string err error } func (err *FieldError) Error() string { return fmt.Sprintf("conf: error assigning to field %s: converting '%s' to type %s. details: %s", err.fieldName, err.value, err.typeName, err.err) } // Sourcer provides the ability to source data from a configuration source. // Consider the use of lazy-loading for sourcing large datasets or systems. type Sourcer interface { // Source takes the field key and attempts to locate that key in its // configuration data. Returns true if found with the value. Source(fld Field) (string, bool) } // Version provides the abitily to add version and description to the application. type Version struct { SVN string Desc string } // ParseOSArgs parses the configuration allowing command line // arguments to override settings. Function returns ErrHelpWanted // for any information to be provided to the user. func ParseOSArgs(prefix string, cfg interface{}) (string, error) { err := Parse(os.Args[1:], prefix, cfg) if err == nil { return "", nil } switch err { case ErrHelpWanted: usage, err := Usage(prefix, cfg) if err != nil { return "", fmt.Errorf("generating config usage: %w", err) } return usage, ErrHelpWanted case ErrVersionWanted: version, err := VersionString(prefix, cfg) if err != nil { return "", fmt.Errorf("generating config version: %w", err) } return version, ErrHelpWanted } return "", fmt.Errorf("parsing config: %w", err) } // Parse parses configuration into the provided struct. func Parse(args []string, namespace string, cfgStruct interface{}, sources ...Sourcer) error { // Create the flag source. flag, err := newSourceFlag(args) if err != nil { return err } // Append default sources to any provided list. sources = append(sources, newSourceEnv(namespace)) sources = append(sources, flag) // Get the list of fields from the configuration struct to process. fields, err := extractFields(nil, cfgStruct) if err != nil { return err } if len(fields) == 0 { return errors.New("no fields identified in config struct") } // Process all fields found in the config struct provided. for _, field := range fields { // If the field is supposed to hold the leftover args then copy them in // from the flags source. if field.Field.Type() == argsT { args := reflect.ValueOf(Args(flag.args)) field.Field.Set(args) continue } // Set any default value into the struct for this field. if field.Options.DefaultVal != "" { if err := processField(field.Options.DefaultVal, field.Field); err != nil { return &FieldError{ fieldName: field.Name, typeName: field.Field.Type().String(), value: field.Options.DefaultVal, err: err, } } } // Process each field against all sources. var everProvided bool for _, sourcer := range sources { if sourcer == nil { continue } value, provided := sourcer.Source(field) if !provided { continue } everProvided = true // A value was found so update the struct value with it. if err := processField(value, field.Field); err != nil { return &FieldError{ fieldName: field.Name, typeName: field.Field.Type().String(), value: value, err: err, } } } // If this key is not provided by any source, check if it was // required to be provided. if !everProvided && field.Options.Required { return fmt.Errorf("required field %s is missing value", field.Name) } } return nil } // Usage provides output to display the config usage on the command line. func Usage(namespace string, v interface{}) (string, error) { fields, err := extractFields(nil, v) if err != nil { return "", err } return fmtUsage(namespace, fields), nil } // VersionString provides output to display the application version and description on the command line. func VersionString(namespace string, v interface{}) (string, error) { fields, err := extractFields(nil, v) if err != nil { return "", err } var str strings.Builder for i := range fields { if fields[i].Name == versionKey && fields[i].Field.Len() > 0 { str.WriteString("Version: ") str.WriteString(fields[i].Field.String()) continue } if fields[i].Name == descKey && fields[i].Field.Len() > 0 { if str.Len() > 0 { str.WriteString("\n") } str.WriteString(fields[i].Field.String()) break } } return str.String(), nil } // String returns a stringified version of the provided conf-tagged // struct, minus any fields tagged with `noprint`. func String(v interface{}) (string, error) { fields, err := extractFields(nil, v) if err != nil { return "", err } var s strings.Builder for i, fld := range fields { if fld.Options.Noprint { continue } s.WriteString(flagUsage(fld)) s.WriteString("=") v := fmt.Sprintf("%v", fld.Field.Interface()) switch { case fld.Options.Mask: if u, err := url.Parse(v); err == nil { userPass := u.User.String() if userPass != "" { v = strings.Replace(v, userPass, "xxxxxx:xxxxxx", 1) s.WriteString(v) break } } s.WriteString("xxxxxx") default: s.WriteString(v) } if i < len(fields)-1 { s.WriteString("\n") } } return s.String(), nil } // Args holds command line arguments after flags have been parsed. type Args []string // argsT is used by Parse and Usage to detect struct fields of the Args type. var argsT = reflect.TypeOf(Args{}) // Num returns the i'th argument in the Args slice. It returns an empty string // the request element is not present. func (a Args) Num(i int) string { if i < 0 || i >= len(a) { return "" } return a[i] }