/* * Copyright (C) 2017 eschao * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package config import ( "encoding/json" "errors" "flag" "fmt" "os" "path/filepath" "reflect" "gopkg.in/yaml.v3" ) // Default configuration file const ( DefaultJSONConfig = "config.json" DefaultYamlConfig = "config.yaml" DefaultPropConfig = "config.properties" ) const ( JSONConfigType = "json" YamlConfigType = "yaml" PropConfigType = "properties" ) var ( errExpectStructPointerInsteadOf = errors.New("expect a structure pointer type instead of") errFileNotFound = errors.New("file not found") errNotImplemented = errors.New("not implemented") errUnsupportedConfigFile = errors.New("unsupported config file") errUnsupportedType = errors.New("unsupported type") errValueCannotBeChanged = errors.New("value cannot be changed") ) // ParseDefault parses the given structure, extract default value from its tag // and set structure with these values. // Normally, ParseDefault should be called before any other parsing functions // to set default values for structure. func ParseDefault(i interface{}) error { ptrRef := reflect.ValueOf(i) if ptrRef.IsNil() || ptrRef.Kind() != reflect.Ptr { return fmt.Errorf("%w: %s", errExpectStructPointerInsteadOf, ptrRef.Kind().String()) } valueOfStruct := ptrRef.Elem() if valueOfStruct.Kind() != reflect.Struct { return fmt.Errorf("%w: %s", errExpectStructPointerInsteadOf, valueOfStruct.Kind().String()) } return parseValue(valueOfStruct) } func parseValue(value reflect.Value) error { var err error typeOfStruct := value.Type() for i := 0; i < value.NumField() && err == nil; i++ { valueOfField := value.Field(i) kindOfField := valueOfField.Kind() structOfField := typeOfStruct.Field(i) if kindOfField == reflect.Ptr { if !valueOfField.IsNil() && valueOfField.CanSet() { err = ParseDefault(valueOfField.Interface()) } else { continue } } else if kindOfField == reflect.Struct { err = parseValue(valueOfField) } defValue, ok := structOfField.Tag.Lookup("default") if !ok { continue } err = setValue(valueOfField, defValue, structOfField) } return err } func setValue(value reflect.Value, defValue string, field reflect.StructField) error { if setUnmarshalTextValue(value, defValue) { return nil } if ok, err := setDurationValue(value, defValue); ok { return err } var err error switch value.Kind() { case reflect.Bool: err = setValueWithBool(value, defValue) case reflect.String: value.SetString(defValue) case reflect.Int8: err = setValueWithIntX(value, defValue, Int8Size) case reflect.Int16: err = setValueWithIntX(value, defValue, Int16Size) case reflect.Int, reflect.Int32: err = setValueWithIntX(value, defValue, Int32Size) case reflect.Int64: err = setValueWithIntX(value, defValue, Int64Size) case reflect.Uint8: err = setValueWithUintX(value, defValue, Uint8Size) case reflect.Uint16: err = setValueWithUintX(value, defValue, Uint16Size) case reflect.Uint, reflect.Uint32: err = setValueWithUintX(value, defValue, Uint32Size) case reflect.Uint64: err = setValueWithUintX(value, defValue, Uint64Size) case reflect.Float32: err = setValueWithFloatX(value, defValue, Float32Size) case reflect.Float64: err = setValueWithFloatX(value, defValue, Float64Size) case reflect.Slice: sp, ok := field.Tag.Lookup("separator") if !ok { sp = ":" } err = setValueWithSlice(value, defValue, sp) default: err = fmt.Errorf("%w: %s", errUnsupportedType, value.Kind().String()) } return err } // ParseCli parses given structure interface and set it with command line input func ParseCli(out interface{}, name string, args []string) ([]string, error) { cli := NewCLI(name) if err := cli.Init(out); err != nil { return nil, err } err := cli.Parse(args) return cli.Args, err } // ParseConfig parses given structure interface and set it with default // configuration file. // The configFlag is a command line flag to tell where to locate configure file. // If the config file doesn't exist, the default config fill will be searched // under the same folder with the fixed order: config.json, config.yaml and // config.properties func ParseConfig(out interface{}, configFlag string) error { configFile := flag.String(configFlag, "", "Specify configuration file") flag.Parse() return ParseConfigFile(out, *configFile) } // ParseConfigFile parses given structure interface and set its value with // the specified configuration file func ParseConfigFile(out interface{}, configFile string) error { var err error if configFile == "" { configFile, err = getDefaultConfigFile() if err != nil { return err } } configType, err := getConfigFileType(configFile) if err != nil { return err } switch configType { case JSONConfigType: err = parseJSON(out, configFile) case YamlConfigType: err = parseYaml(out, configFile) case PropConfigType: err = parseProp(out, configFile) default: err = fmt.Errorf("%w: %s", errUnsupportedConfigFile, configFile) } return err } // parseJSON parses JSON file and set structure with its value func parseJSON(out interface{}, jsonFile string) error { raw, err := os.ReadFile(jsonFile) if err != nil { return fmt.Errorf("open json config file: %w", err) } if err := json.Unmarshal(raw, out); err != nil { return fmt.Errorf("json unmarshal: %w", err) } return nil } // parseYaml parses Yaml file and set structure with its value func parseYaml(out interface{}, yamlFile string) error { raw, err := os.ReadFile(yamlFile) if err != nil { return fmt.Errorf("open yaml config file: %w", err) } if err := yaml.Unmarshal(raw, out); err != nil { return fmt.Errorf("unmarshal yaml: %w", err) } return nil } // parseProp parses Properties file and set structure with its value func parseProp(_ interface{}, _ /* The propFile */ string) error { return fmt.Errorf("%w: properties config", errNotImplemented) } // getDefaultConfigFile returns a existing default config file. The checking // order is fixed with beginning from: config.json to config.yaml and // config.properties func getDefaultConfigFile() (string, error) { exe, err := os.Executable() if err != nil { return "", fmt.Errorf("%w: %w", errFileNotFound, err) } path := filepath.Dir(exe) + string(filepath.Separator) // Check json config jsonConfig := path + DefaultJSONConfig if _, err := os.Stat(jsonConfig); err == nil { return jsonConfig, nil } // Check yaml config yamlConfig := path + DefaultYamlConfig if _, err := os.Stat(yamlConfig); err == nil { return yamlConfig, nil } // Check prop config propConfig := path + DefaultPropConfig if _, err := os.Stat(propConfig); err == nil { return propConfig, nil } return "", fmt.Errorf("default config %w in path: %s", errFileNotFound, path) } // getConfigFileType analyzes config file extension name and return // corresponding type: json, yaml or properties func getConfigFileType(configFile string) (string, error) { ext := filepath.Ext(configFile) switch ext { case ".json": return JSONConfigType, nil case ".yaml": return YamlConfigType, nil case ".properties", ".prop": return PropConfigType, nil } return "", fmt.Errorf("%w: %s", errUnsupportedConfigFile, configFile) }