Skip to content

Commit

Permalink
feat: support squash
Browse files Browse the repository at this point in the history
  • Loading branch information
hui.wang committed Apr 15, 2022
1 parent 22e2485 commit e5e3fa6
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 21 deletions.
6 changes: 5 additions & 1 deletion default.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ func ParseDefault(valPtr interface{}, opts ...Option) (err error) {
func (x *XConf) parseDefault(valPtr interface{}) (data map[string]interface{}, parsed bool, err error) {
var flagVals []string
var keys []string
_, fieldInfo := NewStruct(reflect.New(reflect.ValueOf(valPtr).Type().Elem()).Interface(), x.cc.TagName, x.cc.TagNameForDefaultValue, x.cc.FieldTagConvertor).Map()
_, fieldInfo := NewStruct(
reflect.New(reflect.ValueOf(valPtr).Type().Elem()).Interface(),
x.cc.TagName,
x.cc.TagNameForDefaultValue,
x.cc.FieldTagConvertor).Map()
for k, v := range fieldInfo {
keys = append(keys, k)
if v.DefaultGot {
Expand Down
1 change: 1 addition & 0 deletions flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func xflagMapstructure(
if err := xf.Set(structPtr); err != nil {
return nil, fmt.Errorf("got error while xflag Set, err :%w ", err)
}
fmt.Println("validFieldPath:", validFieldPath, xf.FlagKeys())
dataArgs := args(xf)
err := xf.Parse(dataArgs)
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions gen_options_optiongen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 24 additions & 3 deletions structs_map_structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,30 @@ func (s *Struct) fillMapStructure(out map[string]interface{}, outPath map[string
if val.Kind() == reflect.Ptr && val.IsNil() {
val = reflect.New(val.Type().Elem())
}
v := reflect.ValueOf(val.Interface())
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
isSubStruct := false
switch v.Kind() {
case reflect.Map, reflect.Struct:
isSubStruct = true
}
squash := false
if tagOpts.Has("squash") && isSubStruct {
fullKey = prefix
squash = true
}
finalVal := s.nested(val, outPath, fullKey, fileNameNow)
out[name] = finalVal
if squash {
for k := range finalVal.(map[string]interface{}) {
out[k] = finalVal.(map[string]interface{})[k]
}
} else {
out[name] = finalVal
}
defaultVal, defaultValGot := field.Tag.Lookup(s.tagNameDefaultValue)
// 忽略的字段
noConf := strings.HasPrefix(fullKey, "-") || strings.HasSuffix(fullKey, "-") || strings.Contains(fullKey, ".-.")
if !noConf {
outPath[fullKey] = StructFieldPathInfo{
Expand Down Expand Up @@ -132,8 +153,8 @@ func (s *Struct) nested(val reflect.Value, outPath map[string]StructFieldPathInf
m := make(map[string]interface{})
n.fillMapStructure(m, outPath, prefix, fieldNames)

// do not add the converted value if there are no exported fields, ie:
// time.Time
//do not add the converted value if there are no exported fields, ie:
//time.Time
if len(m) == 0 {
finalVal = val.Interface()
} else {
Expand Down
42 changes: 42 additions & 0 deletions tests/all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,48 @@ func TestMapMerge(t *testing.T) {
})
}

type confTestNestedSquash struct {
Nested1 `xconf:",squash"`
Nested2 `xconf:",squash"`
}
type confTestNestedSquashOff struct {
Nested1 `xconf:"nested1"`
Nested2 `xconf:"nested2"`
}

func TestSquash(t *testing.T) {
Convey("TestSquash Enable", t, func(c C) {
cc := &confTestNestedSquash{}
cc.Nested1.Deadline = time.Now()
cc.TimeoutMap = map[string]time.Duration{"read": time.Second}
x := xconf.New(
xconf.WithFlagSet(flag.NewFlagSet("suqash_anable", flag.ContinueOnError)),
xconf.WithFlagArgs(),
xconf.WithDebug(true),
xconf.WithMapMerge(true),
)
So(x.Parse(cc), ShouldBeNil)
So(strings.Contains(string(x.MustSaveToBytes(xconf.ConfigTypeYAML)), "nested"), ShouldBeFalse)
So(strings.Contains(string(x.MustSaveToBytes(xconf.ConfigTypeJSON)), "nested"), ShouldBeFalse)
So(strings.Contains(string(x.MustSaveToBytes(xconf.ConfigTypeTOML)), "nested"), ShouldBeFalse)
})
Convey("TestSquash Disable", t, func(c C) {
cc := &confTestNestedSquashOff{}
cc.Nested1.Deadline = time.Now()
cc.Nested2.TimeoutMap = map[string]time.Duration{"read": time.Second}
x := xconf.New(
xconf.WithFlagSet(flag.NewFlagSet("suqash_disable", flag.ContinueOnError)),
xconf.WithFlagArgs(),
xconf.WithDebug(true),
xconf.WithMapMerge(true),
)
So(x.Parse(cc), ShouldBeNil)
So(strings.Contains(string(x.MustSaveToBytes(xconf.ConfigTypeYAML)), "nested"), ShouldBeTrue)
So(strings.Contains(string(x.MustSaveToBytes(xconf.ConfigTypeJSON)), "nested"), ShouldBeTrue)
So(strings.Contains(string(x.MustSaveToBytes(xconf.ConfigTypeTOML)), "nested"), ShouldBeTrue)
})
}

type TestConf1 struct {
HTTPAddress string `xconf:"http_address" default:"0.0.0.0:0000"`
Hosts []string `flag:"hosts" cfg:"hosts" default:"127.0.0.0,127.0.0.1"`
Expand Down
14 changes: 14 additions & 0 deletions tests/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ type RedisTimeout = redis.Timeout

var optionUsage = `在这里描述一些应用级别的配置规则`

type Nested1 struct {
Deadline time.Time `xconf:"deadline"`
DeadlineAsSecond int
}
type Nested2 struct {
TimeoutMap map[string]time.Duration `xconf:"timeout_map"`
}

var defaultNested1 = Nested1{Deadline: time.Now()}

// ConfigOptionDeclareWithDefault go-lint
//go:generate optiongen --option_with_struct_name=false --new_func=NewTestConfig --xconf=true --empty_composite_nil=true --usage_tag_name=usage
func ConfigOptionDeclareWithDefault() interface{} {
Expand Down Expand Up @@ -61,5 +71,9 @@ func ConfigOptionDeclareWithDefault() interface{} {
"RedisAsPointer": (*Redis)(&redis.Conf{}),
"Redis": (Redis)(redis.Conf{}),
"RedisTimeout": (*RedisTimeout)(&redis.Timeout{}),
// annotation@Nested1(xconf=",squash",inline="true")
"Nested1": (Nested1)(defaultNested1),
// annotation@Nested2(inline="true")
"Nested2": (*Nested2)(nil),
}
}
28 changes: 28 additions & 0 deletions tests/gen_config_optiongen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tests/main/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func main() {
cc.Redis.RedisAddress = "127.0.0.1:6637"
xx := xconf.New(
xconf.WithFiles("c1.yaml"),
xconf.WithDebug(false),
xconf.WithDebug(true),
xconf.WithEnvironPrefix("test_prefix_"),
)
if err := xx.Parse(cc); err != nil {
Expand Down
12 changes: 9 additions & 3 deletions xconf.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,12 @@ func (x *XConf) defaultXFlagOptions() []xflag.Option {

// ZeroStructKeysTagList 获取参数s的空结构的Filed信息
func (x *XConf) ZeroStructKeysTagList(s interface{}) map[string]StructFieldPathInfo {
_, v := NewStruct(reflect.New(reflect.ValueOf(s).Type().Elem()).Interface(), x.cc.TagName, x.cc.TagNameForDefaultValue, x.cc.FieldTagConvertor).Map()
_, v := NewStruct(
reflect.New(reflect.ValueOf(s).Type().Elem()).Interface(),
x.cc.TagName,
x.cc.TagNameForDefaultValue,
x.cc.FieldTagConvertor,
).Map()
return v
}

Expand All @@ -144,7 +149,8 @@ func FieldMap(valPtr interface{}, x *XConf) map[string]StructFieldPathInfo {
reflect.New(reflect.ValueOf(valPtr).Type().Elem()).Interface(),
x.cc.TagName,
x.cc.TagNameForDefaultValue,
x.cc.FieldTagConvertor).Map()
x.cc.FieldTagConvertor,
).Map()
return fieldsMap
}

Expand Down Expand Up @@ -287,7 +293,6 @@ func (x *XConf) parseFlagFilesForXConf(valPtr interface{}, flagSet *flag.FlagSet
}
validKeys = append(validKeys, MetaKeyFlagFiles)
}

flagData, err = xflagMapstructure(
valPtr,
validKeys,
Expand Down Expand Up @@ -422,6 +427,7 @@ func (x *XConf) updateDstDataWithEnviron(environ ...string) (err error) {
func (x *XConf) decode(data map[string]interface{}, valPtr interface{}) error {
config := x.defaultDecoderConfig(valPtr)
config.TagName = x.cc.TagName
// config.Squash = x.cc.EnableSquash
// xconf默认使用的SnakeCase规则转换filedName
config.MatchName = func(mapKey, fieldName string) bool {
equal := strings.EqualFold(mapKey, fieldName)
Expand Down
36 changes: 27 additions & 9 deletions xflag/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func (fm *Maker) enumerateAndCreate(prefix string, tags xfield.TagList, value re
if containsString(fm.cc.FlagCreateIgnoreFiledPath, prefix) || strings.HasPrefix(prefix, "-") || strings.HasSuffix(prefix, "-") || strings.Contains(prefix, ".-.") {
return
}

switch value.Kind() {
case
// do no create flag for these types
Expand Down Expand Up @@ -183,30 +184,45 @@ func (fm *Maker) enumerateAndCreate(prefix string, tags xfield.TagList, value re
fm.enumerateAndCreate(prefix, tags, value.Elem(), usageFromTag)
return
case reflect.Struct:
switch v := value.Addr().Interface().(type) {
// 独立处理时间类型,time.Time没有暴露任何字段
case *time.Time:
fm.fs.Var(vars.NewTime(v, fm.cc.StringAlias), prefix, usageFromTag)
return
}
default:
fm.warningCanNotCreate(prefix, reflect.TypeOf(value.Interface()).Name())
return
}

numFields := value.NumField()
tt := value.Type()

for i := 0; i < numFields; i++ {
stField := tt.Field(i)
fieldCreated := 0
for i := 0; i < value.NumField(); i++ {
stField := value.Type().Field(i)
// Skip unexported fields, as only exported fields can be set. This is similar to how json and yaml work.
if stField.PkgPath != "" && !stField.Anonymous {
continue
}
if stField.Anonymous && fm.getUnderlyingType(stField.Type).Kind() != reflect.Struct {
continue
}
fieldCreated++
field := value.Field(i)
optName, tags := fm.getName(stField)
usage := stField.Tag.Get(fm.cc.UsageTagName)
if len(prefix) > 0 && !fm.cc.Flatten {
optName = prefix + "." + optName
// 子元素
fullKey := prefix
if fullKey != "" {
fullKey += "."
}
fm.enumerateAndCreate(optName, tags, field, usage)
squash := tags.Has("squash")
if squash {
fullKey = prefix
} else {
fullKey += optName
}
fm.enumerateAndCreate(fullKey, tags, field, usage)
}
if fieldCreated == 0 {
fm.warningCanNotCreate(prefix, reflect.TypeOf(value.Interface()).Name())
}
}
func (fm *Maker) getName(field reflect.StructField) (string, xfield.TagList) {
Expand Down Expand Up @@ -305,5 +321,7 @@ func (fm *Maker) defineFlag(name string, value reflect.Value, usageFromTag strin
case reflect.Uint64:
v := ptrValue.Convert(uint64PtrType).Interface().(*uint64)
fm.fs.Var(vars.NewUint64(v, fm.cc.StringAlias), name, usage)
default:
fm.warningCanNotCreate(name, reflect.TypeOf(value.Interface()).Name())
}
}
12 changes: 12 additions & 0 deletions xflag/vars/gen.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package vars

import (
"fmt"
"strconv"
"time"
)
Expand Down Expand Up @@ -63,6 +64,15 @@ func parseString(s string) (string, error) { return s, nil }
func parseTimeDuration(s string) (time.Duration, error) {
return time.ParseDuration(s)
}

var TimeLayout string

func parseTime(s string) (time.Time, error) {
if TimeLayout == "" {
return time.Now(), fmt.Errorf("timestamp Layout is required")
}
return time.Parse(s, TimeLayout)
}
func parseBool(s string) (bool, error) { return strconv.ParseBool(s) }

//go:generate gotemplate -outfmt gen_%v "../templates/xslice" "SliceStrig(string,parseString,SetProviderByFieldType,StringValueDelim)"
Expand Down Expand Up @@ -100,6 +110,8 @@ func parseBool(s string) (bool, error) { return strconv.ParseBool(s) }
//go:generate gotemplate -outfmt gen_%v "../templates/xvar" "Float32(float32,parseFloat32)"
//go:generate gotemplate -outfmt gen_%v "../templates/xvar" "Float64(float64,parseFloat64)"

//go:generate gotemplate -outfmt gen_%v "../templates/xvar" "Time(time.Time,parseTime)"

//go:generate gotemplate -outfmt gen_%v "../templates/xmap" "MapStringString(string,string,parseString,parseString,SetProviderByFieldType,StringValueDelim)"
//go:generate gotemplate -outfmt gen_%v "../templates/xmap" "MapStringInt(string,int,parseString,parseInt,SetProviderByFieldType,StringValueDelim)"
//go:generate gotemplate -outfmt gen_%v "../templates/xmap" "MapIntString(int,string,parseInt,parseString,SetProviderByFieldType,StringValueDelim)"
Expand Down
Loading

0 comments on commit e5e3fa6

Please sign in to comment.