mirror of
https://github.com/charlienet/go-mixed.git
synced 2025-07-17 16:12:42 +08:00
structs
This commit is contained in:
@ -1,7 +0,0 @@
|
||||
package structs
|
||||
|
||||
func Copy(source, target any, opts ...optionFunc) {
|
||||
opt := createOptions(opts)
|
||||
|
||||
_ = opt
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
29
structs/field.go
Normal file
29
structs/field.go
Normal file
@ -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)
|
||||
}
|
@ -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()))
|
@ -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()
|
||||
}
|
21
structs/struct_test.go
Normal file
21
structs/struct_test.go
Normal file
@ -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())
|
||||
}
|
@ -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
|
||||
}
|
139
structs/structs.go
Normal file
139
structs/structs.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
45
structs/tags_test.go
Normal file
45
structs/tags_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user