chestnut/encoding/json/encoders/lookup/encoder.go

115 lines
3.8 KiB
Go

package lookup
import (
"errors"
"unsafe"
"github.com/jrapoport/chestnut/encoding/json/encoders"
"github.com/jrapoport/chestnut/log"
jsoniter "github.com/json-iterator/go"
"github.com/modern-go/reflect2"
)
var cleanEncoder = encoders.NewEncoder()
// Encoder is a ValEncoder that encodes the data to lookup table and encodes a
// entry key for the data into the stream that can be read later by the decoder.
type Encoder struct {
token string
stream *jsoniter.Stream
valType reflect2.Type
encoder jsoniter.ValEncoder
log log.Logger
}
// NewLookupEncoder returns an encoder that builds a lookup table. It will strip out tagged
// struct fields and collect the encoded values in the provided stream as a map. As it strips
// out values, it replaces them with a token key for the lookup table. Later we can use this
// key as a lookup to reconstruct the encoded struct as it is decoded. The hash encoder must
// be run before this encoder, so the struct fields are hashed before they are stripped.
func NewLookupEncoder(ctx *Context, typ reflect2.Type, encoder jsoniter.ValEncoder) jsoniter.ValEncoder {
logger := log.Log
if encoder == nil {
logger.Panic(errors.New("value encoder required"))
return nil
}
if typ == nil {
logger.Panic(errors.New("encoder type required"))
return nil
}
if ctx == nil {
logger.Panic(errors.New("lookup context required"))
return nil
}
if ctx.Token == InvalidToken {
logger.Panic(errors.New("lookup token required"))
return nil
}
if ctx.Stream == nil {
logger.Panic(errors.New("lookup stream required"))
return nil
}
return &Encoder{
token: ctx.Token,
stream: ctx.Stream,
valType: typ,
encoder: encoder,
log: logger,
}
}
// SetLogger changes the logger for the encoder.
func (e *Encoder) SetLogger(l log.Logger) {
e.log = l
}
// Encode writes the value of ptr to stream.
func (e *Encoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) {
e.log.Debugf("encoding type %s", e.valType)
// FIXME: I've looked around for a way to avoid this, or unwrap the encoder, but it's
// not clear what the best way to do that is or if it's possible with jsoniter as-is.
// NOTE: This is *SUPER important*. This is so when UpdateStructDescriptor is called
// recursively for nested structs the ValEncoder we use is a ORIGINAL ValEncoder, and
// NOT a copy of our modified Encode (that strips out values). If we don't do this,
// tagged fields will also be stripped out of our steam and not just the encoded stream.
// We know this is happening because when it does: encoding stream == lookup stream.
if stream == e.stream {
// we are being called recursively so try and get a clean encoder.
if subEncoder := cleanEncoder.EncoderOf(e.valType); subEncoder != nil {
e.log.Debugf("use sub-encoder type %s", e.valType)
// use the clean encoder to encode to our own stream.
subEncoder.Encode(ptr, stream)
}
return
}
// encode the ptr to the lookup table
key := e.encodeLookup(ptr, e.nextIndex())
// encode our lookup key to the main stream
e.log.Debugf("encoded lookup key: %s", key)
stream.WriteString(key.String())
}
// IsEmpty returns true is ptr is empty, otherwise false.
func (e *Encoder) IsEmpty(ptr unsafe.Pointer) bool {
return e.encoder.IsEmpty(ptr)
}
func (e *Encoder) encodeLookup(ptr unsafe.Pointer, tableIndex int) Key {
key := NewLookupKey(e.token, tableIndex, e.valType)
// encode the actual data into our lookup table
if tableIndex > 0 {
e.stream.WriteMore()
}
e.stream.WriteObjectField(key.String())
e.encoder.Encode(ptr, e.stream)
e.log.Debugf("encoded lookup for key %s: %s", string(e.stream.Buffer()), key)
return key
}
// we shouldn't need locking here since it should not to be called concurrently.
func (e *Encoder) nextIndex() int {
idx, _ := e.stream.Attachment.(int)
e.stream.Attachment = idx + 1
return idx
}