From 83920bfb2b6ddf1cf64d721fe47feb97d0bc3fb3 Mon Sep 17 00:00:00 2001 From: NoahLan <6995syu@163.com> Date: Fri, 18 Aug 2023 10:18:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=8C=85nmapper,=20=E7=94=A8=E4=BA=8E=E5=B0=86struct=E4=B8=8Em?= =?UTF-8?q?ap=E4=BA=92=E8=BD=AC=EF=BC=8C=E7=9B=AE=E5=89=8D=E9=87=8D?= =?UTF-8?q?=E7=82=B9=E6=98=AFstrMap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nmapper/const.go | 6 + nmapper/mapper.go | 62 ++++++++ nmapper/mapper_object.go | 265 +++++++++++++++++++++++++++++++++ nmapper/mapper_object_test.go | 89 +++++++++++ nmapper/wrapper/str_common.go | 41 +++++ nmapper/wrapper/str_time.go | 55 +++++++ nmapper/wrapper/str_wrapper.go | 8 + nmapper/wrapper/wrapper.go | 32 ++++ 8 files changed, 558 insertions(+) create mode 100644 nmapper/const.go create mode 100644 nmapper/mapper.go create mode 100644 nmapper/mapper_object.go create mode 100644 nmapper/mapper_object_test.go create mode 100644 nmapper/wrapper/str_common.go create mode 100644 nmapper/wrapper/str_time.go create mode 100644 nmapper/wrapper/str_wrapper.go create mode 100644 nmapper/wrapper/wrapper.go diff --git a/nmapper/const.go b/nmapper/const.go new file mode 100644 index 0000000..2c86d4b --- /dev/null +++ b/nmapper/const.go @@ -0,0 +1,6 @@ +package nmapper + +const ( + mapperTagKey = "mapper" + jsonTagKey = "json" +) diff --git a/nmapper/mapper.go b/nmapper/mapper.go new file mode 100644 index 0000000..e424af7 --- /dev/null +++ b/nmapper/mapper.go @@ -0,0 +1,62 @@ +package nmapper + +import "git.noahlan.cn/noahlan/ntool/nmapper/wrapper" + +var defaultMapper *MapperObject + +type Option func(m *MapperObject) + +func init() { + defaultMapper = NewMapper() +} + +// SetCustomTag 为默认的Mapper设置自定义tag +func SetCustomTag(tag string) { + defaultMapper.customTagName = tag +} + +// SetTypeCheck 为默认的Mapper设置开启类型检查 +func SetTypeCheck(enabled bool) { + defaultMapper.enabledTypeChecking = enabled +} + +// SetTypeWrapper 为默认的Mapper设置类型转换器 +func SetTypeWrapper(wrappers ...wrapper.TypeWrapper) { + defaultMapper.SetTypeWrapper(wrappers...) +} + +// WithCustomTag 设置自定义 tag +func WithCustomTag(tag string) Option { + return func(m *MapperObject) { + m.customTagName = tag + } +} + +// WithTypeCheck 开启类型安全 +func WithTypeCheck() Option { + return func(m *MapperObject) { + m.enabledTypeChecking = true + } +} + +// WithTypeWrapper 设置类型转换器 +func WithTypeWrapper(wrappers ...wrapper.TypeWrapper) Option { + return func(m *MapperObject) { + m.SetTypeWrapper(wrappers...) + } +} + +func Mapper(dst any, src any) error { + return defaultMapper.Mapper(dst, src) +} + +func ParseMap(dst any, src map[string]any) error { + return defaultMapper.ParseMap(dst, src) +} + +func ParseStrMap(dst any, src map[string]string) error { + defaultMapper.SetTypeWrapper(wrapper.StrWrapper()...) + defer defaultMapper.SetTypeWrapper() + + return defaultMapper.ParseStrMap(dst, src) +} diff --git a/nmapper/mapper_object.go b/nmapper/mapper_object.go new file mode 100644 index 0000000..c97c294 --- /dev/null +++ b/nmapper/mapper_object.go @@ -0,0 +1,265 @@ +package nmapper + +import ( + "errors" + "git.noahlan.cn/noahlan/ntool/internal/convert" + "git.noahlan.cn/noahlan/ntool/nmap" + "git.noahlan.cn/noahlan/ntool/nmapper/wrapper" + "git.noahlan.cn/noahlan/ntool/nmath" + "git.noahlan.cn/noahlan/ntool/nreflect" + "git.noahlan.cn/noahlan/ntool/nstr" + "git.noahlan.cn/noahlan/ntool/nstruct" + "reflect" + "time" +) + +type MapperObject struct { + ZeroValue reflect.Value + + typeWrappers []wrapper.TypeWrapper + timeType reflect.Type // 时间类型 + enabledTypeChecking bool // 是否开启类型检查 + + customTagName string // 自定义Tag名称 +} + +func NewMapper(opts ...Option) *MapperObject { + tmp := &MapperObject{ + ZeroValue: reflect.Value{}, + timeType: reflect.TypeOf(time.Time{}), + typeWrappers: make([]wrapper.TypeWrapper, 0), + enabledTypeChecking: false, + customTagName: "", + } + + for _, opt := range opts { + opt(tmp) + } + + return tmp +} + +func (m *MapperObject) SetTypeWrapper(wrappers ...wrapper.TypeWrapper) { + m.typeWrappers = wrappers +} + +func (m *MapperObject) RegisterTypeWrapper(wrappers ...wrapper.TypeWrapper) { + m.typeWrappers = append(m.typeWrappers, wrappers...) +} + +// MapperObject 将src转换为dst +// src为struct类型, dst为map类型 map[string]any 或者 map[string]string +func (m *MapperObject) Mapper(dst any, src any) error { + srcElem := reflect.ValueOf(src) + srcElem = nreflect.Indirect(srcElem) + + dstElem := reflect.ValueOf(dst) + dstElem = nreflect.Indirect(dstElem) + + if srcElem == m.ZeroValue { + return errors.New("src is not legal value") + } + if dstElem == m.ZeroValue { + return errors.New("dst is not legal value") + } + return m.elemMapper(dstElem, srcElem) +} + +func (m *MapperObject) elemMapper(dstElem, srcElem reflect.Value) error { + //if dstElem.Type().Kind() != reflect.Map { + // return errors.New(fmt.Sprintf("dst is not legal value, need map but got %s", dstElem.Type().Kind().String())) + //} + if dstElem.Type().Kind() == reflect.Map { + m.elemToMap(dstElem, srcElem) + } + + return nil +} + +func (m *MapperObject) elemToMap(dstElem, srcElem reflect.Value) { + for i := 0; i < srcElem.NumField(); i++ { + srcFieldInfo := srcElem.Field(i) + // tag + name, tagMap := m.getFieldNameAndMap(srcElem, i) + if name == "" { + continue + } + + valElem := srcFieldInfo + for _, w := range m.typeWrappers { + if w.Supported(srcFieldInfo) { + valElem = w.Wrap(srcFieldInfo, i, tagMap) + break + } + } + dstElem.SetMapIndex(reflect.ValueOf(name), valElem) + } +} + +func (m *MapperObject) ParseMap(dst any, src map[string]any) error { + dstElem := reflect.ValueOf(dst) + dstElem = nreflect.Indirect(dstElem) + + if dstElem == m.ZeroValue { + return errors.New("dst is not legal value") + } + + for i := 0; i < dstElem.NumField(); i++ { + dstFieldInfo := dstElem.Field(i) + name, tagMap := m.getFieldNameAndMap(dstElem, i) + if name == "" { + continue + } + m.setFieldValue(dstFieldInfo, tagMap, src[name]) + } + return nil +} + +func (m *MapperObject) ParseStrMap(dst any, src map[string]string) error { + dstElem := reflect.ValueOf(dst) + dstElem = nreflect.Indirect(dstElem) + + if dstElem == m.ZeroValue { + return errors.New("dst is not legal value") + } + + for i := 0; i < dstElem.NumField(); i++ { + dstFieldInfo := dstElem.Field(i) + name, tagMap := m.getFieldNameAndMap(dstElem, i) + if name == "" { + continue + } + m.setFieldValue(dstFieldInfo, tagMap, src[name]) + } + return nil +} + +func (m *MapperObject) setFieldValue(fieldElem reflect.Value, tagMap nmap.SMap, value any) { + fieldKind := fieldElem.Type().Kind() + + switch fieldKind { + case reflect.Bool: + if value == nil { + fieldElem.SetBool(false) + } else if v, ok := value.(bool); ok { + fieldElem.SetBool(v) + } else { + b, _ := convert.StrToBool(nstr.SafeString(value)) + fieldElem.SetBool(b) + } + case reflect.String: + if value == nil { + fieldElem.SetString("") + } else { + fieldElem.SetString(nstr.SafeString(value)) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if value == nil { + fieldElem.SetInt(0) + } else { + val := reflect.ValueOf(value) + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + fieldElem.SetInt(val.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + fieldElem.SetInt(int64(val.Uint())) + default: + v, _ := nmath.ToInt64(value) + fieldElem.SetInt(v) + } + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if value == nil { + fieldElem.SetUint(0) + } else { + val := reflect.ValueOf(value) + switch val.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + fieldElem.SetUint(uint64(val.Int())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + fieldElem.SetUint(val.Uint()) + default: + v, _ := nmath.ToUint(value) + fieldElem.SetUint(v) + } + } + case reflect.Float64, reflect.Float32: + if value == nil { + fieldElem.SetFloat(0) + } else { + val := reflect.ValueOf(value) + switch val.Kind() { + case reflect.Float64: + fieldElem.SetFloat(val.Float()) + default: + fieldElem.SetFloat(nmath.QuietFloat(value)) + } + } + case reflect.Struct: + if value == nil { + fieldElem.Set(reflect.Zero(fieldElem.Type())) + } else { + for _, w := range m.typeWrappers { + if w.Supported(fieldElem) { + w.Set(fieldElem, value, tagMap) + break + } + } + if reflect.ValueOf(value).Type() == fieldElem.Type() { + fieldElem.Set(reflect.ValueOf(value)) + } + } + default: + if !fieldElem.IsValid() { + fieldElem.Set(reflect.Zero(fieldElem.Type())) + return + } + // Type check + if reflect.ValueOf(value).Type() == fieldElem.Type() { + fieldElem.Set(reflect.ValueOf(value)) + } + } +} + +// getFieldNameAndMap 获取字段名称和tagMap +func (m *MapperObject) getFieldNameAndMap(objElem reflect.Value, index int) (string, nmap.SMap) { + field := objElem.Type().Field(index) + // 1. custom + tagVal, ok := field.Tag.Lookup(m.customTagName) + if ok && tagVal != "" { + sMap, _ := nstruct.ParseTagValueDefault(field.Name, tagVal) + name := sMap.Get("name") + if name != "" { + return name, sMap + } else { + // tag "-" + return "", nil + } + } + // 2. mapper + tagVal, ok = field.Tag.Lookup(mapperTagKey) + if ok && tagVal != "" { + sMap, _ := nstruct.ParseTagValueDefault(field.Name, tagVal) + name := sMap.Get("name") + if name != "" { + return name, sMap + } else { + // tag "-" + return "", nil + } + } + + // 3. json + tagVal, ok = field.Tag.Lookup(jsonTagKey) + if ok && tagVal != "" { + sMap, _ := nstruct.ParseTagValueDefault(field.Name, tagVal) + name := sMap.Get("name") + if name != "" { + return name, sMap + } else { + // tag "-" + return "", nil + } + } + return field.Name, nmap.SMap{"name": field.Name} +} diff --git a/nmapper/mapper_object_test.go b/nmapper/mapper_object_test.go new file mode 100644 index 0000000..026b15c --- /dev/null +++ b/nmapper/mapper_object_test.go @@ -0,0 +1,89 @@ +package nmapper_test + +import ( + "fmt" + "git.noahlan.cn/noahlan/ntool/nmapper" + "git.noahlan.cn/noahlan/ntool/nmapper/wrapper" + "testing" + "time" +) + +type testType struct { + Int int `json:"int"` + Int8 int8 `json:"int8"` + UInt8 uint8 `json:"uint8"` + Float32 float32 `json:"float32"` + Float64 float64 `json:"float64"` + String string `json:"string"` + Time time.Time `json:"time,layout=2006-01-02"` + TimeNoLayout time.Time `json:"time_no_layout"` + Ignore int `json:"-"` +} + +func TestMapper_Mapper(t *testing.T) { + mapper := nmapper.NewMapper() + + dest := make(map[string]any) + src := &testType{ + Int: 1, + Int8: 2, + UInt8: 3, + Float32: 4.2, + Float64: 5.2222222222, + String: "test", + Time: time.Now(), + TimeNoLayout: time.Now().Add(time.Hour * 24), + Ignore: 99, + } + _ = mapper.Mapper(dest, src) + + dest2 := make(map[string]string) + mapper.SetTypeWrapper(wrapper.StrWrapper()...) + _ = mapper.Mapper(dest2, src) + mapper.SetTypeWrapper() + + fmt.Printf("dest1:\n%+v\n", dest) + fmt.Printf("dest2:\n%+v\n", dest2) +} + +func TestMapper_Parse(t *testing.T) { + src := make(map[string]any) + src["int"] = 1 + src["int8"] = 2 + src["uint8"] = 3 + src["float32"] = 4.2 + src["float64"] = 5.2222222222 + src["string"] = "test" + src["time"] = time.Now() + src["time_no_layout"] = time.Now().Add(time.Hour * 24) + src["ignore"] = "ignore" + + var dest testType + + mapper := nmapper.NewMapper() + _ = mapper.ParseMap(&dest, src) + + fmt.Printf("%+v\n", dest) +} + +func TestMapperObject_ParseStrMap(t *testing.T) { + src := make(map[string]string) + src["int"] = "1" + src["int8"] = "2" + src["uint8"] = "3" + src["float32"] = "4.2" + src["float64"] = "5.2222222222" + src["string"] = "test" + src["time"] = "2023-08-18 10:07:40" + src["time_no_layout"] = "2023-08-19 10:07:40" + src["ignore"] = "ignore" + + var dest testType + + mapper := nmapper.NewMapper() + mapper.SetTypeWrapper(wrapper.StrWrapper()...) + + _ = mapper.ParseStrMap(&dest, src) + + fmt.Printf("%+v\n", dest) +} diff --git a/nmapper/wrapper/str_common.go b/nmapper/wrapper/str_common.go new file mode 100644 index 0000000..1822b84 --- /dev/null +++ b/nmapper/wrapper/str_common.go @@ -0,0 +1,41 @@ +package wrapper + +import ( + "git.noahlan.cn/noahlan/ntool/nmap" + "git.noahlan.cn/noahlan/ntool/nreflect" + "reflect" + "time" +) + +// StrCommonWrapper 将普通字段转换为string +// 支持如下类型 +// reflect.Invalid -> "" +// reflect.Bool -> "true", "false" +// reflect.String -> "str" +// reflect.Float32,reflect.Float64 -> "x.x" +// reflect.IntX -> "1" +// reflect.UintX -> "1" +// 非以上类型的,使用fmt.Sprint进行格式化 +type StrCommonWrapper struct { + BaseTypeWrapper +} + +func NewStrCommonWrapper() TypeWrapper { + return &StrCommonWrapper{} +} + +func (*StrCommonWrapper) Supported(value reflect.Value) bool { + // 不对time类型做处理 + if _, ok := value.Interface().(time.Time); ok { + return false + } + return true +} + +func (*StrCommonWrapper) Wrap(valueElem reflect.Value, index int, tagMap nmap.SMap) reflect.Value { + return reflect.ValueOf(nreflect.String(valueElem)) +} + +func (t *StrCommonWrapper) Set(fieldElem reflect.Value, value any, tagMap nmap.SMap) { + // Set是仅针对struct的情况 +} diff --git a/nmapper/wrapper/str_time.go b/nmapper/wrapper/str_time.go new file mode 100644 index 0000000..7668aa7 --- /dev/null +++ b/nmapper/wrapper/str_time.go @@ -0,0 +1,55 @@ +package wrapper + +import ( + "git.noahlan.cn/noahlan/ntool/nmap" + "git.noahlan.cn/noahlan/ntool/ntime" + "reflect" + "time" +) + +// StrTimeWrapper Time->string | string->Time +type StrTimeWrapper struct { + BaseTypeWrapper + timeType reflect.Type +} + +func NewStrTimeWrapper() TypeWrapper { + return &StrTimeWrapper{ + timeType: reflect.TypeOf(time.Time{}), + } +} + +func (*StrTimeWrapper) Supported(value reflect.Value) bool { + if _, ok := value.Interface().(time.Time); ok { + return true + } + return false +} + +func (*StrTimeWrapper) Wrap(valueElem reflect.Value, index int, tagMap nmap.SMap) reflect.Value { + layout := tagMap.Default("layout", time.DateTime) + t := valueElem.Interface().(time.Time) + return reflect.ValueOf(t.Format(layout)) +} + +func (t *StrTimeWrapper) Set(fieldElem reflect.Value, value any, tagMap nmap.SMap) { + layout := tagMap.Default("layout", time.DateTime) + var timeStr string + switch d := value.(type) { + case []byte: + timeStr = string(d) + case string: + timeStr = d + case int64: + fieldElem.Set(reflect.ValueOf(time.Unix(d, 0))) + case time.Time: + if fieldElem.Type() == t.timeType { + timeStr = "" + fieldElem.Set(reflect.ValueOf(value)) + } + } + if timeStr != "" && fieldElem.Type() == t.timeType { + t, _ := ntime.ParseTime(timeStr, layout) + fieldElem.Set(reflect.ValueOf(t)) + } +} diff --git a/nmapper/wrapper/str_wrapper.go b/nmapper/wrapper/str_wrapper.go new file mode 100644 index 0000000..2f5bfe4 --- /dev/null +++ b/nmapper/wrapper/str_wrapper.go @@ -0,0 +1,8 @@ +package wrapper + +func StrWrapper() []TypeWrapper { + return []TypeWrapper{ + NewStrCommonWrapper(), + NewStrTimeWrapper(), + } +} diff --git a/nmapper/wrapper/wrapper.go b/nmapper/wrapper/wrapper.go new file mode 100644 index 0000000..b97efa3 --- /dev/null +++ b/nmapper/wrapper/wrapper.go @@ -0,0 +1,32 @@ +package wrapper + +import ( + "git.noahlan.cn/noahlan/ntool/nmap" + "reflect" +) + +type TypeWrapper interface { + // Supported 转换器是否支持对应属性类型 + Supported(value reflect.Value) bool + // Wrap 将对应属性转换为需要类型的值 + Wrap(valueElem reflect.Value, index int, tagMap nmap.SMap) reflect.Value + // Set 设置对应属性值 + Set(fieldElem reflect.Value, value any, tagMap nmap.SMap) +} + +var _ TypeWrapper = (*BaseTypeWrapper)(nil) + +type BaseTypeWrapper struct { +} + +func (w *BaseTypeWrapper) Supported(value reflect.Value) bool { + return false +} + +func (w *BaseTypeWrapper) Wrap(valueElem reflect.Value, index int, tagMap nmap.SMap) reflect.Value { + panic("implement me") +} + +func (w *BaseTypeWrapper) Set(fieldElem reflect.Value, value any, tagMap nmap.SMap) { + fieldElem.Set(reflect.ValueOf(value)) +}