From 78c957c98e448df047a2b396fbab9ba2e7b1fb73 Mon Sep 17 00:00:00 2001 From: charlie <3140647@qq.com> Date: Sun, 24 Apr 2022 17:06:40 +0800 Subject: [PATCH] structs --- structs/copy.go | 7 - structs/copy_test.go | 2 +- structs/field.go | 29 ++++ .../{struct_to_map_test.go => map_test.go} | 4 +- structs/{utils.go => options.go} | 51 ++++--- structs/struct_test.go | 21 +++ structs/struct_to_map.go | 35 ----- structs/structs.go | 139 ++++++++++++++++++ structs/tags.go | 20 +-- structs/tags_test.go | 45 ++++++ 10 files changed, 270 insertions(+), 83 deletions(-) delete mode 100644 structs/copy.go create mode 100644 structs/field.go rename structs/{struct_to_map_test.go => map_test.go} (82%) rename structs/{utils.go => options.go} (50%) create mode 100644 structs/struct_test.go delete mode 100644 structs/struct_to_map.go create mode 100644 structs/structs.go create mode 100644 structs/tags_test.go diff --git a/structs/copy.go b/structs/copy.go deleted file mode 100644 index ad83bdd..0000000 --- a/structs/copy.go +++ /dev/null @@ -1,7 +0,0 @@ -package structs - -func Copy(source, target any, opts ...optionFunc) { - opt := createOptions(opts) - - _ = opt -} diff --git a/structs/copy_test.go b/structs/copy_test.go index dcb077f..8c37f57 100644 --- a/structs/copy_test.go +++ b/structs/copy_test.go @@ -6,7 +6,7 @@ func TestCopy(t *testing.T) { v1 := struct{ Abc string }{Abc: "abc"} v2 := struct{ Abc string }{} - Copy(v1, v2, IgnoreEmpty()) + Copy(v1, &v2, IgnoreEmpty()) t.Log(v2) } diff --git a/structs/field.go b/structs/field.go new file mode 100644 index 0000000..d39fdcc --- /dev/null +++ b/structs/field.go @@ -0,0 +1,29 @@ +package structs + +import ( + "reflect" + + "github.com/charlienet/go-mixed/expr" +) + +type field struct { + name string + tagName string + ignoreEmpty bool + ignore bool +} + +func parseField(fi reflect.StructField, opt option) field { + name, opts := parseTag(fi.Tag.Get(opt.TagName)) + + return field{ + name: fi.Name, + tagName: expr.If(isValidTag(name), name, expr.If(opt.NameConverter != nil, opt.NameConverter(fi.Name), fi.Name)), + ignoreEmpty: opt.IgnoreEmpty || (opts.Contains("omitempty") && opt.Omitempty), + ignore: name == "-" && opt.Ignore, + } +} + +func (f field) shouldIgnore(s reflect.Value) bool { + return f.ignore || (s.IsZero() && f.ignoreEmpty) +} diff --git a/structs/struct_to_map_test.go b/structs/map_test.go similarity index 82% rename from structs/struct_to_map_test.go rename to structs/map_test.go index 5da1038..0ad61e4 100644 --- a/structs/struct_to_map_test.go +++ b/structs/map_test.go @@ -10,16 +10,18 @@ func TestStructToMap(t *testing.T) { o := struct { UserName string InTagName string `json:"in_tag_name,omitempty"` + Ignore string `json:"-"` KeepEmpty int OmitEmpty int `json:",omitempty"` }{ UserName: "测试字段", InTagName: "具体名称", + Ignore: "这个字段跳过", KeepEmpty: 0, OmitEmpty: 0, } - t.Log(structs.ToMap(o)) + t.Log(structs.ToMap(o, structs.TagName("struct"))) t.Log(structs.ToMap(o, structs.IgnoreEmpty())) t.Log(structs.ToMap(o, structs.Omitempty())) t.Log(structs.ToMap(o, structs.Lcfirst())) diff --git a/structs/utils.go b/structs/options.go similarity index 50% rename from structs/utils.go rename to structs/options.go index d41e87c..fa58354 100644 --- a/structs/utils.go +++ b/structs/options.go @@ -1,18 +1,24 @@ package structs import ( - "reflect" - "github.com/charlienet/go-mixed/json" ) type optionFunc func(*option) type option struct { - NameFunc func(string) string - IgnoreEmpty bool - DeepCopy bool - Omitempty bool + TagName string + DeepCopy bool + Omitempty bool + IgnoreEmpty bool + Ignore bool + NameConverter func(string) string +} + +func TagName(name string) optionFunc { + return func(o *option) { + o.TagName = name + } } func IgnoreEmpty() optionFunc { @@ -21,42 +27,43 @@ func IgnoreEmpty() optionFunc { } } -func DeepCopy() optionFunc { - return func(o *option) { - o.DeepCopy = true - } -} - func Omitempty() optionFunc { return func(o *option) { o.Omitempty = true } } +func DeepCopy() optionFunc { + return func(o *option) { + o.DeepCopy = true + } +} + func Lcfirst() optionFunc { return func(o *option) { - o.NameFunc = json.Lcfirst + o.NameConverter = json.Lcfirst } } func Camel2Case() optionFunc { return func(o *option) { - o.NameFunc = json.Camel2Case + o.NameConverter = json.Camel2Case } } -func createOptions(opts []optionFunc) *option { - o := &option{ - NameFunc: func(s string) string { return s }, +func defaultOptions() option { + return option{ + TagName: defaultTagName, + Ignore: true, + NameConverter: func(s string) string { return s }, } +} +func acquireOptions(opts []optionFunc) option { + o := defaultOptions() for _, f := range opts { - f(o) + f(&o) } return o } - -func shouldIgnore(v reflect.Value, ignoreEmpty bool) bool { - return ignoreEmpty && v.IsZero() -} diff --git a/structs/struct_test.go b/structs/struct_test.go new file mode 100644 index 0000000..eccc435 --- /dev/null +++ b/structs/struct_test.go @@ -0,0 +1,21 @@ +package structs_test + +import ( + "reflect" + "testing" + + "github.com/charlienet/go-mixed/structs" + "github.com/go-playground/assert/v2" +) + +func TestNew(t *testing.T) { + o := struct { + Field1Name string + }{Field1Name: "field 1 name"} + + s := structs.New(o) + assert.Equal(t, reflect.Struct, s.Kind()) + + t.Log(s.Names()) + t.Log(s.Values()) +} diff --git a/structs/struct_to_map.go b/structs/struct_to_map.go deleted file mode 100644 index 4558e40..0000000 --- a/structs/struct_to_map.go +++ /dev/null @@ -1,35 +0,0 @@ -package structs - -import ( - "reflect" -) - -const tagName = "json" - -func ToMap(o any, opts ...optionFunc) map[string]any { - typ := reflect.TypeOf(o) - - kind := typ.Kind() - if kind == reflect.Map { - if h, ok := o.(map[string]any); ok { - return h - } - } - - opt := createOptions(opts) - val := reflect.ValueOf(o) - m := make(map[string]any) - for i := 0; i < val.NumField(); i++ { - fi := typ.Field(i) - - field := getFieldOption(fi) - source := val.FieldByName(fi.Name) - if shouldIgnore(source, opt.IgnoreEmpty || field.omitEmpty && opt.Omitempty) { - continue - } - - m[opt.NameFunc(field.name)] = source.Interface() - } - - return m -} diff --git a/structs/structs.go b/structs/structs.go new file mode 100644 index 0000000..b8e7bef --- /dev/null +++ b/structs/structs.go @@ -0,0 +1,139 @@ +package structs + +import ( + "errors" + "reflect" +) + +const defaultTagName = "json" + +var ( + ErrInvalidCopyDestination = errors.New("copy destination is invalid") +) + +type Struct struct { + opt option + raw any + value reflect.Value + fields []field +} + +func New(o any, opts ...optionFunc) *Struct { + opt := acquireOptions(opts) + v := indirect(reflect.ValueOf(o)) + + return &Struct{ + opt: opt, + raw: o, + value: v, + fields: parseFields(v, opt), + } +} + +func (s *Struct) Kind() reflect.Kind { + return s.value.Kind() +} + +func (s *Struct) Names() []string { + names := make([]string, len(s.fields)) + for i, f := range s.fields { + names[i] = f.name + } + + return names +} + +func (s *Struct) Values() []any { + values := make([]any, 0, len(s.fields)) + for _, fi := range s.fields { + v := s.value.FieldByName(fi.name) + values = append(values, v.Interface()) + } + + return values +} + +func (s *Struct) ToMap() map[string]any { + m := make(map[string]any, len(s.fields)) + for _, fi := range s.fields { + source := s.value.FieldByName(fi.name) + if fi.shouldIgnore(source) { + continue + } + + m[fi.tagName] = source.Interface() + } + + return m +} + +func (s *Struct) Copy(dest any) error { + to := indirect(reflect.ValueOf(dest)) + + if !to.CanAddr() { + return ErrInvalidCopyDestination + } + + t := indirectType(reflect.TypeOf(dest)) + for i := 0; i < t.NumField(); i++ { + destField := t.Field(i) + if fi, ok := s.getByName(destField.Name); ok { + source := s.value.FieldByName(fi.name) + if fi.shouldIgnore(source) { + continue + } + + tv := to.FieldByName(destField.Name) + tv.Set(source) + } + } + + return nil +} + +func (s *Struct) getByName(name string) (field, bool) { + for i := range s.fields { + f := s.fields[i] + if f.name == name { + return f, true + } + } + + return field{}, false +} + +func ToMap(o any, opts ...optionFunc) map[string]any { + return New(o, opts...).ToMap() +} + +func Copy(source, dst any, opts ...optionFunc) { + New(source, opts...).Copy(dst) +} + +func parseFields(t reflect.Value, opt option) []field { + typ := indirectType(t.Type()) + num := typ.NumField() + fields := make([]field, 0, num) + for i := 0; i < num; i++ { + fi := typ.Field(i) + fields = append(fields, parseField(fi, opt)) + } + + return fields +} + +func indirect(v reflect.Value) reflect.Value { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + return v +} + +func indirectType(t reflect.Type) reflect.Type { + if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice { + return t.Elem() + } + + return t +} diff --git a/structs/tags.go b/structs/tags.go index be70b7d..9249cf1 100644 --- a/structs/tags.go +++ b/structs/tags.go @@ -1,11 +1,8 @@ package structs import ( - "reflect" "strings" "unicode" - - "github.com/charlienet/go-mixed/expr" ) type tagOptions string @@ -19,6 +16,7 @@ func (o tagOptions) Contains(optionName string) bool { if len(o) == 0 { return false } + s := string(o) for s != "" { var name string @@ -27,23 +25,10 @@ func (o tagOptions) Contains(optionName string) bool { return true } } + return false } -type fieldOption struct { - name string - omitEmpty bool -} - -func getFieldOption(fi reflect.StructField) fieldOption { - name, opts := parseTag(fi.Tag.Get(tagName)) - - return fieldOption{ - name: expr.If(isValidTag(name), name, fi.Name), - omitEmpty: opts.Contains("omitempty"), - } -} - func isValidTag(s string) bool { if s == "" { return false @@ -56,5 +41,6 @@ func isValidTag(s string) bool { return false } } + return true } diff --git a/structs/tags_test.go b/structs/tags_test.go new file mode 100644 index 0000000..ef03c7d --- /dev/null +++ b/structs/tags_test.go @@ -0,0 +1,45 @@ +package structs + +import "testing" + +func TestParseTag_Name(t *testing.T) { + tags := []struct { + tag string + has bool + }{ + {"", false}, + {"name", true}, + {"name,opt", true}, + {"name , opt, opt2", false}, // has a single whitespace + {", opt, opt2", false}, + } + + for _, tag := range tags { + name, _ := parseTag(tag.tag) + + if (name != "name") && tag.has { + t.Errorf("Parse tag should return name: %#v", tag) + } + } +} + +func TestParseTag_Opts(t *testing.T) { + tags := []struct { + opts string + has bool + }{ + {"name", false}, + {"name,opt", true}, + {"name , opt, opt2", false}, // has a single whitespace + {",opt, opt2", true}, + {", opt3, opt4", false}, + } + + for _, tag := range tags { + _, opts := parseTag(tag.opts) + + if opts.Contains("opt") != tag.has { + t.Errorf("Tag opts should have opt: %#v", tag) + } + } +}