From d7a439458136769f5625562d0ba9833975bdfc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20D=C3=B6ll?= Date: Sun, 29 Dec 2024 20:21:18 +0000 Subject: [PATCH] feat: defaults package --- go.mod | 5 ++ go.sum | 4 + utilx/defaults.go | 174 +++++++++++++++++++++++++++++++++++++++++ utilx/defaults_test.go | 137 ++++++++++++++++++++++++++++++++ utilx/filler.go | 146 ++++++++++++++++++++++++++++++++++ utilx/filler_test.go | 101 ++++++++++++++++++++++++ 6 files changed, 567 insertions(+) create mode 100644 utilx/defaults.go create mode 100644 utilx/defaults_test.go create mode 100644 utilx/filler.go create mode 100644 utilx/filler_test.go diff --git a/go.mod b/go.mod index a9e7d4d..3907ec6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 toolchain go1.23.4 require ( + bou.ke/monkey v1.0.2 firebase.google.com/go/v4 v4.15.1 github.com/gofiber/fiber/v2 v2.52.5 github.com/google/uuid v1.6.0 @@ -16,6 +17,7 @@ require ( golang.org/x/mod v0.22.0 golang.org/x/sync v0.10.0 google.golang.org/api v0.214.0 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gorm.io/gorm v1.25.12 helm.sh/helm v2.17.0+incompatible k8s.io/apimachinery v0.32.0 @@ -56,6 +58,8 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -66,6 +70,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect diff --git a/go.sum b/go.sum index b2fa232..e2ea6e4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= +bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU= @@ -115,6 +117,7 @@ github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/openfga/go-sdk v0.6.3 h1:FO3uDYeV+1y844iVvD7MJYKtmIEP1r4mis7kWCaDG2A= github.com/openfga/go-sdk v0.6.3/go.mod h1:zui7pHE3eLAYh2fFmEMrWg9XbxYns2WW5Xr/GEgili4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -122,6 +125,7 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/utilx/defaults.go b/utilx/defaults.go new file mode 100644 index 0000000..645d710 --- /dev/null +++ b/utilx/defaults.go @@ -0,0 +1,174 @@ +package utilx + +import ( + "reflect" + "regexp" + "strconv" + "strings" + "time" +) + +// SetDefaults sets the default values for the fields of the given struct. +// +// Usage +// +// type ExampleBasic struct { +// Foo bool `default:"true"` +// Bar string `default:"33"` +// Qux int8 +// Dur time.Duration `default:"2m3s"` +// } +// +// foo := &ExampleBasic{} +// SetDefaults(foo) +func SetDefaults(variable interface{}) { + getDefaultFiller().Fill(variable) +} + +var defaultFiller *Filler = nil + +func getDefaultFiller() *Filler { + if defaultFiller == nil { + defaultFiller = newDefaultFiller() + } + + return defaultFiller +} + +// nolint: exhaustive +func newDefaultFiller() *Filler { + funcs := make(map[reflect.Kind]FillerFunc, 0) + funcs[reflect.Bool] = func(field *FieldData) { + value, _ := strconv.ParseBool(field.TagValue) + field.Value.SetBool(value) + } + + funcs[reflect.Int] = func(field *FieldData) { + value, _ := strconv.ParseInt(field.TagValue, 10, 64) + field.Value.SetInt(value) + } + + funcs[reflect.Int8] = funcs[reflect.Int] + funcs[reflect.Int16] = funcs[reflect.Int] + funcs[reflect.Int32] = funcs[reflect.Int] + funcs[reflect.Int64] = func(field *FieldData) { + if field.Field.Type == reflect.TypeOf(time.Second) { + value, _ := time.ParseDuration(field.TagValue) + field.Value.Set(reflect.ValueOf(value)) + } else { + value, _ := strconv.ParseInt(field.TagValue, 10, 64) + field.Value.SetInt(value) + } + } + + funcs[reflect.Float32] = func(field *FieldData) { + value, _ := strconv.ParseFloat(field.TagValue, 64) + field.Value.SetFloat(value) + } + + funcs[reflect.Float64] = funcs[reflect.Float32] + + funcs[reflect.Uint] = func(field *FieldData) { + value, _ := strconv.ParseUint(field.TagValue, 10, 64) + field.Value.SetUint(value) + } + + funcs[reflect.Uint8] = funcs[reflect.Uint] + funcs[reflect.Uint16] = funcs[reflect.Uint] + funcs[reflect.Uint32] = funcs[reflect.Uint] + funcs[reflect.Uint64] = funcs[reflect.Uint] + + funcs[reflect.String] = func(field *FieldData) { + tagValue := parseDateTimeString(field.TagValue) + field.Value.SetString(tagValue) + } + + funcs[reflect.Struct] = func(field *FieldData) { + fields := getDefaultFiller().GetFieldsFromValue(field.Value, nil) + getDefaultFiller().SetDefaultValues(fields) + } + + types := make(map[TypeHash]FillerFunc, 1) + types["time.Duration"] = func(field *FieldData) { + d, _ := time.ParseDuration(field.TagValue) + field.Value.Set(reflect.ValueOf(d)) + } + + funcs[reflect.Slice] = func(field *FieldData) { + k := field.Value.Type().Elem().Kind() + switch k { + case reflect.Uint8: + if field.Value.Bytes() != nil { + return + } + field.Value.SetBytes([]byte(field.TagValue)) + case reflect.Struct: + count := field.Value.Len() + for i := 0; i < count; i++ { + fields := getDefaultFiller().GetFieldsFromValue(field.Value.Index(i), nil) + getDefaultFiller().SetDefaultValues(fields) + } + default: + // 处理形如 [1,2,3,4] + reg := regexp.MustCompile(`^\[(.*)\]$`) + matchs := reg.FindStringSubmatch(field.TagValue) + if len(matchs) != 2 { + return + } + if matchs[1] == "" { + field.Value.Set(reflect.MakeSlice(field.Value.Type(), 0, 0)) + } else { + defaultValue := strings.Split(matchs[1], ",") + result := reflect.MakeSlice(field.Value.Type(), len(defaultValue), len(defaultValue)) + for i := 0; i < len(defaultValue); i++ { + itemValue := result.Index(i) + item := &FieldData{ + Value: itemValue, + Field: reflect.StructField{}, + TagValue: defaultValue[i], + Parent: nil, + } + funcs[k](item) + } + field.Value.Set(result) + } + } + } + + return &Filler{FuncByKind: funcs, FuncByType: types, Tag: "default"} +} + +func parseDateTimeString(data string) string { + pattern := regexp.MustCompile(`\{\{(\w+\:(?:-|)\d*,(?:-|)\d*,(?:-|)\d*)\}\}`) + matches := pattern.FindAllStringSubmatch(data, -1) // matches is [][]string + for _, match := range matches { + + tags := strings.Split(match[1], ":") + if len(tags) == 2 { + + valueStrings := strings.Split(tags[1], ",") + if len(valueStrings) == 3 { + var values [3]int + for key, valueString := range valueStrings { + num, _ := strconv.ParseInt(valueString, 10, 64) + values[key] = int(num) + } + + switch tags[0] { + + case "date": + str := time.Now().AddDate(values[0], values[1], values[2]).Format("2006-01-02") + data = strings.ReplaceAll(data, match[0], str) + case "time": + str := time.Now().Add((time.Duration(values[0]) * time.Hour) + + (time.Duration(values[1]) * time.Minute) + + (time.Duration(values[2]) * time.Second)).Format("15:04:05") + data = strings.ReplaceAll(data, match[0], str) + } + } + } + + } + + return data +} diff --git a/utilx/defaults_test.go b/utilx/defaults_test.go new file mode 100644 index 0000000..259fc28 --- /dev/null +++ b/utilx/defaults_test.go @@ -0,0 +1,137 @@ +package utilx + +import ( + "testing" + "time" + + "bou.ke/monkey" + . "gopkg.in/check.v1" +) + +// Hook up gocheck into the "go test" runner. + +func Test(t *testing.T) { + monkey.Patch(time.Now, func() time.Time { + t, _ := time.Parse("2006-01-02 15:04:05", "2020-06-10 12:00:00") + return t + }) + + TestingT(t) +} + +type DefaultsSuite struct{} + +var _ = Suite(&DefaultsSuite{}) + +type Parent struct { + Children []Child +} + +type Child struct { + Name string + Age int `default:"10"` +} + +type ExampleBasic struct { + Bool bool `default:"true"` + Integer int `default:"33"` + Integer8 int8 `default:"8"` + Integer16 int16 `default:"16"` + Integer32 int32 `default:"32"` + Integer64 int64 `default:"64"` + UInteger uint `default:"11"` + UInteger8 uint8 `default:"18"` + UInteger16 uint16 `default:"116"` + UInteger32 uint32 `default:"132"` + UInteger64 uint64 `default:"164"` + String string `default:"foo"` + Bytes []byte `default:"bar"` + Float32 float32 `default:"3.2"` + Float64 float64 `default:"6.4"` + Struct struct { + Bool bool `default:"true"` + Integer int `default:"33"` + } + Duration time.Duration `default:"1s"` + Children []Child + Second time.Duration `default:"1s"` + StringSlice []string `default:"[1,2,3,4]"` + IntSlice []int `default:"[1,2,3,4]"` + IntSliceSlice [][]int `default:"[[1],[2],[3],[4]]"` + StringSliceSlice [][]string `default:"[[1],[]]"` + + DateTime string `default:"{{date:1,-10,0}} {{time:1,-5,10}}"` +} + +func (s *DefaultsSuite) TestSetDefaultsBasic(c *C) { + foo := &ExampleBasic{} + SetDefaults(foo) + + s.assertTypes(c, foo) +} + +type ExampleNested struct { + Struct ExampleBasic +} + +func (s *DefaultsSuite) TestSetDefaultsNested(c *C) { + foo := &ExampleNested{} + SetDefaults(foo) + + s.assertTypes(c, &foo.Struct) +} + +func (s *DefaultsSuite) assertTypes(c *C, foo *ExampleBasic) { + c.Assert(foo.Bool, Equals, true) + c.Assert(foo.Integer, Equals, 33) + c.Assert(foo.Integer8, Equals, int8(8)) + c.Assert(foo.Integer16, Equals, int16(16)) + c.Assert(foo.Integer32, Equals, int32(32)) + c.Assert(foo.Integer64, Equals, int64(64)) + c.Assert(foo.UInteger, Equals, uint(11)) + c.Assert(foo.UInteger8, Equals, uint8(18)) + c.Assert(foo.UInteger16, Equals, uint16(116)) + c.Assert(foo.UInteger32, Equals, uint32(132)) + c.Assert(foo.UInteger64, Equals, uint64(164)) + c.Assert(foo.String, Equals, "foo") + c.Assert(string(foo.Bytes), Equals, "bar") + c.Assert(foo.Float32, Equals, float32(3.2)) + c.Assert(foo.Float64, Equals, 6.4) + c.Assert(foo.Struct.Bool, Equals, true) + c.Assert(foo.Duration, Equals, time.Second) + c.Assert(foo.Children, IsNil) + c.Assert(foo.Second, Equals, time.Second) + c.Assert(foo.StringSlice, DeepEquals, []string{"1", "2", "3", "4"}) + c.Assert(foo.IntSlice, DeepEquals, []int{1, 2, 3, 4}) + c.Assert(foo.IntSliceSlice, DeepEquals, [][]int{{1}, {2}, {3}, {4}}) + c.Assert(foo.StringSliceSlice, DeepEquals, [][]string{{"1"}, {}}) + c.Assert(foo.DateTime, Equals, "2020-08-10 12:55:10") +} + +func (s *DefaultsSuite) TestSetDefaultsWithValues(c *C) { + foo := &ExampleBasic{ + Integer: 55, + UInteger: 22, + Float32: 9.9, + String: "bar", + Bytes: []byte("foo"), + Children: []Child{{Name: "alice"}, {Name: "bob", Age: 2}}, + } + + SetDefaults(foo) + + c.Assert(foo.Integer, Equals, 55) + c.Assert(foo.UInteger, Equals, uint(22)) + c.Assert(foo.Float32, Equals, float32(9.9)) + c.Assert(foo.String, Equals, "bar") + c.Assert(string(foo.Bytes), Equals, "foo") + c.Assert(foo.Children[0].Age, Equals, 10) + c.Assert(foo.Children[1].Age, Equals, 2) +} + +func (s *DefaultsSuite) BenchmarkLogic(c *C) { + for i := 0; i < c.N; i++ { + foo := &ExampleBasic{} + SetDefaults(foo) + } +} diff --git a/utilx/filler.go b/utilx/filler.go new file mode 100644 index 0000000..8be2459 --- /dev/null +++ b/utilx/filler.go @@ -0,0 +1,146 @@ +package utilx + +import ( + "fmt" + "reflect" +) + +type FieldData struct { + Field reflect.StructField + Value reflect.Value + TagValue string + Parent *FieldData +} + +type FillerFunc func(field *FieldData) + +// Filler contains all the functions to fill any struct field with any type +// allowing to define function by Kind, Type of field name +type Filler struct { + FuncByName map[string]FillerFunc + FuncByType map[TypeHash]FillerFunc + FuncByKind map[reflect.Kind]FillerFunc + Tag string +} + +// Fill apply all the functions contained on Filler, setting all the possible +// values +func (f *Filler) Fill(variable interface{}) { + fields := f.getFields(variable) + f.SetDefaultValues(fields) +} + +func (f *Filler) getFields(variable interface{}) []*FieldData { + valueObject := reflect.ValueOf(variable).Elem() + + return f.GetFieldsFromValue(valueObject, nil) +} + +func (f *Filler) GetFieldsFromValue(valueObject reflect.Value, parent *FieldData) []*FieldData { + typeObject := valueObject.Type() + + count := valueObject.NumField() + var results []*FieldData + for i := 0; i < count; i++ { + value := valueObject.Field(i) + field := typeObject.Field(i) + + if value.CanSet() { + results = append(results, &FieldData{ + Value: value, + Field: field, + TagValue: field.Tag.Get(f.Tag), + Parent: parent, + }) + } + } + + return results +} + +func (f *Filler) SetDefaultValues(fields []*FieldData) { + for _, field := range fields { + if f.isEmpty(field) { + f.SetDefaultValue(field) + } + } +} + +// nolint: exhaustive +func (f *Filler) isEmpty(field *FieldData) bool { + switch field.Value.Kind() { + case reflect.Bool: + return !field.Value.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return field.Value.Int() == 0 + case reflect.Float32, reflect.Float64: + return field.Value.Float() == .0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return field.Value.Uint() == 0 + case reflect.Slice: + switch field.Value.Type().Elem().Kind() { + case reflect.Struct: + // always assume the structs in the slice is empty and can be filled + // the actually struct filling logic should take care of the rest + return true + default: + return field.Value.Len() == 0 + } + case reflect.String: + return field.Value.String() == "" + } + return true +} + +func (f *Filler) SetDefaultValue(field *FieldData) { + getters := []func(field *FieldData) FillerFunc{ + f.getFunctionByName, + f.getFunctionByType, + f.getFunctionByKind, + } + + for _, getter := range getters { + filler := getter(field) + if filler != nil { + filler(field) + return + } + } +} + +func (f *Filler) getFunctionByName(field *FieldData) FillerFunc { + if f, ok := f.FuncByName[field.Field.Name]; ok { + return f + } + + return nil +} + +func (f *Filler) getFunctionByType(field *FieldData) FillerFunc { + if f, ok := f.FuncByType[GetTypeHash(field.Field.Type)]; ok { + return f + } + + return nil +} + +func (f *Filler) getFunctionByKind(field *FieldData) FillerFunc { + if f, ok := f.FuncByKind[field.Field.Type.Kind()]; ok { + return f + } + + return nil +} + +// TypeHash is a string representing a reflect.Type following the next pattern: +// . +type TypeHash string + +// GetTypeHash returns the TypeHash for a given reflect.Type +func GetTypeHash(t reflect.Type) TypeHash { + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + return TypeHash(fmt.Sprintf("%s.%s", t.PkgPath(), t.Name())) +} diff --git a/utilx/filler_test.go b/utilx/filler_test.go new file mode 100644 index 0000000..4564e32 --- /dev/null +++ b/utilx/filler_test.go @@ -0,0 +1,101 @@ +package utilx + +import ( + "fmt" + "reflect" + + . "gopkg.in/check.v1" +) + +type FillerSuite struct{} + +var _ = Suite(&FillerSuite{}) + +type FixtureTypeInt int + +func (s *FillerSuite) TestFuncByNameIsEmpty(c *C) { + calledA := false + calledB := false + + f := &Filler{ + FuncByName: map[string]FillerFunc{ + "Foo": func(field *FieldData) { + calledA = true + }, + }, + FuncByKind: map[reflect.Kind]FillerFunc{ + reflect.Int: func(field *FieldData) { + calledB = true + }, + }, + } + + f.Fill(&struct{ Foo int }{}) + c.Assert(calledA, Equals, true) + c.Assert(calledB, Equals, false) +} + +func (s *FillerSuite) TestFuncByTypeIsEmpty(c *C) { + calledA := false + calledB := false + + t := GetTypeHash(reflect.TypeOf(new(FixtureTypeInt))) + f := &Filler{ + FuncByType: map[TypeHash]FillerFunc{ + t: func(field *FieldData) { + calledA = true + }, + }, + FuncByKind: map[reflect.Kind]FillerFunc{ + reflect.Int: func(field *FieldData) { + calledB = true + }, + }, + } + + f.Fill(&struct{ Foo FixtureTypeInt }{}) + c.Assert(calledA, Equals, true) + c.Assert(calledB, Equals, false) +} + +func (s *FillerSuite) TestFuncByKindIsNotEmpty(c *C) { + called := false + f := &Filler{FuncByKind: map[reflect.Kind]FillerFunc{ + reflect.Int: func(field *FieldData) { + called = true + }, + }} + + f.Fill(&struct{ Foo int }{Foo: 42}) + c.Assert(called, Equals, false) +} + +func (s *FillerSuite) TestFuncByKindSlice(c *C) { + fmt.Println(GetTypeHash(reflect.TypeOf(new([]string)))) +} + +func (s *FillerSuite) TestFuncByKindTag(c *C) { + var called string + f := &Filler{Tag: "foo", FuncByKind: map[reflect.Kind]FillerFunc{ + reflect.Int: func(field *FieldData) { + called = field.TagValue + }, + }} + + f.Fill(&struct { + Foo int `foo:"qux"` + }{}) + c.Assert(called, Equals, "qux") +} + +func (s *FillerSuite) TestFuncByKindIsEmpty(c *C) { + called := false + f := &Filler{FuncByKind: map[reflect.Kind]FillerFunc{ + reflect.Int: func(field *FieldData) { + called = true + }, + }} + + f.Fill(&struct{ Foo int }{}) + c.Assert(called, Equals, true) +}