From f8942f3d43b63f4e6b545027d4a2b2fddad01a25 Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 5 Jun 2020 10:11:45 +0000 Subject: [PATCH] first version --- go.mod | 9 ++ go.sum | 8 ++ loader.go | 278 +++++++++++++++++++++++++++++++++++++++++++++++ loader_test.go | 152 ++++++++++++++++++++++++++ test/config.json | 11 ++ test/config.toml | 13 +++ test/config.yaml | 11 ++ 7 files changed, 482 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 loader.go create mode 100644 loader_test.go create mode 100644 test/config.json create mode 100644 test/config.toml create mode 100644 test/config.yaml diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a68b5cd --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.trj.tw/golang/config-loader + +go 1.14 + +require ( + git.trj.tw/golang/utils v0.0.0-20190225142552-b019626f0349 + github.com/BurntSushi/toml v0.3.1 + gopkg.in/yaml.v2 v2.3.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a50e755 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +git.trj.tw/golang/utils v0.0.0-20190225142552-b019626f0349 h1:V6ifeiJ3ExnjaUylTOz37n6z5uLwm6fjKjnztbTCaQI= +git.trj.tw/golang/utils v0.0.0-20190225142552-b019626f0349/go.mod h1:yE+qbsUsijCTdwsaQRkPT1CXYk7ftMzXsCaaYx/0QI0= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/loader.go b/loader.go new file mode 100644 index 0000000..9deffd0 --- /dev/null +++ b/loader.go @@ -0,0 +1,278 @@ +package confloader + +import ( + "encoding/json" + "errors" + "io/ioutil" + "reflect" + "strconv" + + "git.trj.tw/golang/utils" + "github.com/BurntSushi/toml" + "gopkg.in/yaml.v2" +) + +type ConfigFileType int + +const ( + ConfigFileTypeJSON ConfigFileType = iota + ConfigFileTypeYAML + ConfigFileTypeTOML +) + +type ConfigFile struct { + Type ConfigFileType + Path string +} + +type LoadOptions struct { + ConfigFile *ConfigFile +} + +func Load(i interface{}, opts *LoadOptions) error { + t := reflect.TypeOf(i) + if t.Kind() != reflect.Ptr { + return errors.New("input arg not ptr") + } + + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return errors.New("input not a struct") + } + + // load default value + LoadDefaultIntoStruct(i) + + // not config file opts, return + if opts == nil { + return nil + } + + // no config file + if opts.ConfigFile == nil { + return nil + } + + if opts.ConfigFile.Path == "" { + return errors.New("config file path empty") + } + + // resolve file path + opts.ConfigFile.Path = utils.ParsePath(opts.ConfigFile.Path) + // check file exists + if !utils.CheckExists(opts.ConfigFile.Path, false) { + return errors.New("config file not found") + } + + filebyte, err := ioutil.ReadFile(opts.ConfigFile.Path) + if err != nil { + return err + } + + switch opts.ConfigFile.Type { + case ConfigFileTypeJSON: + err := json.Unmarshal(filebyte, i) + if err != nil { + return err + } + break + case ConfigFileTypeTOML: + err := toml.Unmarshal(filebyte, i) + if err != nil { + return err + } + break + case ConfigFileTypeYAML: + err := yaml.Unmarshal(filebyte, i) + if err != nil { + return err + } + break + default: + return errors.New("file type not impl") + } + + return nil +} + +func LoadDefaultIntoStruct(i interface{}) { + t := reflect.ValueOf(i) + + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + + // not struct skip + if t.Kind() != reflect.Struct { + return + } + + fieldLen := t.NumField() + for idx := 0; idx < fieldLen; idx++ { + v := t.Field(idx) + f := t.Type().Field(idx) + + val, tagExists := f.Tag.Lookup("default") + + if v.Type().Kind() == reflect.Slice { + minLen := 0 + if defLen := f.Tag.Get("length"); defLen != "" { + if convInt, err := strconv.ParseInt(defLen, 10, 64); err == nil { + minLen = int(convInt) + } + } + if minLen < 1 { + return + } + + val, tagExists := f.Tag.Lookup("default") + + slice := reflect.MakeSlice(f.Type, minLen, minLen) + + item := reflect.Indirect(slice.Index(0)) + + if item.Type().Kind() == reflect.Slice { + //slice in slice skip proc + } else if item.Type().Kind() == reflect.Struct { + LoadDefaultIntoStruct(item.Addr().Interface()) + } else { + if tagExists { + procValue(item, val) + } + } + + for i := 0; i < slice.Len(); i++ { + slice.Index(i).Set(item) + } + v.Set(slice) + } else if v.Type().Kind() == reflect.Struct { + LoadDefaultIntoStruct(v.Addr().Interface()) + } else { + if tagExists { + procValue(v, val) + } + } + } +} + +func procValue(v reflect.Value, val string) { + if !v.IsValid() || !v.CanSet() { + return + } + switch v.Type().Kind() { + case reflect.String: + v.SetString(val) + break + case reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64: + if convInt, err := strconv.ParseInt(val, 10, 64); err == nil { + if !v.OverflowInt(convInt) { + v.SetInt(convInt) + } + } + break + case reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64: + if convUint, err := strconv.ParseUint(val, 10, 64); err == nil { + if !v.OverflowUint(convUint) { + v.SetUint(convUint) + } + } + break + case reflect.Float32: + case reflect.Float64: + if convFloat, err := strconv.ParseFloat(val, 64); err == nil { + if !v.OverflowFloat(convFloat) { + v.SetFloat(convFloat) + } + } + break + case reflect.Bool: + if convBool, err := strconv.ParseBool(val); err == nil { + v.SetBool(convBool) + } + break + } +} + +func procSlice(field *reflect.StructField) { + minLen := 0 + if defLen := field.Tag.Get("length"); defLen != "" { + if convInt, err := strconv.ParseInt(defLen, 10, 64); err == nil { + minLen = int(convInt) + } + } + if minLen < 1 { + return + } + + val := field.Tag.Get("default") + + slice := reflect.MakeSlice(field.Type, minLen, minLen) + + item := reflect.Indirect(slice.Index(0)) + + switch item.Kind() { + case reflect.String: + for i := 0; i < slice.Len(); i++ { + slice.Index(i).Set(reflect.ValueOf(val)) + } + break + case reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64: + if convInt, err := strconv.ParseInt(val, 10, 64); err == nil { + if !slice.Index(0).OverflowInt(convInt) { + for i := 0; i < slice.Len(); i++ { + slice.Index(i).Set(reflect.ValueOf(convInt)) + } + } + } + break + case reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64: + if convUint, err := strconv.ParseUint(val, 10, 64); err == nil { + if !slice.Index(0).OverflowUint(convUint) { + for i := 0; i < slice.Len(); i++ { + slice.Index(i).Set(reflect.ValueOf(convUint)) + } + } + } + break + case reflect.Float32, + reflect.Float64: + if convFloat, err := strconv.ParseFloat(val, 64); err == nil { + if !slice.Index(0).OverflowFloat(convFloat) { + for i := 0; i < slice.Len(); i++ { + slice.Index(i).Set(reflect.ValueOf(convFloat)) + } + } + } + break + case reflect.Bool: + if conv, err := strconv.ParseBool(val); err == nil { + for i := 0; i < slice.Len(); i++ { + slice.Index(i).Set(reflect.ValueOf(conv)) + } + } + break + case reflect.Struct: + break + } + + v := reflect.ValueOf(field) + v.Set(slice) +} diff --git a/loader_test.go b/loader_test.go new file mode 100644 index 0000000..9430508 --- /dev/null +++ b/loader_test.go @@ -0,0 +1,152 @@ +package confloader + +import ( + "reflect" + "testing" +) + +func TestLoad(t *testing.T) { + type ObjKey struct { + Name string `json:"name" yaml:"name" toml:"name"` + } + type ArrObj struct { + Key string `json:"key" yaml:"key" toml:"key"` + } + type Config struct { + StrKey string `json:"strKey" yaml:"strKey" toml:"strKey" default:"def value"` + IntKey int `json:"intKey" yaml:"intKey" toml:"intKey"` + BoolKey bool `json:"boolKey" yaml:"boolKey" toml:"boolKey"` + FloatKey float64 `json:"floatKey" yaml:"floatKey" toml:"floatKey"` + StrArr []string `json:"strArr" yaml:"strArr" toml:"strArr" default:"arrval" length:"1"` + StrArr2 []string `json:"strArr2" yaml:"strArr2" toml:"strArr2" default:"arrval2" length:"2"` + ObjKey ObjKey `json:"objKey" yaml:"objKey" toml:"objKey"` + ArrObj []ArrObj `json:"arrObj" yaml:"arrObj" toml:"arrObj"` + } + + expected := Config{ + StrKey: "string value", + IntKey: 1000, + BoolKey: true, + FloatKey: 1.2345, + StrArr: []string{"arr1"}, + StrArr2: []string{"arrval2", "arrval2"}, + ObjKey: ObjKey{ + Name: "name", + }, + ArrObj: []ArrObj{{Key: "val"}}, + } + + src := Config{} + + type args struct { + i interface{} + opts *LoadOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + // TODO: Add test cases. + { + name: "test load json file with default", + args: args{ + i: &src, + opts: &LoadOptions{ + ConfigFile: &ConfigFile{ + Type: ConfigFileTypeJSON, + Path: "./test/config.json", + }, + }, + }, + wantErr: false, + }, + { + name: "test load yaml file with default", + args: args{ + i: &src, + opts: &LoadOptions{ + ConfigFile: &ConfigFile{ + Type: ConfigFileTypeYAML, + Path: "./test/config.yaml", + }, + }, + }, + wantErr: false, + }, + { + name: "test load toml file with default", + args: args{ + i: &src, + opts: &LoadOptions{ + ConfigFile: &ConfigFile{ + Type: ConfigFileTypeTOML, + Path: "./test/config.toml", + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := Load(tt.args.i, tt.args.opts); (err != nil) != tt.wantErr { + t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(src, expected) { + t.Errorf("Load and expected not match") + } + }) + } +} + +func Test_loadDefaultIntoStruct(t *testing.T) { + type SubStruct struct { + S string `default:"sss"` + } + type ArrStruct struct { + S string `default:"inslice"` + } + + type SS struct { + S string `default:"str"` + I int `default:"123"` + B bool `default:"true"` + SS SubStruct + SL []string `default:"z" length:"1"` + SL2 []ArrStruct `length:"1"` + SL3 []int `default:"100" length:"3"` + } + + s := SS{} + expected := SS{ + S: "str", + I: 123, + B: true, + SS: SubStruct{S: "sss"}, + SL: []string{"z"}, + SL2: []ArrStruct{{S: "inslice"}}, + SL3: []int{100, 100, 100}, + } + + type args struct { + i interface{} + } + tests := []struct { + name string + args args + }{ + { + name: "test 1", + args: args{ + i: &s, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + LoadDefaultIntoStruct(tt.args.i) + reflect.DeepEqual(s, expected) + }) + } +} diff --git a/test/config.json b/test/config.json new file mode 100644 index 0000000..cd999d2 --- /dev/null +++ b/test/config.json @@ -0,0 +1,11 @@ +{ + "strKey": "string value", + "intKey": 1000, + "boolKey": true, + "floatKey": 1.2345, + "strArr": ["arr1"], + "objKey": { + "name": "name" + }, + "arrObj": [{"key": "val"}] +} diff --git a/test/config.toml b/test/config.toml new file mode 100644 index 0000000..b207d07 --- /dev/null +++ b/test/config.toml @@ -0,0 +1,13 @@ +strKey = "string value" +intKey = 1000 +boolKey = true +floatKey = 1.2345 +strArr = [ + "arr1" +] + +[objKey] +name = "name" + +[[arrObj]] +key = "val" diff --git a/test/config.yaml b/test/config.yaml new file mode 100644 index 0000000..f3abb02 --- /dev/null +++ b/test/config.yaml @@ -0,0 +1,11 @@ +strKey: string value +intKey: 1000 +boolKey: true +floatKey: 1.2345 +strArr: + - arr1 +objKey: + name: name +arrObj: + - key: val +