From 33dcd50a5d0da71a5dbced748dc636e2a352546d Mon Sep 17 00:00:00 2001 From: Masaaki Goshima Date: Mon, 4 May 2020 18:39:17 +0900 Subject: [PATCH] Support MarshalJSON/MarshalText --- encode.go | 20 ++++--- encode_compile.go | 17 ++++++ encode_opcode.go | 6 ++ encode_test.go | 20 +++++++ encode_vm.go | 26 +++++++++ json.go | 138 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 220 insertions(+), 7 deletions(-) diff --git a/encode.go b/encode.go index 6e4d203..b7696c5 100644 --- a/encode.go +++ b/encode.go @@ -45,9 +45,15 @@ func (m *opcodeMap) set(k uintptr, op *opcodeSet) { m.Store(k, op) } +type marshalText interface { + MarshalText() ([]byte, error) +} + var ( - encPool sync.Pool - cachedOpcode opcodeMap + encPool sync.Pool + cachedOpcode opcodeMap + marshalJSONType reflect.Type + marshalTextType reflect.Type ) func init() { @@ -60,6 +66,8 @@ func init() { }, } cachedOpcode = opcodeMap{} + marshalJSONType = reflect.TypeOf((*Marshaler)(nil)).Elem() + marshalTextType = reflect.TypeOf((*marshalText)(nil)).Elem() } // NewEncoder returns a new encoder that writes to w. @@ -137,9 +145,7 @@ func (e *Encoder) encodeForMarshal(v interface{}) ([]byte, error) { func (e *Encoder) encode(v interface{}) error { header := (*interfaceHeader)(unsafe.Pointer(&v)) typ := header.typ - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - } + typeptr := uintptr(unsafe.Pointer(typ)) if codeSet := cachedOpcode.get(typeptr); codeSet != nil { var code *opcode @@ -159,11 +165,11 @@ func (e *Encoder) encode(v interface{}) error { // to noescape trick for header.typ ( reflect.*rtype ) copiedType := (*rtype)(unsafe.Pointer(typeptr)) - codeIndent, err := e.compile(copiedType, true) + codeIndent, err := e.compileHead(copiedType, true) if err != nil { return err } - code, err := e.compile(copiedType, false) + code, err := e.compileHead(copiedType, false) if err != nil { return err } diff --git a/encode_compile.go b/encode_compile.go index b528c61..c3df340 100644 --- a/encode_compile.go +++ b/encode_compile.go @@ -9,7 +9,24 @@ import ( "golang.org/x/xerrors" ) +func (e *Encoder) compileHead(typ *rtype, withIndent bool) (*opcode, error) { + if typ.Implements(marshalJSONType) { + return newOpCode(opMarshalJSON, typ, e.indent, newEndOp(e.indent)), nil + } else if typ.Implements(marshalTextType) { + return newOpCode(opMarshalText, typ, e.indent, newEndOp(e.indent)), nil + } + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + return e.compile(typ, withIndent) +} + func (e *Encoder) compile(typ *rtype, withIndent bool) (*opcode, error) { + if typ.Implements(marshalJSONType) { + return newOpCode(opMarshalJSON, typ, e.indent, newEndOp(e.indent)), nil + } else if typ.Implements(marshalTextType) { + return newOpCode(opMarshalText, typ, e.indent, newEndOp(e.indent)), nil + } switch typ.Kind() { case reflect.Ptr: return e.compilePtr(typ, withIndent) diff --git a/encode_opcode.go b/encode_opcode.go index 0070724..341a100 100644 --- a/encode_opcode.go +++ b/encode_opcode.go @@ -27,6 +27,8 @@ const ( opBool opInterface opPtr + opMarshalJSON + opMarshalText opSliceHead opSliceElem @@ -292,6 +294,10 @@ func (t opType) String() string { return "INTERFACE" case opPtr: return "PTR" + case opMarshalJSON: + return "MARSHAL_JSON" + case opMarshalText: + return "MARSHAL_TEXT" case opSliceHead: return "SLICE_HEAD" diff --git a/encode_test.go b/encode_test.go index 03366f8..48e2b8e 100644 --- a/encode_test.go +++ b/encode_test.go @@ -2,6 +2,7 @@ package json_test import ( "testing" + "time" "github.com/goccy/go-json" ) @@ -206,6 +207,25 @@ func Test_Marshal(t *testing.T) { }) } +type marshalJSON struct{} + +func (*marshalJSON) MarshalJSON() ([]byte, error) { + return []byte(`1`), nil +} + +func Test_MarshalJSON(t *testing.T) { + t.Run("*struct", func(t *testing.T) { + bytes, err := json.Marshal(&marshalJSON{}) + assertErr(t, err) + assertEq(t, "MarshalJSON", "1", string(bytes)) + }) + t.Run("time", func(t *testing.T) { + bytes, err := json.Marshal(time.Time{}) + assertErr(t, err) + assertEq(t, "MarshalJSON", `"0001-01-01T00:00:00Z"`, string(bytes)) + }) +} + func Test_MarshalIndent(t *testing.T) { prefix := "-" indent := "\t" diff --git a/encode_vm.go b/encode_vm.go index 7739602..12a065f 100644 --- a/encode_vm.go +++ b/encode_vm.go @@ -74,6 +74,32 @@ func (e *Encoder) run(code *opcode) error { c.ptr = uintptr(header.ptr) c.beforeLastCode().next = code.next code = c + case opMarshalJSON: + ptr := code.ptr + v := *(*interface{})(unsafe.Pointer(&interfaceHeader{ + typ: code.typ, + ptr: unsafe.Pointer(ptr), + })) + bytes, err := v.(Marshaler).MarshalJSON() + if err != nil { + return err + } + e.encodeBytes(bytes) + code = code.next + code.ptr = ptr + case opMarshalText: + ptr := code.ptr + v := *(*interface{})(unsafe.Pointer(&interfaceHeader{ + typ: code.typ, + ptr: unsafe.Pointer(ptr), + })) + bytes, err := v.(marshalText).MarshalText() + if err != nil { + return err + } + e.encodeBytes(bytes) + code = code.next + code.ptr = ptr case opSliceHead: p := code.ptr headerCode := code.toSliceHeaderCode() diff --git a/json.go b/json.go index b341a81..1511c48 100644 --- a/json.go +++ b/json.go @@ -2,6 +2,141 @@ package json import "bytes" +// Marshaler is the interface implemented by types that +// can marshal themselves into valid JSON. +type Marshaler interface { + MarshalJSON() ([]byte, error) +} + +// Marshal returns the JSON encoding of v. +// +// Marshal traverses the value v recursively. +// If an encountered value implements the Marshaler interface +// and is not a nil pointer, Marshal calls its MarshalJSON method +// to produce JSON. If no MarshalJSON method is present but the +// value implements encoding.TextMarshaler instead, Marshal calls +// its MarshalText method and encodes the result as a JSON string. +// The nil pointer exception is not strictly necessary +// but mimics a similar, necessary exception in the behavior of +// UnmarshalJSON. +// +// Otherwise, Marshal uses the following type-dependent default encodings: +// +// Boolean values encode as JSON booleans. +// +// Floating point, integer, and Number values encode as JSON numbers. +// +// String values encode as JSON strings coerced to valid UTF-8, +// replacing invalid bytes with the Unicode replacement rune. +// The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e" +// to keep some browsers from misinterpreting JSON output as HTML. +// Ampersand "&" is also escaped to "\u0026" for the same reason. +// This escaping can be disabled using an Encoder that had SetEscapeHTML(false) +// called on it. +// +// Array and slice values encode as JSON arrays, except that +// []byte encodes as a base64-encoded string, and a nil slice +// encodes as the null JSON value. +// +// Struct values encode as JSON objects. +// Each exported struct field becomes a member of the object, using the +// field name as the object key, unless the field is omitted for one of the +// reasons given below. +// +// The encoding of each struct field can be customized by the format string +// stored under the "json" key in the struct field's tag. +// The format string gives the name of the field, possibly followed by a +// comma-separated list of options. The name may be empty in order to +// specify options without overriding the default field name. +// +// The "omitempty" option specifies that the field should be omitted +// from the encoding if the field has an empty value, defined as +// false, 0, a nil pointer, a nil interface value, and any empty array, +// slice, map, or string. +// +// As a special case, if the field tag is "-", the field is always omitted. +// Note that a field with name "-" can still be generated using the tag "-,". +// +// Examples of struct field tags and their meanings: +// +// // Field appears in JSON as key "myName". +// Field int `json:"myName"` +// +// // Field appears in JSON as key "myName" and +// // the field is omitted from the object if its value is empty, +// // as defined above. +// Field int `json:"myName,omitempty"` +// +// // Field appears in JSON as key "Field" (the default), but +// // the field is skipped if empty. +// // Note the leading comma. +// Field int `json:",omitempty"` +// +// // Field is ignored by this package. +// Field int `json:"-"` +// +// // Field appears in JSON as key "-". +// Field int `json:"-,"` +// +// The "string" option signals that a field is stored as JSON inside a +// JSON-encoded string. It applies only to fields of string, floating point, +// integer, or boolean types. This extra level of encoding is sometimes used +// when communicating with JavaScript programs: +// +// Int64String int64 `json:",string"` +// +// The key name will be used if it's a non-empty string consisting of +// only Unicode letters, digits, and ASCII punctuation except quotation +// marks, backslash, and comma. +// +// Anonymous struct fields are usually marshaled as if their inner exported fields +// were fields in the outer struct, subject to the usual Go visibility rules amended +// as described in the next paragraph. +// An anonymous struct field with a name given in its JSON tag is treated as +// having that name, rather than being anonymous. +// An anonymous struct field of interface type is treated the same as having +// that type as its name, rather than being anonymous. +// +// The Go visibility rules for struct fields are amended for JSON when +// deciding which field to marshal or unmarshal. If there are +// multiple fields at the same level, and that level is the least +// nested (and would therefore be the nesting level selected by the +// usual Go rules), the following extra rules apply: +// +// 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, +// even if there are multiple untagged fields that would otherwise conflict. +// +// 2) If there is exactly one field (tagged or not according to the first rule), that is selected. +// +// 3) Otherwise there are multiple fields, and all are ignored; no error occurs. +// +// Handling of anonymous struct fields is new in Go 1.1. +// Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of +// an anonymous struct field in both current and earlier versions, give the field +// a JSON tag of "-". +// +// Map values encode as JSON objects. The map's key type must either be a +// string, an integer type, or implement encoding.TextMarshaler. The map keys +// are sorted and used as JSON object keys by applying the following rules, +// subject to the UTF-8 coercion described for string values above: +// - string keys are used directly +// - encoding.TextMarshalers are marshaled +// - integer keys are converted to strings +// +// Pointer values encode as the value pointed to. +// A nil pointer encodes as the null JSON value. +// +// Interface values encode as the value contained in the interface. +// A nil interface value encodes as the null JSON value. +// +// Channel, complex, and function values cannot be encoded in JSON. +// Attempting to encode such a value causes Marshal to return +// an UnsupportedTypeError. +// +// JSON cannot represent cyclic data structures and Marshal does not +// handle them. Passing cyclic structures to Marshal will result in +// an infinite recursion. +// func Marshal(v interface{}) ([]byte, error) { var b *bytes.Buffer enc := NewEncoder(b) @@ -14,6 +149,9 @@ func Marshal(v interface{}) ([]byte, error) { return bytes, nil } +// MarshalIndent is like Marshal but applies Indent to format the output. +// Each JSON element in the output will begin on a new line beginning with prefix +// followed by one or more copies of indent according to the indentation nesting. func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { var b *bytes.Buffer enc := NewEncoder(b)