diff --git a/LICENSE b/LICENSE index 17d7e97..3b7cdf0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2020 Liang Yaopei +Copyright (c) 2024 Abdul Hakim Ghaniy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7b63a28..fa74cbb 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,12 @@ Also, it skips unexported filed , nil pointer and field without tag. ## Tags -For now, it supports 4 tags: +For now, it supports 5 tags: - '-' means ignoring this field - 'omitempty' means ignoring this field when it is empty - 'dive' means recursively traversing the struct, converting every filed in struct to be a key in map - 'wildcard' applies for string value, it returns "%" +value +"%", which is conveniently for db wildcard query +- 'dotted' is useful for some parsers that need dot-style keys, such as "user.address.street": "his street address" ## Example diff --git a/README_zh.md b/README_zh.md index a4e9465..cf3b0c8 100644 --- a/README_zh.md +++ b/README_zh.md @@ -7,11 +7,12 @@ 并且,会跳过没有导出的域,空指针,和没有标签的域。 ## 标签 -目前,支持4种标签 +目前,支持5种标签 - '-':忽略当前这个域 - 'omitempty' : 当这个域的值为空,忽略这个域 - 'dive' : 递归地遍历这个结构体,将所有字段作为键 - 'wildcard': 只适用于字符串类型,返回"%"+值+"%",这是为了方便数据库的模糊查询 +- 'dotted': 对于某些需要点式样式关键字的解析器很有用,例如“user.address.street”:“his street address” ## 例子 举个例子, diff --git a/go.mod b/go.mod index b46b931..7b7b157 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/liangyaopei/structmap +module github.com/GhaniyKie/structmap -go 1.14 +go 1.14 \ No newline at end of file diff --git a/structmap.go b/structmap.go index 53db7d2..96910f9 100644 --- a/structmap.go +++ b/structmap.go @@ -6,180 +6,222 @@ import ( "strings" ) -const ( - methodResNum = 2 -) +type MappedStruct map[string]interface{} const ( - OptIgnore = "-" - OptOmitempty = "omitempty" - OptDive = "dive" - OptWildcard = "wildcard" + OPTION_IGNORE = "-" + OPTION_OMITEMPTY = "omitempty" + OPTION_DIVE = "dive" + OPTION_WILDCARD = "wildcard" + OPTION_DOTTED = "dotted" + + method_results_total = 2 ) const ( - flagIgnore = 1 << iota - flagOmiEmpty - flagDive - flagWildcard + FLAG_IGNORE = 1 << iota + FLAG_OMITEMPTY + FLAG_DIVE + FLAG_WILDCARD + FLAG_DOTTED ) -// StructToMap convert a golang sturct to a map -// key can be specified by tag, LIKE `map:"tag"`. -// If there is no tag, struct filed name will be used instead -// methodName is the name the field has implemented. -// If implemented, it uses the method to get the key and value -func StructToMap(s interface{}, tag string, methodName string) (res map[string]interface{}, err error) { - v := reflect.ValueOf(s) - - if v.Kind() == reflect.Ptr && v.IsNil() { - return nil, fmt.Errorf("%s is a nil pointer", v.Kind().String()) - } - if v.Kind() == reflect.Ptr { - v = v.Elem() +// StructToMap maps a struct by its tag. +// +// Key can be specified by tag, LIKE `json:"tag"`, or `map:"tag"`. Whatever you want. +// Specify the tag in the second parameter. Options are: +// - `omitempty` to omit empty fields +// - `dive` to dive into the struct and map the fields +// - `wildcard` to add `%` to the string value +// - `dotted` to add a dot `.` to the key +// +// Notes: +// Dive options will map the child's struct fields directly to the parent map. +// Example: +// +// type A struct { +// AA string `json:"aa"` +// B B `json:"b,dive"` +// } +// type B struct { +// C string `json:"c"` +// } +// +// Result: +// map[aa:string c:string] +// +// While dive options will map the child's struct fields directly to the parent map. +// Dotted options will add a dot `.` to the child's struct tag, followed by it's fields tag. Example: +// +// type A struct { +// AA string `json:"aa"` +// B B `json:"b,dotted"` +// } +// type B struct { +// C string `json:"c"` +// } +// +// Result: +// map[aa:string b.c:string] +func StructToMap(data interface{}, tag string, method string) (MappedStruct, error) { + result := make(MappedStruct) + reflectedValue := reflect.ValueOf(data) + + if reflectedValue.Kind() == reflect.Pointer { + if reflectedValue.IsNil() { + return nil, fmt.Errorf("%s is a nil pointer", reflectedValue.Kind().String()) + } + reflectedValue = reflectedValue.Elem() } - // only accept struct param - if v.Kind() != reflect.Struct { - return nil, fmt.Errorf("s is not a struct but %s", v.Kind().String()) + if reflectedValue.Kind() != reflect.Struct { + return nil, fmt.Errorf("data is not a struct but %s", reflectedValue.Kind().String()) } - t := v.Type() - res = make(map[string]interface{}) - for i := 0; i < t.NumField(); i++ { - fieldType := t.Field(i) + reflectType := reflectedValue.Type() + for i := 0; i < reflectType.NumField(); i++ { + fieldType := reflectType.Field(i) // ignore unexported field if fieldType.PkgPath != "" { continue } - // read tag - tagVal, flag := readTag(fieldType, tag) - if flag&flagIgnore != 0 { + tagVal, flag := tagsReader(fieldType, tag) + if flag&FLAG_IGNORE != 0 { continue } - fieldValue := v.Field(i) - if flag&flagOmiEmpty != 0 && fieldValue.IsZero() { + fieldValue := reflectedValue.Field(i) + if flag&FLAG_OMITEMPTY != 0 && fieldValue.IsZero() { continue } + if fieldValue.Kind() == reflect.Pointer { + if fieldValue.IsNil() { + continue + } + fieldValue = fieldValue.Elem() + } - // ignore nil pointer in field - if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { - continue + key, value, err := assignValueWithMethod(fieldValue, method) + if err != nil { + return nil, err } - if fieldValue.Kind() == reflect.Ptr { - fieldValue = fieldValue.Elem() + if key != "" { + result[key] = value + continue } - // get kind switch fieldValue.Kind() { - case reflect.Slice, reflect.Array: - if methodName != "" { - _, ok := fieldValue.Type().MethodByName(methodName) - if ok { - key, value, err := callFunc(fieldValue, methodName) - if err != nil { - return nil, err - } - res[key] = value - continue - } - } - res[tagVal] = fieldValue + case reflect.Slice, reflect.Array, reflect.Map, reflect.Chan: + result[tagVal] = fieldValue case reflect.Struct: - if methodName != "" { - _, ok := fieldValue.Type().MethodByName(methodName) - if ok { - key, value, err := callFunc(fieldValue, methodName) - if err != nil { - return nil, err - } - res[key] = value - continue - } - } - - // recursive - deepRes, deepErr := StructToMap(fieldValue.Interface(), tag, methodName) + deepRes, deepErr := StructToMap(fieldValue.Interface(), tag, method) if deepErr != nil { return nil, deepErr } - if flag&flagDive != 0 { + if flag&FLAG_DIVE != 0 { + for k, v := range deepRes { + result[k] = v + } + } else if flag&FLAG_DOTTED != 0 { for k, v := range deepRes { - res[k] = v + result[tagVal+"."+k] = v } } else { - res[tagVal] = deepRes + result[tagVal] = deepRes } - case reflect.Map: - res[tagVal] = fieldValue - case reflect.Chan: - res[tagVal] = fieldValue - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int, reflect.Int64: - res[tagVal] = fieldValue.Int() - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint, reflect.Uint64: - res[tagVal] = fieldValue.Uint() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + result[tagVal] = fieldValue.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + result[tagVal] = fieldValue.Uint() case reflect.Float32, reflect.Float64: - res[tagVal] = fieldValue.Float() + result[tagVal] = fieldValue.Float() case reflect.String: - if flag&flagWildcard != 0 { - res[tagVal] = "%" + fieldValue.String() + "%" + if flag&FLAG_WILDCARD != 0 { + result[tagVal] = "%" + fieldValue.String() + "%" } else { - res[tagVal] = fieldValue.String() + result[tagVal] = fieldValue.String() } case reflect.Bool: - res[tagVal] = fieldValue.Bool() + result[tagVal] = fieldValue.Bool() case reflect.Complex64, reflect.Complex128: - res[tagVal] = fieldValue.Complex() + result[tagVal] = fieldValue.Complex() case reflect.Interface: - res[tagVal] = fieldValue.Interface() - default: + result[tagVal] = fieldValue.Interface() } } - return + + return result, nil } -// readTag read tag with format `json:"name,omitempty"` or `json:"-"` +// tagsReader read tag with format `json:"name,omitempty"` or `json:"-"` // For now, only supports above format -func readTag(f reflect.StructField, tag string) (string, int) { - val, ok := f.Tag.Lookup(tag) - fieldTag := "" - flag := 0 +func tagsReader(structField reflect.StructField, tag string) (string, int) { + var ( + flag int = 0 + fTag string = "" + ) - // no tag, skip this field + tagValue, ok := structField.Tag.Lookup(tag) if !ok { - flag |= flagIgnore - return "", flag + // if tag not found, ignore the field. + // returns empty string and ignore flag + flag |= FLAG_IGNORE + return fTag, flag } - opts := strings.Split(val, ",") - fieldTag = opts[0] + opts := strings.Split(tagValue, ",") + fTag = opts[0] + for i := 0; i < len(opts); i++ { switch opts[i] { - case OptIgnore: - flag |= flagIgnore - case OptOmitempty: - flag |= flagOmiEmpty - case OptDive: - flag |= flagDive - case OptWildcard: - flag |= flagWildcard + case OPTION_IGNORE: + flag |= FLAG_IGNORE + case OPTION_OMITEMPTY: + flag |= FLAG_OMITEMPTY + case OPTION_DIVE: + flag |= FLAG_DIVE + case OPTION_WILDCARD: + flag |= FLAG_WILDCARD + case OPTION_DOTTED: + flag |= FLAG_DOTTED } } - return fieldTag, flag + return fTag, flag +} + +func assignValueWithMethod(reflectedValue reflect.Value, method string) (key string, value interface{}, err error) { + if method == "" { + return "", nil, nil + } + + _, ok := reflectedValue.Type().MethodByName(method) + if !ok { + return "", nil, nil + } + + key, value, err = callFunc(reflectedValue, method) + if err != nil { + return "", nil, err + } + + return key, value, nil } -// call function -func callFunc(fv reflect.Value, methodName string) (string, interface{}, error) { - methodRes := fv.MethodByName(methodName).Call([]reflect.Value{}) - if len(methodRes) != methodResNum { - return "", nil, fmt.Errorf("wrong method %s, should have 2 output: (string,interface{})", methodName) +// callFunc calls the method and returns the key and value. +// The method should have 2 outputs: (string,interface{}). +func callFunc(reflectedValue reflect.Value, method string) (key string, value interface{}, err error) { + methodResults := reflectedValue.MethodByName(method).Call([]reflect.Value{}) + if len(methodResults) != method_results_total { + return "", nil, fmt.Errorf("wrong method %s, should have 2 output: (string,interface{})", method) } - if methodRes[0].Kind() != reflect.String { - return "", nil, fmt.Errorf("wrong method %s, first output should be string", methodName) + if methodResults[0].Kind() != reflect.String { + return "", nil, fmt.Errorf("wrong method %s, first output should be string", method) } - key := methodRes[0].String() - return key, methodRes[1], nil + + key = methodResults[0].String() + value = methodResults[1].Interface() + + return key, value, nil } diff --git a/structmap_test/to_map_test.go b/structmap_test/structmap_test.go similarity index 91% rename from structmap_test/to_map_test.go rename to structmap_test/structmap_test.go index e05c8e0..76414a9 100644 --- a/structmap_test/to_map_test.go +++ b/structmap_test/structmap_test.go @@ -1,4 +1,4 @@ -package struct_to_map_test +package structmap_test import ( "encoding/json" @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/liangyaopei/structmap" + "github.com/GhaniyKie/structmap" ) const timeLayout = "2006-01-02 15:04:05" @@ -55,6 +55,11 @@ type StructNoDive struct { NoDive int `map:"no_dive"` } +type Address struct { + Province string `map:"province"` + City string `map:"city"` +} + // User is used for demonstration type User struct { Name string `map:"name,omitempty,wildcard"` // string @@ -66,6 +71,7 @@ type User struct { Arr []int `map:"arr,omitempty"` // normal slice MyArr MySlice `map:"my_arr,omitempty"` // slice implements its own method IgnoreFiled string `map:"-"` + Address Address `map:"address,dotted"` } func newUser() User { @@ -85,6 +91,11 @@ func newUser() User { } arr := []int{1, 2, 3} myArr := MySlice{11, 12, 13} + address := Address{ + Province: "province", + City: "city", + } + return User{ Name: name, Email: &email, @@ -95,6 +106,7 @@ func newUser() User { Arr: arr, MyArr: myArr, IgnoreFiled: "ignore", + Address: address, } }