API: Improve MarshalToJson() in common/reflect/marshal.go (#3655)

* Serialize enum to string in MarshalToJson().

* MarshalToJson() respect json tags.

* Add insertTypeInfo parameter to MarshalToJson().

* Omit empty string in MarshalToJson().

* Serialize PortList to string in MarshalToJson().

---------

Co-authored-by: nobody <nobody@nowhere.mars>
This commit is contained in:
nobody 2024-08-08 22:35:46 +08:00 committed by GitHub
parent 4a1c0d7124
commit ac628a9427
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 148 additions and 41 deletions

View file

@ -2,13 +2,17 @@ package reflect
import ( import (
"encoding/json" "encoding/json"
"fmt"
"reflect" "reflect"
"strings"
cnet "github.com/xtls/xray-core/common/net"
cserial "github.com/xtls/xray-core/common/serial" cserial "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/infra/conf"
) )
func MarshalToJson(v interface{}) (string, bool) { func MarshalToJson(v interface{}, insertTypeInfo bool) (string, bool) {
if itf := marshalInterface(v, true); itf != nil { if itf := marshalInterface(v, true, insertTypeInfo); itf != nil {
if b, err := json.MarshalIndent(itf, "", " "); err == nil { if b, err := json.MarshalIndent(itf, "", " "); err == nil {
return string(b[:]), true return string(b[:]), true
} }
@ -16,7 +20,7 @@ func MarshalToJson(v interface{}) (string, bool) {
return "", false return "", false
} }
func marshalTypedMessage(v *cserial.TypedMessage, ignoreNullValue bool) interface{} { func marshalTypedMessage(v *cserial.TypedMessage, ignoreNullValue bool, insertTypeInfo bool) interface{} {
if v == nil { if v == nil {
return nil return nil
} }
@ -24,36 +28,67 @@ func marshalTypedMessage(v *cserial.TypedMessage, ignoreNullValue bool) interfac
if err != nil { if err != nil {
return nil return nil
} }
r := marshalInterface(tmsg, ignoreNullValue) r := marshalInterface(tmsg, ignoreNullValue, insertTypeInfo)
if msg, ok := r.(map[string]interface{}); ok { if msg, ok := r.(map[string]interface{}); ok && insertTypeInfo {
msg["_TypedMessage_"] = v.Type msg["_TypedMessage_"] = v.Type
} }
return r return r
} }
func marshalSlice(v reflect.Value, ignoreNullValue bool) interface{} { func marshalSlice(v reflect.Value, ignoreNullValue bool, insertTypeInfo bool) interface{} {
r := make([]interface{}, 0) r := make([]interface{}, 0)
for i := 0; i < v.Len(); i++ { for i := 0; i < v.Len(); i++ {
rv := v.Index(i) rv := v.Index(i)
if rv.CanInterface() { if rv.CanInterface() {
value := rv.Interface() value := rv.Interface()
r = append(r, marshalInterface(value, ignoreNullValue)) r = append(r, marshalInterface(value, ignoreNullValue, insertTypeInfo))
} }
} }
return r return r
} }
func marshalStruct(v reflect.Value, ignoreNullValue bool) interface{} { func isNullValue(f reflect.StructField, rv reflect.Value) bool {
if rv.Kind() == reflect.String && rv.Len() == 0 {
return true
} else if !isValueKind(rv.Kind()) && rv.IsNil() {
return true
} else if tag := f.Tag.Get("json"); strings.Contains(tag, "omitempty") {
if !rv.IsValid() || rv.IsZero() {
return true
}
}
return false
}
func toJsonName(f reflect.StructField) string {
if tags := f.Tag.Get("protobuf"); len(tags) > 0 {
for _, tag := range strings.Split(tags, ",") {
if before, after, ok := strings.Cut(tag, "="); ok && before == "json" {
return after
}
}
}
if tag := f.Tag.Get("json"); len(tag) > 0 {
if before, _, ok := strings.Cut(tag, ","); ok {
return before
} else {
return tag
}
}
return f.Name
}
func marshalStruct(v reflect.Value, ignoreNullValue bool, insertTypeInfo bool) interface{} {
r := make(map[string]interface{}) r := make(map[string]interface{})
t := v.Type() t := v.Type()
for i := 0; i < v.NumField(); i++ { for i := 0; i < v.NumField(); i++ {
rv := v.Field(i) rv := v.Field(i)
if rv.CanInterface() { if rv.CanInterface() {
ft := t.Field(i) ft := t.Field(i)
name := ft.Name if !ignoreNullValue || !isNullValue(ft, rv) {
name := toJsonName(ft)
value := rv.Interface() value := rv.Interface()
tv := marshalInterface(value, ignoreNullValue) tv := marshalInterface(value, ignoreNullValue, insertTypeInfo)
if tv != nil || !ignoreNullValue {
r[name] = tv r[name] = tv
} }
} }
@ -61,7 +96,7 @@ func marshalStruct(v reflect.Value, ignoreNullValue bool) interface{} {
return r return r
} }
func marshalMap(v reflect.Value, ignoreNullValue bool) interface{} { func marshalMap(v reflect.Value, ignoreNullValue bool, insertTypeInfo bool) interface{} {
// policy.level is map[uint32] *struct // policy.level is map[uint32] *struct
kt := v.Type().Key() kt := v.Type().Key()
vt := reflect.TypeOf((*interface{})(nil)) vt := reflect.TypeOf((*interface{})(nil))
@ -71,7 +106,7 @@ func marshalMap(v reflect.Value, ignoreNullValue bool) interface{} {
rv := v.MapIndex(key) rv := v.MapIndex(key)
if rv.CanInterface() { if rv.CanInterface() {
iv := rv.Interface() iv := rv.Interface()
tv := marshalInterface(iv, ignoreNullValue) tv := marshalInterface(iv, ignoreNullValue, insertTypeInfo)
if tv != nil || !ignoreNullValue { if tv != nil || !ignoreNullValue {
r.SetMapIndex(key, reflect.ValueOf(&tv)) r.SetMapIndex(key, reflect.ValueOf(&tv))
} }
@ -87,27 +122,63 @@ func marshalIString(v interface{}) (r string, ok bool) {
ok = false ok = false
} }
}() }()
if iStringFn, ok := v.(interface{ String() string }); ok { if iStringFn, ok := v.(interface{ String() string }); ok {
return iStringFn.String(), true return iStringFn.String(), true
} }
return "", false return "", false
} }
func marshalKnownType(v interface{}, ignoreNullValue bool) (interface{}, bool) { func serializePortList(portList *cnet.PortList) (interface{}, bool) {
if portList == nil {
return nil, false
}
n := len(portList.Range)
if n == 1 {
if first := portList.Range[0]; first.From == first.To {
return first.From, true
}
}
r := make([]string, 0, n)
for _, pr := range portList.Range {
if pr.From == pr.To {
r = append(r, pr.FromPort().String())
} else {
r = append(r, fmt.Sprintf("%d-%d", pr.From, pr.To))
}
}
return strings.Join(r, ","), true
}
func marshalKnownType(v interface{}, ignoreNullValue bool, insertTypeInfo bool) (interface{}, bool) {
switch ty := v.(type) { switch ty := v.(type) {
case cserial.TypedMessage: case cserial.TypedMessage:
return marshalTypedMessage(&ty, ignoreNullValue), true return marshalTypedMessage(&ty, ignoreNullValue, insertTypeInfo), true
case *cserial.TypedMessage: case *cserial.TypedMessage:
return marshalTypedMessage(ty, ignoreNullValue), true return marshalTypedMessage(ty, ignoreNullValue, insertTypeInfo), true
case map[string]json.RawMessage: case map[string]json.RawMessage:
return ty, true return ty, true
case []json.RawMessage: case []json.RawMessage:
return ty, true return ty, true
case *json.RawMessage: case *json.RawMessage, json.RawMessage:
return ty, true
case json.RawMessage:
return ty, true return ty, true
case *cnet.IPOrDomain:
if domain := v.(*cnet.IPOrDomain); domain != nil {
return domain.AsAddress().String(), true
}
return nil, false
case *cnet.PortList:
npl := v.(*cnet.PortList)
return serializePortList(npl)
case *conf.PortList:
cpl := v.(*conf.PortList)
return serializePortList(cpl.Build())
case cnet.Address:
if addr := v.(cnet.Address); addr != nil {
return addr.String(), true
}
return nil, false
default: default:
return nil, false return nil, false
} }
@ -138,9 +209,9 @@ func isValueKind(kind reflect.Kind) bool {
} }
} }
func marshalInterface(v interface{}, ignoreNullValue bool) interface{} { func marshalInterface(v interface{}, ignoreNullValue bool, insertTypeInfo bool) interface{} {
if r, ok := marshalKnownType(v, ignoreNullValue); ok { if r, ok := marshalKnownType(v, ignoreNullValue, insertTypeInfo); ok {
return r return r
} }
@ -152,19 +223,27 @@ func marshalInterface(v interface{}, ignoreNullValue bool) interface{} {
if k == reflect.Invalid { if k == reflect.Invalid {
return nil return nil
} }
if isValueKind(k) {
if ty := rv.Type().Name(); isValueKind(k) {
if k.String() != ty {
if s, ok := marshalIString(v); ok {
return s
}
}
return v return v
} }
// fmt.Println("kind:", k, "type:", rv.Type().Name())
switch k { switch k {
case reflect.Struct: case reflect.Struct:
return marshalStruct(rv, ignoreNullValue) return marshalStruct(rv, ignoreNullValue, insertTypeInfo)
case reflect.Slice: case reflect.Slice:
return marshalSlice(rv, ignoreNullValue) return marshalSlice(rv, ignoreNullValue, insertTypeInfo)
case reflect.Array: case reflect.Array:
return marshalSlice(rv, ignoreNullValue) return marshalSlice(rv, ignoreNullValue, insertTypeInfo)
case reflect.Map: case reflect.Map:
return marshalMap(rv, ignoreNullValue) return marshalMap(rv, ignoreNullValue, insertTypeInfo)
default: default:
break break
} }

View file

@ -6,11 +6,40 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/xtls/xray-core/common/protocol"
. "github.com/xtls/xray-core/common/reflect" . "github.com/xtls/xray-core/common/reflect"
cserial "github.com/xtls/xray-core/common/serial" cserial "github.com/xtls/xray-core/common/serial"
iserial "github.com/xtls/xray-core/infra/conf/serial" iserial "github.com/xtls/xray-core/infra/conf/serial"
"github.com/xtls/xray-core/proxy/shadowsocks"
) )
func TestMashalAccount(t *testing.T) {
account := &shadowsocks.Account{
Password: "shadowsocks-password",
CipherType: shadowsocks.CipherType_CHACHA20_POLY1305,
}
user := &protocol.User{
Level: 0,
Email: "love@v2ray.com",
Account: cserial.ToTypedMessage(account),
}
j, ok := MarshalToJson(user, false)
if !ok || strings.Contains(j, "_TypedMessage_") {
t.Error("marshal account failed")
}
kws := []string{"CHACHA20_POLY1305", "cipherType", "shadowsocks-password"}
for _, kw := range kws {
if !strings.Contains(j, kw) {
t.Error("marshal account failed")
}
}
// t.Log(j)
}
func TestMashalStruct(t *testing.T) { func TestMashalStruct(t *testing.T) {
type Foo = struct { type Foo = struct {
N int `json:"n"` N int `json:"n"`
@ -36,8 +65,8 @@ func TestMashalStruct(t *testing.T) {
Arr: &arr, Arr: &arr,
} }
s, ok1 := MarshalToJson(f1) s, ok1 := MarshalToJson(f1, true)
sp, ok2 := MarshalToJson(&f1) sp, ok2 := MarshalToJson(&f1, true)
if !ok1 || !ok2 || s != sp { if !ok1 || !ok2 || s != sp {
t.Error("marshal failed") t.Error("marshal failed")
@ -69,7 +98,7 @@ func TestMarshalConfigJson(t *testing.T) {
} }
tmsg := cserial.ToTypedMessage(bc) tmsg := cserial.ToTypedMessage(bc)
tc, ok := MarshalToJson(tmsg) tc, ok := MarshalToJson(tmsg, true)
if !ok { if !ok {
t.Error("marshal config failed") t.Error("marshal config failed")
} }
@ -79,15 +108,14 @@ func TestMarshalConfigJson(t *testing.T) {
keywords := []string{ keywords := []string{
"4784f9b8-a879-4fec-9718-ebddefa47750", "4784f9b8-a879-4fec-9718-ebddefa47750",
"bing.com", "bing.com",
"DomainStrategy", "inboundTag",
"InboundTag", "level",
"Level", "stats",
"Stats", "userDownlink",
"UserDownlink", "userUplink",
"UserUplink", "system",
"System", "inboundDownlink",
"InboundDownlink", "outboundUplink",
"OutboundUplink",
} }
for _, kw := range keywords { for _, kw := range keywords {
if !strings.Contains(tc, kw) { if !strings.Contains(tc, kw) {

View file

@ -17,7 +17,7 @@ func MergeConfigFromFiles(files []string, formats []string) (string, error) {
return "", err return "", err
} }
if j, ok := creflect.MarshalToJson(c); ok { if j, ok := creflect.MarshalToJson(c, true); ok {
return j, nil return j, nil
} }
return "", errors.New("marshal to json failed.").AtError() return "", errors.New("marshal to json failed.").AtError()