first version

This commit is contained in:
Jay 2020-06-05 10:11:45 +00:00
commit f8942f3d43
7 changed files with 482 additions and 0 deletions

9
go.mod Normal file
View File

@ -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
)

8
go.sum Normal file
View File

@ -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=

278
loader.go Normal file
View File

@ -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)
}

152
loader_test.go Normal file
View File

@ -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)
})
}
}

11
test/config.json Normal file
View File

@ -0,0 +1,11 @@
{
"strKey": "string value",
"intKey": 1000,
"boolKey": true,
"floatKey": 1.2345,
"strArr": ["arr1"],
"objKey": {
"name": "name"
},
"arrObj": [{"key": "val"}]
}

13
test/config.toml Normal file
View File

@ -0,0 +1,13 @@
strKey = "string value"
intKey = 1000
boolKey = true
floatKey = 1.2345
strArr = [
"arr1"
]
[objKey]
name = "name"
[[arrObj]]
key = "val"

11
test/config.yaml Normal file
View File

@ -0,0 +1,11 @@
strKey: string value
intKey: 1000
boolKey: true
floatKey: 1.2345
strArr:
- arr1
objKey:
name: name
arrObj:
- key: val