diff --git a/README.md b/README.md index 8e3ceec..692be60 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ json/yaml/http header/query string 转成struct定义,免去手写struct的烦恼. -## 一、json字符串生成结构体 +## 一、json/yaml/http header/query string 生成结构体 +### 1.1 json字符串生成结构体 ```go import ( "github.com/antlabs/tostruct/json" @@ -73,7 +74,7 @@ func main() { } ``` -## 二、http header生成结构体 +### 1.2 http header生成结构体 ```go import ( "github.com/antlabs/tostruct/header" @@ -105,7 +106,7 @@ type test struct { */ } ``` -## 三、查询字符串生成结构体 +### 1.3、查询字符串生成结构体 ```go import ( "github.com/antlabs/tostruct/url" @@ -132,7 +133,7 @@ type test struct { ``` -## 四、yaml生成结构体 +### 1.4 yaml生成结构体 ```go import ( "github.com/antlabs/tostruct/url" @@ -209,3 +210,39 @@ type F struct { */ } ``` + +## 二、各种配置项函数用法 +### 2.1 option.WithSpecifyType 指定生成类型 +```go +obj := ` +{ + "action": "get", + "count": 0, + "data": { + "a123": "delivered" + }, + "duration": 5, + "entities": [], + "timestamp": 1542601830084, + "uri": "http://XXXX/XXXX/XXXX/users/user1/offline_msg_status/123" +} + ` +// 默认对象是转成结构体的,比如这里的data成员,有些接口返回的key是变动的,比如key是用户名value是消息是否投递成功(假装在im系统中) +// 此类业务就需要转成map[string]string类型,这里就可以用上option.WithSpecifyType +// 传参的类型是map[string]string, key为json路径,value值是要指定的类型 +all, err := Marshal([]byte(obj), option.WithStructName("reqName"), option.WithTagName("json"), option.WithSpecifyType(map[string]string{ + ".data": "map[string]string", +})) + +// output all +type reqName struct { + Action string `json:"action"` + Count int `json:"count"` + Data map[string]string `json:"data"` + Duration int `json:"duration"` + entities interface{} `json:"json:entities"` + Timestamp int `json:"timestamp"` + URI string `json:"uri"` +} + +``` \ No newline at end of file diff --git a/json/json.go b/json/json.go index 015d4b6..76908cd 100644 --- a/json/json.go +++ b/json/json.go @@ -88,6 +88,7 @@ const ( boolFmt = "%s %sbool `%s:\"%s\"`" float64Fmt = "%s %sfloat64 `%s:\"%s\"`" intFmt = "%s %sint `%s:\"%s\"`" + specifytFmt = "%s %s `%s:\"%s\"`" ) func Marshal[T Type](t T, opt ...option.OptionFunc) (b []byte, err error) { @@ -104,6 +105,7 @@ func newDefault() *JSON { Option: option.Option{Tag: "json", StructName: defStructName, Inline: true}} } + func new[T Type](t T, opt ...option.OptionFunc) (f *JSON, err error) { var tmp any = t var rv *JSON @@ -155,7 +157,7 @@ func new[T Type](t T, opt ...option.OptionFunc) (f *JSON, err error) { } func (f *JSON) marshal() (b []byte, err error) { - f.marshalValue("", f.obj, false, 0, &f.buf) + f.marshalValue("", f.obj, false, 0, &f.buf, "") f.buf.WriteString(endStruct) if !f.Inline { keys := mapex.Keys(f.structBuf) @@ -170,6 +172,7 @@ func (f *JSON) marshal() (b []byte, err error) { } if b, err = format.Source(f.buf.Bytes()); err != nil { + fmt.Printf("%s\n", f.buf.String()) return nil, err } @@ -193,7 +196,11 @@ func (f *JSON) getStructTypeName(fieldName string) (structTypeName string, buf * } } -func (f *JSON) marshalMap(key string, m map[string]interface{}, typePrefix string, depth int, buf *bytes.Buffer) { +func appendKeyPath(pathKey string, key string) string { + return fmt.Sprintf("%s.%s", pathKey, strings.ToLower(key)) +} + +func (f *JSON) marshalMap(key string, m map[string]interface{}, typePrefix string, depth int, buf *bytes.Buffer, pathKey string) { remaining := len(m) @@ -212,6 +219,7 @@ func (f *JSON) marshalMap(key string, m map[string]interface{}, typePrefix strin if len(key) > 0 { if f.Inline { + // 如果是内嵌结构体 buf.WriteString(fmt.Sprintf(startInlineMap, fieldName, typePrefix)) } else { // 生成struct类型名和子结构体可以保存的子buf @@ -229,7 +237,7 @@ func (f *JSON) marshalMap(key string, m map[string]interface{}, typePrefix strin f.writeIndent(buf, depth+1) - f.marshalValue(key, m[key], false, depth+1, buf) + f.marshalValue(key, m[key], false, depth+1, buf, appendKeyPath(pathKey, key)) f.writeObjSep(buf) } @@ -245,16 +253,16 @@ func (f *JSON) marshalMap(key string, m map[string]interface{}, typePrefix strin } } -func (f *JSON) marshalArray(key string, a []interface{}, depth int, buf *bytes.Buffer) { +func (f *JSON) marshalArray(key string, a []interface{}, depth int, buf *bytes.Buffer, keyPath string) { if len(a) == 0 { buf.WriteString(fmt.Sprintf("%s interface{} `json:\"json:%s\"`", key, key)) return } - f.marshalValue(key, a[0], true, depth, buf) + f.marshalValue(key, a[0], true, depth, buf, keyPath) } -func (f *JSON) marshalValue(key string, obj interface{}, fromArray bool, depth int, buf *bytes.Buffer) { +func (f *JSON) marshalValue(key string, obj interface{}, fromArray bool, depth int, buf *bytes.Buffer, keyPath string) { typePrefix := "" if fromArray { typePrefix = "[]" @@ -262,6 +270,14 @@ func (f *JSON) marshalValue(key string, obj interface{}, fromArray bool, depth i fieldName, tagName := name.GetFieldAndTagName(key) + if f.TypeMap != nil { + fieldType, ok := f.TypeMap[keyPath] + if ok { + buf.WriteString(fmt.Sprintf(specifytFmt, fieldName, fieldType, f.Tag, tagName)) + return + } + } + tmpFieldName := strings.ToUpper(fieldName) if tab.InitialismsTab[tmpFieldName] { fieldName = tmpFieldName @@ -269,9 +285,9 @@ func (f *JSON) marshalValue(key string, obj interface{}, fromArray bool, depth i switch v := obj.(type) { case map[string]interface{}: - f.marshalMap(key, v, typePrefix, depth, buf) + f.marshalMap(key, v, typePrefix, depth, buf, keyPath) case []interface{}: - f.marshalArray(key, v, depth, buf) + f.marshalArray(key, v, depth, buf, keyPath+"[0]") case string: buf.WriteString(fmt.Sprintf(stringFmt, fieldName, typePrefix, f.Tag, tagName)) case float64: //json默认解析的数字是float64类型 diff --git a/json/json_empty_test.go b/json/json_empty_test.go new file mode 100644 index 0000000..f6d0ffc --- /dev/null +++ b/json/json_empty_test.go @@ -0,0 +1,15 @@ +package json + +import ( + "testing" + + "github.com/antlabs/tostruct/option" + "github.com/stretchr/testify/assert" +) + +func TestEmpty(t *testing.T) { + str := []byte("") + all, err := Marshal(str, option.WithStructName("empty"), option.WithTagName("json")) + assert.Error(t, err) + assert.Equal(t, all, []byte(nil)) +} diff --git a/json/json_test.go b/json/json_test.go index d8be94c..c747c07 100644 --- a/json/json_test.go +++ b/json/json_test.go @@ -170,3 +170,89 @@ func Test_Gen_Obj_JSON6(t *testing.T) { } } + +func Test_Gen_Obj_JSON7(t *testing.T) { + + obj := ` +{ + "action": "get", + "count": 0, + "data": { + "a123": "delivered" + }, + "duration": 5, + "entities": [], + "timestamp": 1542601830084, + "uri": "http://XXXX/XXXX/XXXX/users/user1/offline_msg_status/123" +} + ` + + for _, v := range [][]byte{ + func() []byte { + all, err := Marshal([]byte(obj), option.WithStructName("reqName"), option.WithTagName("json"), option.WithSpecifyType(map[string]string{ + ".data": "map[string]string", + })) + assert.NoError(t, err) + return all + }(), + func() []byte { + all, err := Marshal([]byte(obj), option.WithStructName("reqName"), option.WithTagName("json"), option.WithNotInline(), option.WithSpecifyType(map[string]string{ + ".data": "map[string]string", + })) + assert.NoError(t, err) + return all + }(), + } { + + all := v + fmt.Println(string(all)) + need, err := os.ReadFile("../testdata/testjson7.0.txt") + assert.NoError(t, err) + assert.Equal(t, string(bytes.TrimSpace(need)), string(bytes.TrimSpace(all))) + } + +} + +func Test_Gen_Obj_JSON8(t *testing.T) { + + obj := ` +{ + "action": "get", + "count": 0, + "data": { + "data": { + "a123": "delivered" + } + }, + "duration": 5, + "entities": [], + "timestamp": 1542601830084, + "uri": "http://XXXX/XXXX/XXXX/users/user1/offline_msg_status/123" +} + ` + + for k, v := range [][]byte{ + func() []byte { + all, err := Marshal([]byte(obj), option.WithStructName("reqName"), option.WithTagName("json"), option.WithSpecifyType(map[string]string{ + ".data.data": "map[string]string", + })) + assert.NoError(t, err) + return all + }(), + func() []byte { + all, err := Marshal([]byte(obj), option.WithStructName("reqName"), option.WithTagName("json"), option.WithNotInline(), option.WithSpecifyType(map[string]string{ + ".data.data": "map[string]string", + })) + assert.NoError(t, err) + return all + }(), + } { + + all := v + fmt.Println(string(all)) + need, err := os.ReadFile(fmt.Sprintf("../testdata/testjson8.%d.txt", k)) + assert.NoError(t, err) + assert.Equal(t, string(bytes.TrimSpace(need)), string(bytes.TrimSpace(all))) + } + +} diff --git a/option/option.go b/option/option.go index 13c15d0..5d6a8d2 100644 --- a/option/option.go +++ b/option/option.go @@ -8,6 +8,7 @@ type Option struct { Inline bool Tag string StructName string + TypeMap map[string]string } // 控制生成的结构体是否内联 @@ -42,3 +43,22 @@ func WithStructName(name string) OptionFunc { c.StructName = name } } + +// 指定类型, datal默认转成struct, 这里直接指定生成map[string]string类型 +// { +// +// "data" : { +// "user1": "111" +// } +// } +// +// WithSpecifyType(map[string]string{ +// ".data": "map[string]string" +// }) +// +// 目录只支持json/yaml +func WithSpecifyType(typeMap map[string]string) OptionFunc { + return func(c *Option) { + c.TypeMap = typeMap + } +} diff --git a/testdata/testjson7.0.txt b/testdata/testjson7.0.txt new file mode 100644 index 0000000..e8a3abf --- /dev/null +++ b/testdata/testjson7.0.txt @@ -0,0 +1,9 @@ +type reqName struct { + Action string `json:"action"` + Count int `json:"count"` + Data map[string]string `json:"data"` + Duration int `json:"duration"` + entities interface{} `json:"json:entities"` + Timestamp int `json:"timestamp"` + URI string `json:"uri"` +} diff --git a/testdata/testjson8.0.txt b/testdata/testjson8.0.txt new file mode 100644 index 0000000..85f5ecf --- /dev/null +++ b/testdata/testjson8.0.txt @@ -0,0 +1,11 @@ +type reqName struct { + Action string `json:"action"` + Count int `json:"count"` + Data struct { + Data map[string]string `json:"data"` + } `json:"data"` + Duration int `json:"duration"` + entities interface{} `json:"json:entities"` + Timestamp int `json:"timestamp"` + URI string `json:"uri"` +} diff --git a/testdata/testjson8.1.txt b/testdata/testjson8.1.txt new file mode 100644 index 0000000..be68375 --- /dev/null +++ b/testdata/testjson8.1.txt @@ -0,0 +1,13 @@ +type reqName struct { + Action string `json:"action"` + Count int `json:"count"` + Data Data `json:"data"` + Duration int `json:"duration"` + entities interface{} `json:"json:entities"` + Timestamp int `json:"timestamp"` + URI string `json:"uri"` +} + +type Data struct { + Data map[string]string `json:"data"` +}