WIP: logger/unwrap: add unwrap method #143
							
								
								
									
										535
									
								
								logger/unwrap/unwrap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										535
									
								
								logger/unwrap/unwrap.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,535 @@ | ||||
| package unwrap | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"reflect" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"go.unistack.org/micro/v3/codec" | ||||
| ) | ||||
|  | ||||
| const sf = "0-+# " | ||||
|  | ||||
| var hexDigits = "0123456789abcdef" | ||||
|  | ||||
| var ( | ||||
| 	panicBytes         = []byte("(PANIC=") | ||||
| 	plusBytes          = []byte("+") | ||||
| 	iBytes             = []byte("i") | ||||
| 	trueBytes          = []byte("true") | ||||
| 	falseBytes         = []byte("false") | ||||
| 	interfaceBytes     = []byte("(interface {})") | ||||
| 	openBraceBytes     = []byte("{") | ||||
| 	closeBraceBytes    = []byte("}") | ||||
| 	asteriskBytes      = []byte("*") | ||||
| 	ampBytes           = []byte("&") | ||||
| 	colonBytes         = []byte(":") | ||||
| 	openParenBytes     = []byte("(") | ||||
| 	closeParenBytes    = []byte(")") | ||||
| 	spaceBytes         = []byte(" ") | ||||
| 	commaBytes         = []byte(",") | ||||
| 	pointerChainBytes  = []byte("->") | ||||
| 	nilAngleBytes      = []byte("<nil>") | ||||
| 	circularShortBytes = []byte("<shown>") | ||||
| 	invalidAngleBytes  = []byte("<invalid>") | ||||
| 	openBracketBytes   = []byte("[") | ||||
| 	closeBracketBytes  = []byte("]") | ||||
| 	percentBytes       = []byte("%") | ||||
| 	precisionBytes     = []byte(".") | ||||
| 	openAngleBytes     = []byte("<") | ||||
| 	closeAngleBytes    = []byte(">") | ||||
| 	openMapBytes       = []byte("{") | ||||
| 	closeMapBytes      = []byte("}") | ||||
| ) | ||||
|  | ||||
| type unwrap struct { | ||||
| 	val            interface{} | ||||
| 	s              fmt.State | ||||
| 	depth          int | ||||
| 	pointers       map[uintptr]int | ||||
| 	opts           *Options | ||||
| 	ignoreNextType bool | ||||
| } | ||||
|  | ||||
| type Options struct { | ||||
| 	Codec         codec.Codec | ||||
| 	Indent        string | ||||
| 	UnwrapMethods bool | ||||
| } | ||||
|  | ||||
| func NewOptions(opts ...Option) Options { | ||||
| 	options := Options{ | ||||
| 		Indent:        " ", | ||||
| 		UnwrapMethods: false, | ||||
| 	} | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
| 	return options | ||||
| } | ||||
|  | ||||
| type Option func(*Options) | ||||
|  | ||||
| func UnwrapIndent(f string) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.Indent = f | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func UnwrapMethods(b bool) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.UnwrapMethods = b | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func UnwrapCodec(c codec.Codec) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.Codec = c | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func Unwrap(val interface{}, opts ...Option) *unwrap { | ||||
| 	options := NewOptions(opts...) | ||||
| 	return &unwrap{val: val, opts: &options, pointers: make(map[uintptr]int)} | ||||
| } | ||||
|  | ||||
| func (f *unwrap) unpackValue(v reflect.Value) reflect.Value { | ||||
| 	if v.Kind() == reflect.Interface { | ||||
| 		f.ignoreNextType = false | ||||
| 		if !v.IsNil() { | ||||
| 			v = v.Elem() | ||||
| 		} | ||||
| 	} | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // formatPtr handles formatting of pointers by indirecting them as necessary. | ||||
| func (f *unwrap) formatPtr(v reflect.Value) { | ||||
| 	// Display nil if top level pointer is nil. | ||||
| 	showTypes := f.s.Flag('#') | ||||
| 	if v.IsNil() && (!showTypes || f.ignoreNextType) { | ||||
| 		_, _ = f.s.Write(nilAngleBytes) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Remove pointers at or below the current depth from map used to detect | ||||
| 	// circular refs. | ||||
| 	for k, depth := range f.pointers { | ||||
| 		if depth >= f.depth { | ||||
| 			delete(f.pointers, k) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Keep list of all dereferenced pointers to possibly show later. | ||||
| 	pointerChain := make([]uintptr, 0) | ||||
|  | ||||
| 	// Figure out how many levels of indirection there are by derferencing | ||||
| 	// pointers and unpacking interfaces down the chain while detecting circular | ||||
| 	// references. | ||||
| 	nilFound := false | ||||
| 	cycleFound := false | ||||
| 	indirects := 0 | ||||
| 	ve := v | ||||
| 	for ve.Kind() == reflect.Ptr { | ||||
| 		if ve.IsNil() { | ||||
| 			nilFound = true | ||||
| 			break | ||||
| 		} | ||||
| 		indirects++ | ||||
| 		addr := ve.Pointer() | ||||
| 		pointerChain = append(pointerChain, addr) | ||||
| 		if pd, ok := f.pointers[addr]; ok && pd < f.depth { | ||||
| 			cycleFound = true | ||||
| 			indirects-- | ||||
| 			break | ||||
| 		} | ||||
| 		f.pointers[addr] = f.depth | ||||
|  | ||||
| 		ve = ve.Elem() | ||||
| 		if ve.Kind() == reflect.Interface { | ||||
| 			if ve.IsNil() { | ||||
| 				nilFound = true | ||||
| 				break | ||||
| 			} | ||||
| 			ve = ve.Elem() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Display type or indirection level depending on flags. | ||||
| 	if showTypes && !f.ignoreNextType { | ||||
| 		if f.depth > 0 { | ||||
| 			_, _ = f.s.Write(openParenBytes) | ||||
| 		} | ||||
| 		if f.depth > 0 { | ||||
| 			_, _ = f.s.Write(bytes.Repeat(asteriskBytes, indirects)) | ||||
| 		} else { | ||||
| 			_, _ = f.s.Write(bytes.Repeat(ampBytes, indirects)) | ||||
| 		} | ||||
|  | ||||
| 		_, _ = f.s.Write([]byte(ve.Type().String())) | ||||
|  | ||||
| 		if f.depth > 0 { | ||||
| 			_, _ = f.s.Write(closeParenBytes) | ||||
| 		} | ||||
| 	} else { | ||||
| 		if nilFound || cycleFound { | ||||
| 			indirects += strings.Count(ve.Type().String(), "*") | ||||
| 		} | ||||
| 		_, _ = f.s.Write(openAngleBytes) | ||||
| 		_, _ = f.s.Write([]byte(strings.Repeat("*", indirects))) | ||||
| 		_, _ = f.s.Write(closeAngleBytes) | ||||
| 	} | ||||
|  | ||||
| 	// Display pointer information depending on flags. | ||||
| 	if f.s.Flag('+') && (len(pointerChain) > 0) { | ||||
| 		_, _ = f.s.Write(openParenBytes) | ||||
| 		for i, addr := range pointerChain { | ||||
| 			if i > 0 { | ||||
| 				_, _ = f.s.Write(pointerChainBytes) | ||||
| 			} | ||||
| 			getHexPtr(f.s, addr) | ||||
| 		} | ||||
| 		_, _ = f.s.Write(closeParenBytes) | ||||
| 	} | ||||
|  | ||||
| 	// Display dereferenced value. | ||||
| 	switch { | ||||
| 	case nilFound: | ||||
| 		_, _ = f.s.Write(nilAngleBytes) | ||||
|  | ||||
| 	case cycleFound: | ||||
| 		_, _ = f.s.Write(circularShortBytes) | ||||
|  | ||||
| 	default: | ||||
| 		f.ignoreNextType = true | ||||
| 		f.format(ve) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // format is the main workhorse for providing the Formatter interface.  It | ||||
| // uses the passed reflect value to figure out what kind of object we are | ||||
| // dealing with and formats it appropriately.  It is a recursive function, | ||||
| // however circular data structures are detected and handled properly. | ||||
| func (f *unwrap) format(v reflect.Value) { | ||||
| 	if f.opts.Codec != nil { | ||||
| 		buf, err := f.opts.Codec.Marshal(v.Interface()) | ||||
| 		if err != nil { | ||||
| 			_, _ = f.s.Write(invalidAngleBytes) | ||||
| 			return | ||||
| 		} | ||||
| 		_, _ = f.s.Write(buf) | ||||
| 		return | ||||
| 	} | ||||
| 	// Handle invalid reflect values immediately. | ||||
| 	kind := v.Kind() | ||||
| 	if kind == reflect.Invalid { | ||||
| 		_, _ = f.s.Write(invalidAngleBytes) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Handle pointers specially. | ||||
| 	if kind == reflect.Ptr { | ||||
| 		f.formatPtr(v) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// get type information unless already handled elsewhere. | ||||
| 	if !f.ignoreNextType && f.s.Flag('#') { | ||||
| 		if v.Type().Kind() != reflect.Map { | ||||
| 			_, _ = f.s.Write(openParenBytes) | ||||
| 		} | ||||
| 		_, _ = f.s.Write([]byte(v.Type().String())) | ||||
| 		if v.Type().Kind() != reflect.Map { | ||||
| 			_, _ = f.s.Write(closeParenBytes) | ||||
| 		} | ||||
| 	} | ||||
| 	f.ignoreNextType = false | ||||
|  | ||||
| 	// Call Stringer/error interfaces if they exist and the handle methods | ||||
| 	// flag is enabled. | ||||
| 	if !f.opts.UnwrapMethods { | ||||
| 		if (kind != reflect.Invalid) && (kind != reflect.Interface) { | ||||
| 			if handled := handleMethods(f.opts, f.s, v); handled { | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	switch kind { | ||||
| 	case reflect.Invalid: | ||||
| 		_, _ = f.s.Write(invalidAngleBytes) | ||||
| 	case reflect.Bool: | ||||
| 		getBool(f.s, v.Bool()) | ||||
| 	case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: | ||||
| 		getInt(f.s, v.Int(), 10) | ||||
| 	case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: | ||||
| 		getUint(f.s, v.Uint(), 10) | ||||
| 	case reflect.Float32: | ||||
| 		getFloat(f.s, v.Float(), 32) | ||||
| 	case reflect.Float64: | ||||
| 		getFloat(f.s, v.Float(), 64) | ||||
| 	case reflect.Complex64: | ||||
| 		getComplex(f.s, v.Complex(), 32) | ||||
| 	case reflect.Complex128: | ||||
| 		getComplex(f.s, v.Complex(), 64) | ||||
| 	case reflect.Slice: | ||||
| 		if v.IsNil() { | ||||
| 			_, _ = f.s.Write(nilAngleBytes) | ||||
| 			break | ||||
| 		} | ||||
| 		fallthrough | ||||
| 	case reflect.Array: | ||||
| 		_, _ = f.s.Write(openBracketBytes) | ||||
| 		f.depth++ | ||||
| 		numEntries := v.Len() | ||||
| 		for i := 0; i < numEntries; i++ { | ||||
| 			if i > 0 { | ||||
| 				_, _ = f.s.Write(spaceBytes) | ||||
| 			} | ||||
| 			f.ignoreNextType = true | ||||
| 			f.format(f.unpackValue(v.Index(i))) | ||||
| 		} | ||||
| 		f.depth-- | ||||
| 		_, _ = f.s.Write(closeBracketBytes) | ||||
| 	case reflect.String: | ||||
| 		_, _ = f.s.Write([]byte(`"` + v.String() + `"`)) | ||||
| 	case reflect.Interface: | ||||
| 		// The only time we should get here is for nil interfaces due to | ||||
| 		// unpackValue calls. | ||||
| 		if v.IsNil() { | ||||
| 			_, _ = f.s.Write(nilAngleBytes) | ||||
| 		} | ||||
| 	case reflect.Ptr: | ||||
| 		// Do nothing.  We should never get here since pointers have already | ||||
| 		// been handled above. | ||||
| 	case reflect.Map: | ||||
| 		// nil maps should be indicated as different than empty maps | ||||
| 		if v.IsNil() { | ||||
| 			_, _ = f.s.Write(nilAngleBytes) | ||||
| 			break | ||||
| 		} | ||||
| 		_, _ = f.s.Write(openMapBytes) | ||||
| 		f.depth++ | ||||
| 		keys := v.MapKeys() | ||||
| 		for i, key := range keys { | ||||
| 			if i > 0 { | ||||
| 				_, _ = f.s.Write(spaceBytes) | ||||
| 			} | ||||
| 			f.ignoreNextType = true | ||||
| 			f.format(f.unpackValue(key)) | ||||
| 			_, _ = f.s.Write(colonBytes) | ||||
| 			f.ignoreNextType = true | ||||
| 			f.format(f.unpackValue(v.MapIndex(key))) | ||||
| 		} | ||||
| 		f.depth-- | ||||
| 		_, _ = f.s.Write(closeMapBytes) | ||||
| 	case reflect.Struct: | ||||
| 		numFields := v.NumField() | ||||
| 		_, _ = f.s.Write(openBraceBytes) | ||||
| 		f.depth++ | ||||
| 		vt := v.Type() | ||||
| 		for i := 0; i < numFields; i++ { | ||||
| 			if i > 0 { | ||||
| 				_, _ = f.s.Write(commaBytes) | ||||
| 				_, _ = f.s.Write(spaceBytes) | ||||
| 			} | ||||
| 			vtf := vt.Field(i) | ||||
| 			if f.s.Flag('+') || f.s.Flag('#') { | ||||
| 				_, _ = f.s.Write([]byte(vtf.Name)) | ||||
| 				_, _ = f.s.Write(colonBytes) | ||||
| 			} | ||||
| 			f.format(f.unpackValue(v.Field(i))) | ||||
| 		} | ||||
| 		f.depth-- | ||||
| 		_, _ = f.s.Write(closeBraceBytes) | ||||
| 	case reflect.Uintptr: | ||||
| 		getHexPtr(f.s, uintptr(v.Uint())) | ||||
| 	case reflect.UnsafePointer, reflect.Chan, reflect.Func: | ||||
| 		getHexPtr(f.s, v.Pointer()) | ||||
| 	// There were not any other types at the time this code was written, but | ||||
| 	// fall back to letting the default fmt package handle it if any get added. | ||||
| 	default: | ||||
| 		format := f.buildDefaultFormat() | ||||
| 		if v.CanInterface() { | ||||
| 			_, _ = fmt.Fprintf(f.s, format, v.Interface()) | ||||
| 		} else { | ||||
| 			_, _ = fmt.Fprintf(f.s, format, v.String()) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (f *unwrap) Format(s fmt.State, verb rune) { | ||||
| 	f.s = s | ||||
|  | ||||
| 	// Use standard formatting for verbs that are not v. | ||||
| 	if verb != 'v' { | ||||
| 		format := f.constructOrigFormat(verb) | ||||
| 		_, _ = fmt.Fprintf(s, format, f.val) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if f.val == nil { | ||||
| 		if s.Flag('#') { | ||||
| 			_, _ = s.Write(interfaceBytes) | ||||
| 		} | ||||
| 		_, _ = s.Write(nilAngleBytes) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	f.format(reflect.ValueOf(f.val)) | ||||
| } | ||||
|  | ||||
| // handle special methods like error.Error() or fmt.Stringer interface | ||||
| func handleMethods(_ *Options, w io.Writer, v reflect.Value) (handled bool) { | ||||
| 	if !v.CanInterface() { | ||||
| 		// not our case | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	if !v.CanAddr() { | ||||
| 		// not our case | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	if v.CanAddr() { | ||||
| 		v = v.Addr() | ||||
| 	} | ||||
|  | ||||
| 	// Is it an error or Stringer? | ||||
| 	switch iface := v.Interface().(type) { | ||||
| 	case error: | ||||
| 		defer catchPanic(w, v) | ||||
| 		_, _ = w.Write([]byte(iface.Error())) | ||||
| 		return true | ||||
| 	case fmt.Stringer: | ||||
| 		defer catchPanic(w, v) | ||||
| 		_, _ = w.Write([]byte(iface.String())) | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // getBool outputs a boolean value as true or false to Writer w. | ||||
| func getBool(w io.Writer, val bool) { | ||||
| 	if val { | ||||
| 		_, _ = w.Write(trueBytes) | ||||
| 	} else { | ||||
| 		_, _ = w.Write(falseBytes) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getInt outputs a signed integer value to Writer w. | ||||
| func getInt(w io.Writer, val int64, base int) { | ||||
| 	_, _ = w.Write([]byte(strconv.FormatInt(val, base))) | ||||
| } | ||||
|  | ||||
| // getUint outputs an unsigned integer value to Writer w. | ||||
| func getUint(w io.Writer, val uint64, base int) { | ||||
| 	_, _ = w.Write([]byte(strconv.FormatUint(val, base))) | ||||
| } | ||||
|  | ||||
| // getFloat outputs a floating point value using the specified precision, | ||||
| // which is expected to be 32 or 64bit, to Writer w. | ||||
| func getFloat(w io.Writer, val float64, precision int) { | ||||
| 	_, _ = w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision))) | ||||
| } | ||||
|  | ||||
| // getComplex outputs a complex value using the specified float precision | ||||
| // for the real and imaginary parts to Writer w. | ||||
| func getComplex(w io.Writer, c complex128, floatPrecision int) { | ||||
| 	r := real(c) | ||||
| 	_, _ = w.Write(openParenBytes) | ||||
| 	_, _ = w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision))) | ||||
| 	i := imag(c) | ||||
| 	if i >= 0 { | ||||
| 		_, _ = w.Write(plusBytes) | ||||
| 	} | ||||
| 	_, _ = w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision))) | ||||
| 	_, _ = w.Write(iBytes) | ||||
| 	_, _ = w.Write(closeParenBytes) | ||||
| } | ||||
|  | ||||
| // getHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x' | ||||
| // prefix to Writer w. | ||||
| func getHexPtr(w io.Writer, p uintptr) { | ||||
| 	// Null pointer. | ||||
| 	num := uint64(p) | ||||
| 	if num == 0 { | ||||
| 		_, _ = w.Write(nilAngleBytes) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix | ||||
| 	buf := make([]byte, 18) | ||||
|  | ||||
| 	// It's simpler to construct the hex string right to left. | ||||
| 	base := uint64(16) | ||||
| 	i := len(buf) - 1 | ||||
| 	for num >= base { | ||||
| 		buf[i] = hexDigits[num%base] | ||||
| 		num /= base | ||||
| 		i-- | ||||
| 	} | ||||
| 	buf[i] = hexDigits[num] | ||||
|  | ||||
| 	// Add '0x' prefix. | ||||
| 	i-- | ||||
| 	buf[i] = 'x' | ||||
| 	i-- | ||||
| 	buf[i] = '0' | ||||
|  | ||||
| 	// Strip unused leading bytes. | ||||
| 	buf = buf[i:] | ||||
| 	_, _ = w.Write(buf) | ||||
| } | ||||
|  | ||||
| func catchPanic(w io.Writer, _ reflect.Value) { | ||||
| 	if err := recover(); err != nil { | ||||
| 		_, _ = w.Write(panicBytes) | ||||
| 		_, _ = fmt.Fprintf(w, "%v", err) | ||||
| 		_, _ = w.Write(closeParenBytes) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (f *unwrap) buildDefaultFormat() (format string) { | ||||
| 	buf := bytes.NewBuffer(percentBytes) | ||||
|  | ||||
| 	for _, flag := range sf { | ||||
| 		if f.s.Flag(int(flag)) { | ||||
| 			_, _ = buf.WriteRune(flag) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	_, _ = buf.WriteRune('v') | ||||
|  | ||||
| 	format = buf.String() | ||||
| 	return format | ||||
| } | ||||
|  | ||||
| func (f *unwrap) constructOrigFormat(verb rune) (format string) { | ||||
| 	buf := bytes.NewBuffer(percentBytes) | ||||
|  | ||||
| 	for _, flag := range sf { | ||||
| 		if f.s.Flag(int(flag)) { | ||||
| 			_, _ = buf.WriteRune(flag) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if width, ok := f.s.Width(); ok { | ||||
| 		_, _ = buf.WriteString(strconv.Itoa(width)) | ||||
| 	} | ||||
|  | ||||
| 	if precision, ok := f.s.Precision(); ok { | ||||
| 		_, _ = buf.Write(precisionBytes) | ||||
| 		_, _ = buf.WriteString(strconv.Itoa(precision)) | ||||
| 	} | ||||
|  | ||||
| 	_, _ = buf.WriteRune(verb) | ||||
|  | ||||
| 	format = buf.String() | ||||
| 	return format | ||||
| } | ||||
							
								
								
									
										46
									
								
								logger/unwrap/unwrap_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								logger/unwrap/unwrap_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| package unwrap | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"go.unistack.org/micro/v3/codec" | ||||
| ) | ||||
|  | ||||
| func TestUnwrap(t *testing.T) { | ||||
| 	string1 := "string1" | ||||
| 	string2 := "string2" | ||||
|  | ||||
| 	type val1 struct { | ||||
| 		mp  map[string]string | ||||
| 		val *val1 | ||||
| 		str *string | ||||
| 		ar  []*string | ||||
| 	} | ||||
|  | ||||
| 	v1 := &val1{ar: []*string{&string1, &string2}, str: &string1, val: &val1{str: &string2}, mp: map[string]string{"key": "val"}} | ||||
|  | ||||
| 	t.Logf("output: %#v", Unwrap(v1)) | ||||
|  | ||||
| 	type val2 struct { | ||||
| 		mp  map[string]string | ||||
| 		val *val2 | ||||
| 		str string | ||||
| 		ar  []string | ||||
| 	} | ||||
|  | ||||
| 	v2 := &val2{ar: []string{string1, string2}, str: string1, val: &val2{str: string2}, mp: map[string]string{"key": "val"}} | ||||
|  | ||||
| 	t.Logf("output: %#v", v2) | ||||
| } | ||||
|  | ||||
| func TestUnwrapCodec(t *testing.T) { | ||||
| 	type val struct { | ||||
| 		MP  map[string]string `json:"mp"` | ||||
| 		STR string            `json:"str"` | ||||
| 		AR  []string          `json:"ar"` | ||||
| 	} | ||||
|  | ||||
| 	v1 := &val{AR: []string{"string1", "string2"}, STR: "string", MP: map[string]string{"key": "val"}} | ||||
|  | ||||
| 	t.Logf("output: %#v", Unwrap(v1, UnwrapCodec(codec.NewCodec()))) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user