From 200d5ada15b97fd252ca75c9aac9050702e2222a Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Tue, 17 Jun 2025 19:50:14 +0200 Subject: [PATCH 1/5] migrate github.com/json-iterator/go to encoding/json/v2 --- fieldpath/path.go | 2 +- fieldpath/serialize-pe.go | 184 ++++++++++++++------------- fieldpath/serialize.go | 250 +++++++++++++++++++------------------ go.mod | 11 +- go.sum | 18 +-- value/reflectcache.go | 3 +- value/reflectcache_test.go | 11 -- value/value.go | 52 ++------ 8 files changed, 248 insertions(+), 283 deletions(-) diff --git a/fieldpath/path.go b/fieldpath/path.go index a865ec42..68cdb1ee 100644 --- a/fieldpath/path.go +++ b/fieldpath/path.go @@ -80,7 +80,7 @@ func (fp Path) Copy() Path { // MakePath constructs a Path. The parts may be PathElements, ints, strings. func MakePath(parts ...interface{}) (Path, error) { - var fp Path + fp := make(Path, 0, len(parts)) for _, p := range parts { switch t := p.(type) { case PathElement: diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index f4b00c2e..9ee36426 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -17,13 +17,15 @@ limitations under the License. package fieldpath import ( + "bytes" "errors" "fmt" "io" "strconv" "strings" - jsoniter "github.com/json-iterator/go" + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" "sigs.k8s.io/structured-merge-diff/v6/value" ) @@ -31,58 +33,85 @@ var ErrUnknownPathElementType = errors.New("unknown path element type") const ( // Field indicates that the content of this path element is a field's name - peField = "f" + peField byte = 'f' // Value indicates that the content of this path element is a field's value - peValue = "v" + peValue byte = 'v' // Index indicates that the content of this path element is an index in an array - peIndex = "i" + peIndex byte = 'i' // Key indicates that the content of this path element is a key value map - peKey = "k" + peKey byte = 'k' // Separator separates the type of a path element from the contents - peSeparator = ":" + peSeparator byte = ':' ) var ( - peFieldSepBytes = []byte(peField + peSeparator) - peValueSepBytes = []byte(peValue + peSeparator) - peIndexSepBytes = []byte(peIndex + peSeparator) - peKeySepBytes = []byte(peKey + peSeparator) - peSepBytes = []byte(peSeparator) + peFieldSepBytes = []byte{peField, peSeparator} + peValueSepBytes = []byte{peValue, peSeparator} + peIndexSepBytes = []byte{peIndex, peSeparator} + peKeySepBytes = []byte{peKey, peSeparator} ) -// readJSONIter reads a Value from a JSON iterator. -// DO NOT EXPORT -// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202 -func readJSONIter(iter *jsoniter.Iterator) (value.Value, error) { - v := iter.Read() - if iter.Error != nil && iter.Error != io.EOF { - return nil, iter.Error - } - return value.NewValueInterface(v), nil +// writeValueToEncoder writes a value to an Encoder. +func writeValueToEncoder(v value.Value, enc *jsontext.Encoder) error { + return json.MarshalEncode(enc, v.Unstructured(), json.Deterministic(true)) } -// writeJSONStream writes a value into a JSON stream. -// DO NOT EXPORT -// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202 -func writeJSONStream(v value.Value, stream *jsoniter.Stream) { - stream.WriteVal(v.Unstructured()) +// FieldListFromJSON is a helper function for reading a JSON document. +func fieldListFromJSON(input []byte) (value.FieldList, error) { + parser := jsontext.NewDecoder(bytes.NewBuffer(input)) + + if objStart, err := parser.ReadToken(); err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } else if objStart.Kind() != jsontext.BeginObject.Kind() { + return nil, fmt.Errorf("expected object") + } + + var fields value.FieldList + for { + if parser.PeekKind() == jsontext.EndObject.Kind() { + if _, err := parser.ReadToken(); err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } + break + } + + rawKey, err := parser.ReadToken() + if err == io.EOF { + return nil, fmt.Errorf("unexpected EOF") + } else if err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } + + k := rawKey.String() + + var v any + if err := json.UnmarshalDecode(parser, &v); err == io.EOF { + return nil, fmt.Errorf("unexpected EOF") + } else if err != nil { + return nil, fmt.Errorf("parsing JSON: %v", err) + } + + fields = append(fields, value.Field{Name: k, Value: value.NewValueInterface(v)}) + } + + return fields, nil } // DeserializePathElement parses a serialized path element func DeserializePathElement(s string) (PathElement, error) { b := []byte(s) if len(b) < 2 { - return PathElement{}, errors.New("key must be 2 characters long:") + return PathElement{}, errors.New("key must be 2 characters long") } - typeSep, b := b[:2], b[2:] - if typeSep[1] != peSepBytes[0] { + typeSep0, typeSep1, b := b[0], b[1], b[2:] + if typeSep1 != peSeparator { return PathElement{}, fmt.Errorf("missing colon: %v", s) } - switch typeSep[0] { + switch typeSep0 { case peFieldSepBytes[0]: // Slice s rather than convert b, to save on // allocations. @@ -91,29 +120,18 @@ func DeserializePathElement(s string) (PathElement, error) { FieldName: &str, }, nil case peValueSepBytes[0]: - iter := readPool.BorrowIterator(b) - defer readPool.ReturnIterator(iter) - v, err := readJSONIter(iter) + v, err := value.FromJSON(b) if err != nil { return PathElement{}, err } return PathElement{Value: &v}, nil case peKeySepBytes[0]: - iter := readPool.BorrowIterator(b) - defer readPool.ReturnIterator(iter) - fields := value.FieldList{} - - iter.ReadObjectCB(func(iter *jsoniter.Iterator, key string) bool { - v, err := readJSONIter(iter) - if err != nil { - iter.Error = err - return false - } - fields = append(fields, value.Field{Name: key, Value: v}) - return true - }) + fields, err := fieldListFromJSON(b) + if err != nil { + return PathElement{}, err + } fields.Sort() - return PathElement{Key: &fields}, iter.Error + return PathElement{Key: &fields}, nil case peIndexSepBytes[0]: i, err := strconv.Atoi(s[2:]) if err != nil { @@ -127,60 +145,58 @@ func DeserializePathElement(s string) (PathElement, error) { } } -var ( - readPool = jsoniter.NewIterator(jsoniter.ConfigCompatibleWithStandardLibrary).Pool() - writePool = jsoniter.NewStream(jsoniter.ConfigCompatibleWithStandardLibrary, nil, 1024).Pool() -) +type PathElementSerializer struct { + buffer bytes.Buffer + encoder jsontext.Encoder +} // SerializePathElement serializes a path element func SerializePathElement(pe PathElement) (string, error) { - buf := strings.Builder{} - err := serializePathElementToWriter(&buf, pe) - return buf.String(), err + byteVal, err := (&PathElementSerializer{}).serialize(pe) + return string(byteVal), err } -func serializePathElementToWriter(w io.Writer, pe PathElement) error { - stream := writePool.BorrowStream(w) - defer writePool.ReturnStream(stream) +func (pes *PathElementSerializer) serialize(pe PathElement) (string, error) { + pes.buffer.Reset() + switch { case pe.FieldName != nil: - if _, err := stream.Write(peFieldSepBytes); err != nil { - return err + if _, err := pes.buffer.Write(peFieldSepBytes); err != nil { + return "", err } - stream.WriteRaw(*pe.FieldName) + pes.buffer.WriteString(*pe.FieldName) case pe.Key != nil: - if _, err := stream.Write(peKeySepBytes); err != nil { - return err + if _, err := pes.buffer.Write(peKeySepBytes); err != nil { + return "", err } - stream.WriteObjectStart() - - for i, field := range *pe.Key { - if i > 0 { - stream.WriteMore() + pes.encoder.Reset(&pes.buffer) + pes.encoder.WriteToken(jsontext.BeginObject) + for _, f := range *pe.Key { + if err := pes.encoder.WriteToken(jsontext.String(f.Name)); err != nil { + return "", err + } + if err := writeValueToEncoder(f.Value, &pes.encoder); err != nil { + return "", err } - stream.WriteObjectField(field.Name) - writeJSONStream(field.Value, stream) } - stream.WriteObjectEnd() + pes.encoder.WriteToken(jsontext.EndObject) case pe.Value != nil: - if _, err := stream.Write(peValueSepBytes); err != nil { - return err + if _, err := pes.buffer.Write(peValueSepBytes); err != nil { + return "", err + } + pes.encoder.Reset(&pes.buffer) + if err := writeValueToEncoder(*pe.Value, &pes.encoder); err != nil { + return "", err } - writeJSONStream(*pe.Value, stream) case pe.Index != nil: - if _, err := stream.Write(peIndexSepBytes); err != nil { - return err + if _, err := pes.buffer.Write(peIndexSepBytes); err != nil { + return "", err } - stream.WriteInt(*pe.Index) + pes.buffer.WriteString(strconv.Itoa(*pe.Index)) default: - return errors.New("invalid PathElement") + return "", errors.New("invalid PathElement") } - b := stream.Buffer() - err := stream.Flush() - // Help jsoniter manage its buffers--without this, the next - // use of the stream is likely to require an allocation. Look - // at the jsoniter stream code to understand why. They were probably - // optimizing for folks using the buffer directly. - stream.SetBuffer(b[:0]) - return err + + // TODO: is there a way to not emit newlines + return strings.TrimSpace(pes.buffer.String()), nil } diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index b992b93c..d593fabe 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -18,117 +18,99 @@ package fieldpath import ( "bytes" + "fmt" "io" - "unsafe" + "sort" + "sync" + "unicode" - jsoniter "github.com/json-iterator/go" + "github.com/go-json-experiment/json/jsontext" ) func (s *Set) ToJSON() ([]byte, error) { buf := bytes.Buffer{} - err := s.ToJSONStream(&buf) - if err != nil { + enc := jsontext.Encoder{} + enc.Reset(&buf) + if err := s.emitContentsV1(false, &enc); err != nil { return nil, err } - return buf.Bytes(), nil + return bytes.TrimSpace(buf.Bytes()), nil } func (s *Set) ToJSONStream(w io.Writer) error { - stream := writePool.BorrowStream(w) - defer writePool.ReturnStream(stream) - - var r reusableBuilder - - stream.WriteObjectStart() - err := s.emitContentsV1(false, stream, &r) - if err != nil { + buf := bytes.Buffer{} + enc := jsontext.Encoder{} + enc.Reset(&buf) + if err := s.emitContentsV1(false, &enc); err != nil { return err } - stream.WriteObjectEnd() - return stream.Flush() + bufLen := len(bytes.TrimRightFunc(buf.Bytes(), unicode.IsSpace)) + buf.Truncate(bufLen) + _, err := buf.WriteTo(w) + return err } -func manageMemory(stream *jsoniter.Stream) error { - // Help jsoniter manage its buffers--without this, it does a bunch of - // alloctaions that are not necessary. They were probably optimizing - // for folks using the buffer directly. - b := stream.Buffer() - if len(b) > 4096 || cap(b)-len(b) < 2048 { - if err := stream.Flush(); err != nil { - return err - } - stream.SetBuffer(b[:0]) - } - return nil +var pool = sync.Pool{ + New: func() any { + return &PathElementSerializer{} + }, } -type reusableBuilder struct { - bytes.Buffer -} +func writePathKey(enc *jsontext.Encoder, pe PathElement) error { + pes := pool.Get().(*PathElementSerializer) + defer pool.Put(pes) -func (r *reusableBuilder) unsafeString() string { - b := r.Bytes() - return *(*string)(unsafe.Pointer(&b)) -} + key, err := pes.serialize(pe) + if err != nil { + return err + } -func (r *reusableBuilder) reset() *bytes.Buffer { - r.Reset() - return &r.Buffer + if err := enc.WriteToken(jsontext.String(key)); err != nil { + return err + } + return nil } -func (s *Set) emitContentsV1(includeSelf bool, stream *jsoniter.Stream, r *reusableBuilder) error { - mi, ci := 0, 0 - first := true - preWrite := func() { - if first { - first = false - return - } - stream.WriteMore() +func (s *Set) emitContentsV1(includeSelf bool, om *jsontext.Encoder) error { + if err := om.WriteToken(jsontext.BeginObject); err != nil { + return err } if includeSelf && !(len(s.Members.members) == 0 && len(s.Children.members) == 0) { - preWrite() - stream.WriteObjectField(".") - stream.WriteEmptyObject() + if err := om.WriteToken(jsontext.String(".")); err != nil { + return err + } + if err := om.WriteValue(jsontext.Value("{}")); err != nil { + return err + } } + mi, ci := 0, 0 for mi < len(s.Members.members) && ci < len(s.Children.members) { mpe := s.Members.members[mi] cpe := s.Children.members[ci].pathElement if c := mpe.Compare(cpe); c < 0 { - preWrite() - if err := serializePathElementToWriter(r.reset(), mpe); err != nil { + if err := writePathKey(om, mpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteEmptyObject() - mi++ - } else if c > 0 { - preWrite() - if err := serializePathElementToWriter(r.reset(), cpe); err != nil { + if err := om.WriteValue(jsontext.Value("{}")); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteObjectStart() - if err := s.Children.members[ci].set.emitContentsV1(false, stream, r); err != nil { - return err - } - stream.WriteObjectEnd() - ci++ + + mi++ } else { - preWrite() - if err := serializePathElementToWriter(r.reset(), cpe); err != nil { + if err := writePathKey(om, cpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteObjectStart() - if err := s.Children.members[ci].set.emitContentsV1(true, stream, r); err != nil { + if err := s.Children.members[ci].set.emitContentsV1(c == 0, om); err != nil { return err } - stream.WriteObjectEnd() - mi++ + + // If we also found a member with the same path, we skip this member. + if c == 0 { + mi++ + } ci++ } } @@ -136,103 +118,131 @@ func (s *Set) emitContentsV1(includeSelf bool, stream *jsoniter.Stream, r *reusa for mi < len(s.Members.members) { mpe := s.Members.members[mi] - preWrite() - if err := serializePathElementToWriter(r.reset(), mpe); err != nil { + if err := writePathKey(om, mpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteEmptyObject() + if err := om.WriteValue(jsontext.Value("{}")); err != nil { + return err + } + mi++ } for ci < len(s.Children.members) { cpe := s.Children.members[ci].pathElement - preWrite() - if err := serializePathElementToWriter(r.reset(), cpe); err != nil { + if err := writePathKey(om, cpe); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteObjectStart() - if err := s.Children.members[ci].set.emitContentsV1(false, stream, r); err != nil { + if err := s.Children.members[ci].set.emitContentsV1(false, om); err != nil { return err } - stream.WriteObjectEnd() + ci++ } - return manageMemory(stream) + if err := om.WriteToken(jsontext.EndObject); err != nil { + return err + } + + return nil } // FromJSON clears s and reads a JSON formatted set structure. func (s *Set) FromJSON(r io.Reader) error { - // The iterator pool is completely useless for memory management, grrr. - iter := jsoniter.Parse(jsoniter.ConfigCompatibleWithStandardLibrary, r, 4096) + parser := jsontext.NewDecoder(r) - found, _ := readIterV1(iter) - if found == nil { + found, _, err := readIterV1(parser) + if err != nil { + return err + } else if found == nil { *s = Set{} } else { *s = *found } - return iter.Error + return nil } // returns true if this subtree is also (or only) a member of parent; s is nil // if there are no further children. -func readIterV1(iter *jsoniter.Iterator) (children *Set, isMember bool) { - iter.ReadMapCB(func(iter *jsoniter.Iterator, key string) bool { - if key == "." { +func readIterV1(parser *jsontext.Decoder) (children *Set, isMember bool, err error) { + if objStart, err := parser.ReadToken(); err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } else if objStart.Kind() != jsontext.BeginObject.Kind() { + return nil, false, fmt.Errorf("expected object") + } + + for { + if parser.PeekKind() == jsontext.EndObject.Kind() { + if _, err := parser.ReadToken(); err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } + break + } + + rawKey, err := parser.ReadToken() + if err == io.EOF { + return nil, false, fmt.Errorf("unexpected EOF") + } else if err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } + + k := rawKey.String() + if k == "." { isMember = true - iter.Skip() - return true + if err := parser.SkipValue(); err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } + continue } - pe, err := DeserializePathElement(key) + pe, err := DeserializePathElement(k) if err == ErrUnknownPathElementType { // Ignore these-- a future version maybe knows what // they are. We drop these completely rather than try // to preserve things we don't understand. - iter.Skip() - return true + if err := parser.SkipValue(); err != nil { + return nil, false, fmt.Errorf("parsing JSON: %v", err) + } + continue } else if err != nil { - iter.ReportError("parsing key as path element", err.Error()) - iter.Skip() - return true + return nil, false, fmt.Errorf("parsing key as path element: %v", err) } - grandchildren, childIsMember := readIterV1(iter) - if childIsMember { + + grandChildren, isChildMember, err := readIterV1(parser) + if err != nil { + return nil, false, fmt.Errorf("parsing value as set: %v", err) + } + + if isChildMember { if children == nil { children = &Set{} } + + // Append the member to the members list, we will sort it later m := &children.Members.members - // Since we expect that most of the time these will have been - // serialized in the right order, we just verify that and append. - appendOK := len(*m) == 0 || (*m)[len(*m)-1].Less(pe) - if appendOK { - *m = append(*m, pe) - } else { - children.Members.Insert(pe) - } + *m = append(*m, pe) } - if grandchildren != nil { + + if grandChildren != nil { if children == nil { children = &Set{} } - // Since we expect that most of the time these will have been - // serialized in the right order, we just verify that and append. + + // Append the child to the children list, we will sort it later m := &children.Children.members - appendOK := len(*m) == 0 || (*m)[len(*m)-1].pathElement.Less(pe) - if appendOK { - *m = append(*m, setNode{pe, grandchildren}) - } else { - *children.Children.Descend(pe) = *grandchildren - } + *m = append(*m, setNode{pe, grandChildren}) } - return true - }) + } + + // Sort the members and children + if children != nil { + sort.Sort(children.Members.members) + sort.Sort(children.Children.members) + } + if children == nil { isMember = true } - return children, isMember + return children, isMember, nil } diff --git a/go.mod b/go.mod index f5343b69..bb9f5b5d 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,10 @@ module sigs.k8s.io/structured-merge-diff/v6 +go 1.24 + require ( + github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 github.com/google/go-cmp v0.5.9 - github.com/json-iterator/go v1.1.12 go.yaml.in/yaml/v2 v2.4.2 sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016 ) - -require ( - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect -) - -go 1.23 diff --git a/go.sum b/go.sum index e120aad1..70b2d04e 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,7 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk= +github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/value/reflectcache.go b/value/reflectcache.go index 3b4a402e..97162af5 100644 --- a/value/reflectcache.go +++ b/value/reflectcache.go @@ -18,7 +18,6 @@ package value import ( "bytes" - "encoding/json" "errors" "fmt" "io" @@ -26,6 +25,8 @@ import ( "sort" "sync" "sync/atomic" + + "encoding/json" ) // UnstructuredConverter defines how a type can be converted directly to unstructured. diff --git a/value/reflectcache_test.go b/value/reflectcache_test.go index c1a7c856..0d51374e 100644 --- a/value/reflectcache_test.go +++ b/value/reflectcache_test.go @@ -17,7 +17,6 @@ limitations under the License. package value import ( - "encoding/json" "fmt" "reflect" "testing" @@ -327,16 +326,6 @@ func TestUnmarshal(t *testing.T) { Want: map[string]interface{}{}, WantError: true, }, - { - JSON: `1.0`, - IntoType: reflect.TypeOf(json.Number("")), - Want: json.Number("1.0"), - }, - { - JSON: `1`, - IntoType: reflect.TypeOf(json.Number("")), - Want: json.Number("1"), - }, { JSON: `1.0`, IntoType: reflect.TypeOf(float64(0)), diff --git a/value/value.go b/value/value.go index 140b9903..2b38ab11 100644 --- a/value/value.go +++ b/value/value.go @@ -19,19 +19,12 @@ package value import ( "bytes" "fmt" - "io" "strings" - jsoniter "github.com/json-iterator/go" - + "github.com/go-json-experiment/json" yaml "go.yaml.in/yaml/v2" ) -var ( - readPool = jsoniter.NewIterator(jsoniter.ConfigCompatibleWithStandardLibrary).Pool() - writePool = jsoniter.NewStream(jsoniter.ConfigCompatibleWithStandardLibrary, nil, 1024).Pool() -) - // A Value corresponds to an 'atom' in the schema. It should return true // for at least one of the IsXXX methods below, or the value is // considered "invalid" @@ -84,48 +77,23 @@ type Value interface { // FromJSON is a helper function for reading a JSON document. func FromJSON(input []byte) (Value, error) { - return FromJSONFast(input) + var v any + if err := json.Unmarshal(input, &v); err != nil { + return nil, err + } + + return NewValueInterface(v), nil } // FromJSONFast is a helper function for reading a JSON document. func FromJSONFast(input []byte) (Value, error) { - iter := readPool.BorrowIterator(input) - defer readPool.ReturnIterator(iter) - return readJSONIter(iter) + return FromJSON(input) } // ToJSON is a helper function for producing a JSon document. func ToJSON(v Value) ([]byte, error) { - buf := bytes.Buffer{} - stream := writePool.BorrowStream(&buf) - defer writePool.ReturnStream(stream) - writeJSONStream(v, stream) - b := stream.Buffer() - err := stream.Flush() - // Help jsoniter manage its buffers--without this, the next - // use of the stream is likely to require an allocation. Look - // at the jsoniter stream code to understand why. They were probably - // optimizing for folks using the buffer directly. - stream.SetBuffer(b[:0]) - return buf.Bytes(), err -} - -// readJSONIter reads a Value from a JSON iterator. -// DO NOT EXPORT -// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202 -func readJSONIter(iter *jsoniter.Iterator) (Value, error) { - v := iter.Read() - if iter.Error != nil && iter.Error != io.EOF { - return nil, iter.Error - } - return NewValueInterface(v), nil -} - -// writeJSONStream writes a value into a JSON stream. -// DO NOT EXPORT -// TODO: eliminate this https://github.com/kubernetes-sigs/structured-merge-diff/issues/202 -func writeJSONStream(v Value, stream *jsoniter.Stream) { - stream.WriteVal(v.Unstructured()) + jsonBytes, err := json.Marshal(v.Unstructured(), json.Deterministic(true)) + return bytes.TrimSpace(jsonBytes), err } // ToYAML marshals a value as YAML. From 8e5001352fca3f84a858df0b089823ea44d114ca Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Sat, 14 Jun 2025 08:59:02 +0200 Subject: [PATCH 2/5] use MarshalJSONTo function Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/serialize-pe.go | 47 +++++++++------------------------------ fieldpath/serialize.go | 46 +++++++++++--------------------------- value/fields.go | 18 +++++++++++++++ value/value.go | 4 +--- 4 files changed, 43 insertions(+), 72 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index 9ee36426..14f3c236 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -55,11 +55,6 @@ var ( peKeySepBytes = []byte{peKey, peSeparator} ) -// writeValueToEncoder writes a value to an Encoder. -func writeValueToEncoder(v value.Value, enc *jsontext.Encoder) error { - return json.MarshalEncode(enc, v.Unstructured(), json.Deterministic(true)) -} - // FieldListFromJSON is a helper function for reading a JSON document. func fieldListFromJSON(input []byte) (value.FieldList, error) { parser := jsontext.NewDecoder(bytes.NewBuffer(input)) @@ -145,58 +140,38 @@ func DeserializePathElement(s string) (PathElement, error) { } } -type PathElementSerializer struct { - buffer bytes.Buffer - encoder jsontext.Encoder -} - // SerializePathElement serializes a path element func SerializePathElement(pe PathElement) (string, error) { - byteVal, err := (&PathElementSerializer{}).serialize(pe) - return string(byteVal), err -} - -func (pes *PathElementSerializer) serialize(pe PathElement) (string, error) { - pes.buffer.Reset() + builder := strings.Builder{} switch { case pe.FieldName != nil: - if _, err := pes.buffer.Write(peFieldSepBytes); err != nil { + if _, err := builder.Write(peFieldSepBytes); err != nil { return "", err } - pes.buffer.WriteString(*pe.FieldName) + builder.WriteString(*pe.FieldName) case pe.Key != nil: - if _, err := pes.buffer.Write(peKeySepBytes); err != nil { + if _, err := builder.Write(peKeySepBytes); err != nil { return "", err } - pes.encoder.Reset(&pes.buffer) - pes.encoder.WriteToken(jsontext.BeginObject) - for _, f := range *pe.Key { - if err := pes.encoder.WriteToken(jsontext.String(f.Name)); err != nil { - return "", err - } - if err := writeValueToEncoder(f.Value, &pes.encoder); err != nil { - return "", err - } + if err := json.MarshalWrite(&builder, *pe.Key, json.Deterministic(true)); err != nil { + return "", err } - pes.encoder.WriteToken(jsontext.EndObject) case pe.Value != nil: - if _, err := pes.buffer.Write(peValueSepBytes); err != nil { + if _, err := builder.Write(peValueSepBytes); err != nil { return "", err } - pes.encoder.Reset(&pes.buffer) - if err := writeValueToEncoder(*pe.Value, &pes.encoder); err != nil { + if err := json.MarshalWrite(&builder, (*pe.Value).Unstructured(), json.Deterministic(true)); err != nil { return "", err } case pe.Index != nil: - if _, err := pes.buffer.Write(peIndexSepBytes); err != nil { + if _, err := builder.Write(peIndexSepBytes); err != nil { return "", err } - pes.buffer.WriteString(strconv.Itoa(*pe.Index)) + builder.WriteString(strconv.Itoa(*pe.Index)) default: return "", errors.New("invalid PathElement") } - // TODO: is there a way to not emit newlines - return strings.TrimSpace(pes.buffer.String()), nil + return builder.String(), nil } diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index d593fabe..2ce18d54 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -17,50 +17,24 @@ limitations under the License. package fieldpath import ( - "bytes" "fmt" "io" "sort" - "sync" - "unicode" + "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" ) func (s *Set) ToJSON() ([]byte, error) { - buf := bytes.Buffer{} - enc := jsontext.Encoder{} - enc.Reset(&buf) - if err := s.emitContentsV1(false, &enc); err != nil { - return nil, err - } - return bytes.TrimSpace(buf.Bytes()), nil + return json.Marshal((*setContentsV1)(s)) } func (s *Set) ToJSONStream(w io.Writer) error { - buf := bytes.Buffer{} - enc := jsontext.Encoder{} - enc.Reset(&buf) - if err := s.emitContentsV1(false, &enc); err != nil { - return err - } - bufLen := len(bytes.TrimRightFunc(buf.Bytes(), unicode.IsSpace)) - buf.Truncate(bufLen) - _, err := buf.WriteTo(w) - return err -} - -var pool = sync.Pool{ - New: func() any { - return &PathElementSerializer{} - }, + return json.MarshalWrite(w, (*setContentsV1)(s)) } func writePathKey(enc *jsontext.Encoder, pe PathElement) error { - pes := pool.Get().(*PathElementSerializer) - defer pool.Put(pes) - - key, err := pes.serialize(pe) + key, err := SerializePathElement(pe) if err != nil { return err } @@ -71,7 +45,13 @@ func writePathKey(enc *jsontext.Encoder, pe PathElement) error { return nil } -func (s *Set) emitContentsV1(includeSelf bool, om *jsontext.Encoder) error { +type setContentsV1 Set + +func (s *setContentsV1) MarshalJSONTo(enc *jsontext.Encoder) error { + return s.emitContentsV1(false, enc) +} + +func (s *setContentsV1) emitContentsV1(includeSelf bool, om *jsontext.Encoder) error { if err := om.WriteToken(jsontext.BeginObject); err != nil { return err } @@ -103,7 +83,7 @@ func (s *Set) emitContentsV1(includeSelf bool, om *jsontext.Encoder) error { if err := writePathKey(om, cpe); err != nil { return err } - if err := s.Children.members[ci].set.emitContentsV1(c == 0, om); err != nil { + if err := (*setContentsV1)(s.Children.members[ci].set).emitContentsV1(c == 0, om); err != nil { return err } @@ -134,7 +114,7 @@ func (s *Set) emitContentsV1(includeSelf bool, om *jsontext.Encoder) error { if err := writePathKey(om, cpe); err != nil { return err } - if err := s.Children.members[ci].set.emitContentsV1(false, om); err != nil { + if err := (*setContentsV1)(s.Children.members[ci].set).emitContentsV1(false, om); err != nil { return err } diff --git a/value/fields.go b/value/fields.go index 042b0487..8a05566b 100644 --- a/value/fields.go +++ b/value/fields.go @@ -19,6 +19,9 @@ package value import ( "sort" "strings" + + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" ) // Field is an individual key-value pair. @@ -31,6 +34,21 @@ type Field struct { // have a different name. type FieldList []Field +func (fl FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { + enc.WriteToken(jsontext.BeginObject) + for _, f := range fl { + if err := enc.WriteToken(jsontext.String(f.Name)); err != nil { + return err + } + if err := json.MarshalEncode(enc, f.Value.Unstructured(), json.Deterministic(true)); err != nil { + return err + } + } + enc.WriteToken(jsontext.EndObject) + + return nil +} + // Copy returns a copy of the FieldList. // Values are not copied. func (f FieldList) Copy() FieldList { diff --git a/value/value.go b/value/value.go index 2b38ab11..f6128bbe 100644 --- a/value/value.go +++ b/value/value.go @@ -17,7 +17,6 @@ limitations under the License. package value import ( - "bytes" "fmt" "strings" @@ -92,8 +91,7 @@ func FromJSONFast(input []byte) (Value, error) { // ToJSON is a helper function for producing a JSon document. func ToJSON(v Value) ([]byte, error) { - jsonBytes, err := json.Marshal(v.Unstructured(), json.Deterministic(true)) - return bytes.TrimSpace(jsonBytes), err + return json.Marshal(v.Unstructured(), json.Deterministic(true)) } // ToYAML marshals a value as YAML. From e4c71fb4fa847bb3d2a6208e5beed1b674a5955d Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Sat, 14 Jun 2025 09:17:48 +0200 Subject: [PATCH 3/5] use UnmarshalJSONFrom function Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/serialize-pe.go | 49 ++------------------------------------- fieldpath/serialize.go | 20 +++++++++------- value/fields.go | 48 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 58 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index 14f3c236..e037b7d2 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -17,15 +17,12 @@ limitations under the License. package fieldpath import ( - "bytes" "errors" "fmt" - "io" "strconv" "strings" "github.com/go-json-experiment/json" - "github.com/go-json-experiment/json/jsontext" "sigs.k8s.io/structured-merge-diff/v6/value" ) @@ -55,47 +52,6 @@ var ( peKeySepBytes = []byte{peKey, peSeparator} ) -// FieldListFromJSON is a helper function for reading a JSON document. -func fieldListFromJSON(input []byte) (value.FieldList, error) { - parser := jsontext.NewDecoder(bytes.NewBuffer(input)) - - if objStart, err := parser.ReadToken(); err != nil { - return nil, fmt.Errorf("parsing JSON: %v", err) - } else if objStart.Kind() != jsontext.BeginObject.Kind() { - return nil, fmt.Errorf("expected object") - } - - var fields value.FieldList - for { - if parser.PeekKind() == jsontext.EndObject.Kind() { - if _, err := parser.ReadToken(); err != nil { - return nil, fmt.Errorf("parsing JSON: %v", err) - } - break - } - - rawKey, err := parser.ReadToken() - if err == io.EOF { - return nil, fmt.Errorf("unexpected EOF") - } else if err != nil { - return nil, fmt.Errorf("parsing JSON: %v", err) - } - - k := rawKey.String() - - var v any - if err := json.UnmarshalDecode(parser, &v); err == io.EOF { - return nil, fmt.Errorf("unexpected EOF") - } else if err != nil { - return nil, fmt.Errorf("parsing JSON: %v", err) - } - - fields = append(fields, value.Field{Name: k, Value: value.NewValueInterface(v)}) - } - - return fields, nil -} - // DeserializePathElement parses a serialized path element func DeserializePathElement(s string) (PathElement, error) { b := []byte(s) @@ -121,11 +77,10 @@ func DeserializePathElement(s string) (PathElement, error) { } return PathElement{Value: &v}, nil case peKeySepBytes[0]: - fields, err := fieldListFromJSON(b) - if err != nil { + var fields value.FieldList + if err := json.Unmarshal(b, &fields); err != nil { return PathElement{}, err } - fields.Sort() return PathElement{Key: &fields}, nil case peIndexSepBytes[0]: i, err := strconv.Atoi(s[2:]) diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index 2ce18d54..ac101276 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -128,24 +128,21 @@ func (s *setContentsV1) emitContentsV1(includeSelf bool, om *jsontext.Encoder) e return nil } -// FromJSON clears s and reads a JSON formatted set structure. -func (s *Set) FromJSON(r io.Reader) error { - parser := jsontext.NewDecoder(r) - - found, _, err := readIterV1(parser) +func (s *setContentsV1) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + found, _, err := s.readIterV1(dec) if err != nil { return err } else if found == nil { - *s = Set{} + *(*Set)(s) = Set{} } else { - *s = *found + *(*Set)(s) = *found } return nil } // returns true if this subtree is also (or only) a member of parent; s is nil // if there are no further children. -func readIterV1(parser *jsontext.Decoder) (children *Set, isMember bool, err error) { +func (s *setContentsV1) readIterV1(parser *jsontext.Decoder) (children *Set, isMember bool, err error) { if objStart, err := parser.ReadToken(); err != nil { return nil, false, fmt.Errorf("parsing JSON: %v", err) } else if objStart.Kind() != jsontext.BeginObject.Kind() { @@ -188,7 +185,7 @@ func readIterV1(parser *jsontext.Decoder) (children *Set, isMember bool, err err return nil, false, fmt.Errorf("parsing key as path element: %v", err) } - grandChildren, isChildMember, err := readIterV1(parser) + grandChildren, isChildMember, err := s.readIterV1(parser) if err != nil { return nil, false, fmt.Errorf("parsing value as set: %v", err) } @@ -226,3 +223,8 @@ func readIterV1(parser *jsontext.Decoder) (children *Set, isMember bool, err err return children, isMember, nil } + +// FromJSON clears s and reads a JSON formatted set structure. +func (s *Set) FromJSON(r io.Reader) error { + return json.UnmarshalRead(r, (*setContentsV1)(s)) +} diff --git a/value/fields.go b/value/fields.go index 8a05566b..e62c24cb 100644 --- a/value/fields.go +++ b/value/fields.go @@ -17,6 +17,8 @@ limitations under the License. package value import ( + "fmt" + "io" "sort" "strings" @@ -34,9 +36,9 @@ type Field struct { // have a different name. type FieldList []Field -func (fl FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { +func (fl *FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { enc.WriteToken(jsontext.BeginObject) - for _, f := range fl { + for _, f := range *fl { if err := enc.WriteToken(jsontext.String(f.Name)); err != nil { return err } @@ -49,6 +51,48 @@ func (fl FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { return nil } +// FieldListFromJSON is a helper function for reading a JSON document. +func (fl *FieldList) UnmarshalJSONFrom(parser *jsontext.Decoder) error { + if objStart, err := parser.ReadToken(); err != nil { + return fmt.Errorf("parsing JSON: %v", err) + } else if objStart.Kind() != jsontext.BeginObject.Kind() { + return fmt.Errorf("expected object") + } + + var fields FieldList + for { + if parser.PeekKind() == jsontext.EndObject.Kind() { + if _, err := parser.ReadToken(); err != nil { + return fmt.Errorf("parsing JSON: %v", err) + } + break + } + + rawKey, err := parser.ReadToken() + if err == io.EOF { + return fmt.Errorf("unexpected EOF") + } else if err != nil { + return fmt.Errorf("parsing JSON: %v", err) + } + + k := rawKey.String() + + var v any + if err := json.UnmarshalDecode(parser, &v); err == io.EOF { + return fmt.Errorf("unexpected EOF") + } else if err != nil { + return fmt.Errorf("parsing JSON: %v", err) + } + + fields = append(fields, Field{Name: k, Value: NewValueInterface(v)}) + } + + fields.Sort() + *fl = fields + + return nil +} + // Copy returns a copy of the FieldList. // Values are not copied. func (f FieldList) Copy() FieldList { From f2f24ea83ca976b3abc5dbdec8a104fc2169be64 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Sat, 14 Jun 2025 10:56:06 +0200 Subject: [PATCH 4/5] peformance tuning Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/serialize-pe.go | 39 ++++++++++++++++++++++++--------------- fieldpath/serialize.go | 19 ++++++++++++++++--- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index e037b7d2..1021acf3 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -17,10 +17,10 @@ limitations under the License. package fieldpath import ( + "bytes" "errors" "fmt" "strconv" - "strings" "github.com/go-json-experiment/json" "sigs.k8s.io/structured-merge-diff/v6/value" @@ -97,36 +97,45 @@ func DeserializePathElement(s string) (PathElement, error) { // SerializePathElement serializes a path element func SerializePathElement(pe PathElement) (string, error) { - builder := strings.Builder{} + builder := bytes.Buffer{} + if err := serializePathElementBuilder(pe, &builder); err != nil { + return "", err + } + return builder.String(), nil +} +func serializePathElementBuilder(pe PathElement, builder *bytes.Buffer) error { switch { case pe.FieldName != nil: if _, err := builder.Write(peFieldSepBytes); err != nil { - return "", err + return err + } + if _, err := builder.WriteString(*pe.FieldName); err != nil { + return err } - builder.WriteString(*pe.FieldName) case pe.Key != nil: if _, err := builder.Write(peKeySepBytes); err != nil { - return "", err + return err } - if err := json.MarshalWrite(&builder, *pe.Key, json.Deterministic(true)); err != nil { - return "", err + if err := json.MarshalWrite(builder, pe.Key, json.Deterministic(true)); err != nil { + return err } case pe.Value != nil: if _, err := builder.Write(peValueSepBytes); err != nil { - return "", err + return err } - if err := json.MarshalWrite(&builder, (*pe.Value).Unstructured(), json.Deterministic(true)); err != nil { - return "", err + if err := json.MarshalWrite(builder, (*pe.Value).Unstructured(), json.Deterministic(true)); err != nil { + return err } case pe.Index != nil: if _, err := builder.Write(peIndexSepBytes); err != nil { - return "", err + return err + } + if _, err := builder.WriteString(strconv.Itoa(*pe.Index)); err != nil { + return err } - builder.WriteString(strconv.Itoa(*pe.Index)) default: - return "", errors.New("invalid PathElement") + return errors.New("invalid PathElement") } - - return builder.String(), nil + return nil } diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index ac101276..a1b8774c 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -17,9 +17,11 @@ limitations under the License. package fieldpath import ( + "bytes" "fmt" "io" "sort" + "sync" "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" @@ -33,13 +35,24 @@ func (s *Set) ToJSONStream(w io.Writer) error { return json.MarshalWrite(w, (*setContentsV1)(s)) } +var pool = sync.Pool{ + New: func() any { + return &bytes.Buffer{} + }, +} + func writePathKey(enc *jsontext.Encoder, pe PathElement) error { - key, err := SerializePathElement(pe) - if err != nil { + builder := pool.Get().(*bytes.Buffer) + defer func() { + builder.Reset() + pool.Put(builder) + }() + + if err := serializePathElementBuilder(pe, builder); err != nil { return err } - if err := enc.WriteToken(jsontext.String(key)); err != nil { + if err := enc.WriteToken(jsontext.String(builder.String())); err != nil { return err } return nil From f90a164c304e010637632ae24ab0f05d66851259 Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:22:42 +0200 Subject: [PATCH 5/5] introduce MarshalValue Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- fieldpath/serialize-pe.go | 2 +- value/fields.go | 58 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/fieldpath/serialize-pe.go b/fieldpath/serialize-pe.go index 1021acf3..dc61ff29 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -124,7 +124,7 @@ func serializePathElementBuilder(pe PathElement, builder *bytes.Buffer) error { if _, err := builder.Write(peValueSepBytes); err != nil { return err } - if err := json.MarshalWrite(builder, (*pe.Value).Unstructured(), json.Deterministic(true)); err != nil { + if err := json.MarshalWrite(builder, value.MarshalValue{Value: pe.Value}, json.Deterministic(true)); err != nil { return err } case pe.Index != nil: diff --git a/value/fields.go b/value/fields.go index e62c24cb..13d1b546 100644 --- a/value/fields.go +++ b/value/fields.go @@ -32,6 +32,62 @@ type Field struct { Value Value } +type MarshalValue struct { + Value *Value +} + +func (mv MarshalValue) MarshalJSONTo(enc *jsontext.Encoder) error { + return valueMarshalJSONTo(enc, *mv.Value) +} + +func valueMarshalJSONTo(enc *jsontext.Encoder, v Value) error { + switch { + case v.IsNull(): + return enc.WriteToken(jsontext.Null) + case v.IsFloat(): + return enc.WriteToken(jsontext.Float(v.AsFloat())) + case v.IsInt(): + return enc.WriteToken(jsontext.Int(v.AsInt())) + case v.IsString(): + return enc.WriteToken(jsontext.String(v.AsString())) + case v.IsBool(): + return enc.WriteToken(jsontext.Bool(v.AsBool())) + case v.IsList(): + if err := enc.WriteToken(jsontext.BeginArray); err != nil { + return err + } + list := v.AsList() + for i := 0; i < list.Length(); i++ { + if err := valueMarshalJSONTo(enc, list.At(i)); err != nil { + return err + } + } + return enc.WriteToken(jsontext.EndArray) + case v.IsMap(): + if err := enc.WriteToken(jsontext.BeginObject); err != nil { + return err + } + var iterErr error + v.AsMap().Iterate(func(k string, v Value) bool { + if err := enc.WriteToken(jsontext.String(k)); err != nil { + iterErr = err + return false + } + if err := valueMarshalJSONTo(enc, v); err != nil { + iterErr = err + return false + } + return true + }) + if iterErr != nil { + return iterErr + } + return enc.WriteToken(jsontext.EndObject) + default: + return json.MarshalEncode(enc, v.Unstructured(), json.Deterministic(true)) + } +} + // FieldList is a list of key-value pairs. Each field is expected to // have a different name. type FieldList []Field @@ -42,7 +98,7 @@ func (fl *FieldList) MarshalJSONTo(enc *jsontext.Encoder) error { if err := enc.WriteToken(jsontext.String(f.Name)); err != nil { return err } - if err := json.MarshalEncode(enc, f.Value.Unstructured(), json.Deterministic(true)); err != nil { + if err := valueMarshalJSONTo(enc, f.Value); err != nil { return err } }