commit 6a207556cca57312cf50282ca71961664260505b Author: geek Date: Tue Apr 3 17:43:02 2018 +0900 project init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3733e36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +### Go template +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ +.idea/ +*.iml + +vendor/ +glide.lock +.DS_Store +dist/ +debug diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2ca2b1d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug", + "type": "go", + "request": "launch", + "mode": "debug", + "remotePath": "", + "port": 2345, + "host": "127.0.0.1", + "program": "${workspaceRoot}/main.go", + "env": {}, + "args": [], + "showLog": true + }, + { + "name": "File Debug", + "type": "go", + "request": "launch", + "mode": "debug", + "remotePath": "", + "port": 2345, + "host": "127.0.0.1", + "program": "${fileDirname}", + "env": {}, + "args": [], + "showLog": true + } + + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..20af2f6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +// Place your settings in this file to overwrite default and user settings. +{ +} \ No newline at end of file diff --git a/configuration.go b/configuration.go new file mode 100644 index 0000000..306faca --- /dev/null +++ b/configuration.go @@ -0,0 +1,138 @@ +package configuration + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" +) + +const ( + ConfigEnvPrefix = "CONFIG_ENV_PREFIX" + ConfigTagEnv = "env" +) + +type Configuration interface { + SetConfigPath(in string) error + Load(target interface{}, files ...string) error + LoadReader(target interface{}, ext string, in io.Reader) error + + SetEnvPrefix(in string) +} + +type config struct { + configPath string + // Name of file to look for inside the path + envPrefix string +} + +func New() Configuration { + return &config{} +} + +var _c *config + +func init() { + _c = New().(*config) +} + +// SetConfigPath set a path to search for the config file in. +func SetConfigPath(in string) error { return _c.SetConfigPath(in) } +func (c *config) SetConfigPath(in string) error { + if in != "" { + absin, err := ABSPathify(in) + if nil != err { + return err + } + + c.configPath = absin + } + return nil +} + +// SetEnvPrefix set a prefix to search for the env variable. +func SetEnvPrefix(in string) { _c.SetEnvPrefix(in) } +func (c *config) SetEnvPrefix(in string) { + if in != "" { + c.envPrefix = in + } +} + +// Load will unmarshal from configuration file from disk +func Load(target interface{}, files ...string) error { + return _c.Load(target, files...) +} +func (c *config) Load(target interface{}, files ...string) error { + filenames := c.getConfigFiles(files...) + + if 0 == len(filenames) { + return fmt.Errorf("Config: have no config files in [%v]", files) + } + + for _, file := range filenames { + if err := unmarshalFile(target, file); err != nil { + return err + } + } + + return unmarshalTags(target, c.getENVPrefix()) +} + +// LoadReader will unmarshal from configuration bytes +func LoadReader(target interface{}, ext string, in io.Reader) error { + return _c.LoadReader(target, ext, in) +} +func (c *config) LoadReader(target interface{}, ext string, in io.Reader) error { + buf := new(bytes.Buffer) + buf.ReadFrom(in) + + return unmarshalData(target, ext, buf.Bytes()) +} + +// Save store to configuration file from disk +func Save(target interface{}, file string, overWrite bool) error { + var absPath string + var err error + if absPath, err = ABSPathify(file); nil != err { + return err + } + + return marshalFile(target, absPath, overWrite) +} + +// 1. file +// 2. configPath/file +func (c *config) getConfigFiles(files ...string) []string { + var results []string + + for _, file := range files { + // check configuration + if absin, err := ABSPathify(file); nil == err { + if Exists(absin) { + results = append(results, absin) + } + } + } + + for _, file := range files { + // check configuration + if absin, err := ABSPathify(filepath.Join(c.configPath, file)); nil == err { + if Exists(absin) { + results = append(results, absin) + } + } + } + + return results +} + +func (c *config) getENVPrefix() string { + if c.envPrefix == "" { + if prefix := os.Getenv(ConfigEnvPrefix); prefix != "" { + return prefix + } + return "" + } + return c.envPrefix +} diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000..984ee01 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,2 @@ +package: git.loafle.net/commons/configuration-go +import: [] diff --git a/util.go b/util.go new file mode 100644 index 0000000..9eef1c6 --- /dev/null +++ b/util.go @@ -0,0 +1,194 @@ +package configuration + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + + "github.com/BurntSushi/toml" + yaml "gopkg.in/yaml.v2" +) + +func ABSPathify(inPath string) (string, error) { + if strings.HasPrefix(inPath, "$HOME") { + inPath = userHomeDir() + inPath[5:] + } + + if strings.HasPrefix(inPath, "$") { + end := strings.Index(inPath, string(os.PathSeparator)) + inPath = os.Getenv(inPath[1:end]) + inPath[end:] + } + + if filepath.IsAbs(inPath) { + return filepath.Clean(inPath), nil + } + + p, err := filepath.Abs(inPath) + if err == nil { + return filepath.Clean(p), nil + } + + return "", err +} + +func userHomeDir() string { + if runtime.GOOS == "windows" { + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if home == "" { + home = os.Getenv("USERPROFILE") + } + return home + } + return os.Getenv("HOME") +} + +func Exists(path string) bool { + if fileInfo, err := os.Stat(path); err == nil && fileInfo.Mode().IsRegular() { + return true + } + return false +} + +func unmarshalFile(target interface{}, file string) error { + data, err := ioutil.ReadFile(file) + if err != nil { + return err + } + + return unmarshalData(target, filepath.Ext(file), data) +} + +func unmarshalData(target interface{}, ext string, data []byte) error { + switch ext { + case ".yaml", ".yml": + return yaml.Unmarshal(data, target) + case ".toml": + return toml.Unmarshal(data, target) + case ".json": + return json.Unmarshal(data, target) + default: + if toml.Unmarshal(data, target) != nil { + if json.Unmarshal(data, target) != nil { + if yaml.Unmarshal(data, target) != nil { + return errors.New("failed to decode config") + } + } + } + return nil + } +} + +func unmarshalTags(target interface{}, prefixes ...string) error { + targetValue := reflect.Indirect(reflect.ValueOf(target)) + if targetValue.Kind() != reflect.Struct { + return errors.New("invalid config, should be struct") + } + + targetType := targetValue.Type() + for i := 0; i < targetType.NumField(); i++ { + var ( + envNames []string + fieldStruct = targetType.Field(i) + field = targetValue.Field(i) + envName = fieldStruct.Tag.Get(ConfigTagEnv) // read configuration from shell env + ) + if !field.CanAddr() || !field.CanInterface() { + continue + } + if envName == "" { + envNames = append(envNames, strings.Join(append(prefixes, fieldStruct.Name), "_")) // Configor_DB_Name + envNames = append(envNames, strings.ToUpper(strings.Join(append(prefixes, fieldStruct.Name), "_"))) // CONFIGOR_DB_NAME + } else { + envNames = []string{envName} + } + // Load From Shell ENV + for _, env := range envNames { + if value := os.Getenv(env); value != "" { + if err := yaml.Unmarshal([]byte(value), field.Addr().Interface()); err != nil { + return err + } + break + } + } + if isBlank := reflect.DeepEqual(field.Interface(), reflect.Zero(field.Type()).Interface()); isBlank { + // Set default configuration if blank + if value := fieldStruct.Tag.Get("default"); value != "" { + if err := yaml.Unmarshal([]byte(value), field.Addr().Interface()); err != nil { + return err + } + } else if fieldStruct.Tag.Get("required") == "true" { + // return error if it is required but blank + return fmt.Errorf("Field[%s] is required", fieldStruct.Name) + } + } + for field.Kind() == reflect.Ptr { + field = field.Elem() + } + if field.Kind() == reflect.Struct { + if err := unmarshalTags(field.Addr().Interface(), getPrefixForStruct(prefixes, &fieldStruct)...); err != nil { + return err + } + } + + if field.Kind() == reflect.Slice { + for i := 0; i < field.Len(); i++ { + if reflect.Indirect(field.Index(i)).Kind() == reflect.Struct { + if err := unmarshalTags(field.Index(i).Addr().Interface(), append(getPrefixForStruct(prefixes, &fieldStruct), fmt.Sprint(i))...); err != nil { + return err + } + } + } + } + } + + return nil +} + +func getPrefixForStruct(prefixes []string, fieldStruct *reflect.StructField) []string { + if fieldStruct.Anonymous && fieldStruct.Tag.Get("anonymous") == "true" { + return prefixes + } + return append(prefixes, fieldStruct.Name) +} + +func marshalFile(target interface{}, file string, overWrite bool) error { + var err error + + var b []byte + if b, err = marshal(target, filepath.Ext(file)); nil != err { + return err + } + + if Exists(file) { + if !overWrite { + return fmt.Errorf("Config: File[%s] is exist", file) + } + } + + return ioutil.WriteFile(file, b, 0644) +} + +func marshal(target interface{}, ext string) ([]byte, error) { + switch ext { + case ".yaml", ".yml": + return yaml.Marshal(target) + case ".toml": + var buf bytes.Buffer + enc := toml.NewEncoder(&buf) + if err := enc.Encode(target); nil != err { + return nil, err + } + return buf.Bytes(), nil + case ".json": + return json.MarshalIndent(target, "", "\t") + default: + return nil, fmt.Errorf("Config: Not supported extention[%s]", ext) + } +}