mirror of
https://github.com/charlienet/go-mixed.git
synced 2025-07-18 08:32:40 +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"}
|
v1 := struct{ Abc string }{Abc: "abc"}
|
||||||
v2 := struct{ Abc string }{}
|
v2 := struct{ Abc string }{}
|
||||||
|
|
||||||
Copy(v1, v2, IgnoreEmpty())
|
Copy(v1, &v2, IgnoreEmpty())
|
||||||
|
|
||||||
t.Log(v2)
|
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 {
|
o := struct {
|
||||||
UserName string
|
UserName string
|
||||||
InTagName string `json:"in_tag_name,omitempty"`
|
InTagName string `json:"in_tag_name,omitempty"`
|
||||||
|
Ignore string `json:"-"`
|
||||||
KeepEmpty int
|
KeepEmpty int
|
||||||
OmitEmpty int `json:",omitempty"`
|
OmitEmpty int `json:",omitempty"`
|
||||||
}{
|
}{
|
||||||
UserName: "测试字段",
|
UserName: "测试字段",
|
||||||
InTagName: "具体名称",
|
InTagName: "具体名称",
|
||||||
|
Ignore: "这个字段跳过",
|
||||||
KeepEmpty: 0,
|
KeepEmpty: 0,
|
||||||
OmitEmpty: 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.IgnoreEmpty()))
|
||||||
t.Log(structs.ToMap(o, structs.Omitempty()))
|
t.Log(structs.ToMap(o, structs.Omitempty()))
|
||||||
t.Log(structs.ToMap(o, structs.Lcfirst()))
|
t.Log(structs.ToMap(o, structs.Lcfirst()))
|
@ -1,18 +1,24 @@
|
|||||||
package structs
|
package structs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/charlienet/go-mixed/json"
|
"github.com/charlienet/go-mixed/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
type optionFunc func(*option)
|
type optionFunc func(*option)
|
||||||
|
|
||||||
type option struct {
|
type option struct {
|
||||||
NameFunc func(string) string
|
TagName string
|
||||||
IgnoreEmpty bool
|
|
||||||
DeepCopy bool
|
DeepCopy bool
|
||||||
Omitempty 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 {
|
func IgnoreEmpty() optionFunc {
|
||||||
@ -21,42 +27,43 @@ func IgnoreEmpty() optionFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeepCopy() optionFunc {
|
|
||||||
return func(o *option) {
|
|
||||||
o.DeepCopy = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Omitempty() optionFunc {
|
func Omitempty() optionFunc {
|
||||||
return func(o *option) {
|
return func(o *option) {
|
||||||
o.Omitempty = true
|
o.Omitempty = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeepCopy() optionFunc {
|
||||||
|
return func(o *option) {
|
||||||
|
o.DeepCopy = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Lcfirst() optionFunc {
|
func Lcfirst() optionFunc {
|
||||||
return func(o *option) {
|
return func(o *option) {
|
||||||
o.NameFunc = json.Lcfirst
|
o.NameConverter = json.Lcfirst
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Camel2Case() optionFunc {
|
func Camel2Case() optionFunc {
|
||||||
return func(o *option) {
|
return func(o *option) {
|
||||||
o.NameFunc = json.Camel2Case
|
o.NameConverter = json.Camel2Case
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createOptions(opts []optionFunc) *option {
|
func defaultOptions() option {
|
||||||
o := &option{
|
return option{
|
||||||
NameFunc: func(s string) string { return s },
|
TagName: defaultTagName,
|
||||||
|
Ignore: true,
|
||||||
|
NameConverter: func(s string) string { return s },
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func acquireOptions(opts []optionFunc) option {
|
||||||
|
o := defaultOptions()
|
||||||
for _, f := range opts {
|
for _, f := range opts {
|
||||||
f(o)
|
f(&o)
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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
|
package structs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/charlienet/go-mixed/expr"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type tagOptions string
|
type tagOptions string
|
||||||
@ -19,6 +16,7 @@ func (o tagOptions) Contains(optionName string) bool {
|
|||||||
if len(o) == 0 {
|
if len(o) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
s := string(o)
|
s := string(o)
|
||||||
for s != "" {
|
for s != "" {
|
||||||
var name string
|
var name string
|
||||||
@ -27,23 +25,10 @@ func (o tagOptions) Contains(optionName string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
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 {
|
func isValidTag(s string) bool {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return false
|
return false
|
||||||
@ -56,5 +41,6 @@ func isValidTag(s string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
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