diff --git a/fieldpath/path.go b/fieldpath/path.go index a865ec4..68cdb1e 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 f4b00c2..dc61ff2 100644 --- a/fieldpath/serialize-pe.go +++ b/fieldpath/serialize-pe.go @@ -17,13 +17,12 @@ 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" "sigs.k8s.io/structured-merge-diff/v6/value" ) @@ -31,58 +30,39 @@ 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 -} - -// 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()) -} - // 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 +71,17 @@ 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.Sort() - return PathElement{Key: &fields}, iter.Error + var fields value.FieldList + if err := json.Unmarshal(b, &fields); err != nil { + return PathElement{}, err + } + return PathElement{Key: &fields}, nil case peIndexSepBytes[0]: i, err := strconv.Atoi(s[2:]) if err != nil { @@ -127,60 +95,47 @@ func DeserializePathElement(s string) (PathElement, error) { } } -var ( - readPool = jsoniter.NewIterator(jsoniter.ConfigCompatibleWithStandardLibrary).Pool() - writePool = jsoniter.NewStream(jsoniter.ConfigCompatibleWithStandardLibrary, nil, 1024).Pool() -) - // SerializePathElement serializes a path element func SerializePathElement(pe PathElement) (string, error) { - buf := strings.Builder{} - err := serializePathElementToWriter(&buf, pe) - return buf.String(), err + builder := bytes.Buffer{} + if err := serializePathElementBuilder(pe, &builder); err != nil { + return "", err + } + return builder.String(), nil } -func serializePathElementToWriter(w io.Writer, pe PathElement) error { - stream := writePool.BorrowStream(w) - defer writePool.ReturnStream(stream) +func serializePathElementBuilder(pe PathElement, builder *bytes.Buffer) error { switch { case pe.FieldName != nil: - if _, err := stream.Write(peFieldSepBytes); err != nil { + if _, err := builder.Write(peFieldSepBytes); err != nil { + return err + } + if _, err := builder.WriteString(*pe.FieldName); err != nil { return err } - stream.WriteRaw(*pe.FieldName) case pe.Key != nil: - if _, err := stream.Write(peKeySepBytes); err != nil { + if _, err := builder.Write(peKeySepBytes); err != nil { return err } - stream.WriteObjectStart() - - for i, field := range *pe.Key { - if i > 0 { - stream.WriteMore() - } - stream.WriteObjectField(field.Name) - writeJSONStream(field.Value, stream) + if err := json.MarshalWrite(builder, pe.Key, json.Deterministic(true)); err != nil { + return err } - stream.WriteObjectEnd() case pe.Value != nil: - if _, err := stream.Write(peValueSepBytes); err != nil { + if _, err := builder.Write(peValueSepBytes); err != nil { + return err + } + if err := json.MarshalWrite(builder, value.MarshalValue{Value: pe.Value}, json.Deterministic(true)); err != nil { return err } - writeJSONStream(*pe.Value, stream) case pe.Index != nil: - if _, err := stream.Write(peIndexSepBytes); err != nil { + if _, err := builder.Write(peIndexSepBytes); err != nil { + return err + } + if _, err := builder.WriteString(strconv.Itoa(*pe.Index)); err != nil { return err } - stream.WriteInt(*pe.Index) default: 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 + return nil } diff --git a/fieldpath/serialize.go b/fieldpath/serialize.go index b992b93..a1b8774 100644 --- a/fieldpath/serialize.go +++ b/fieldpath/serialize.go @@ -18,117 +18,92 @@ package fieldpath import ( "bytes" + "fmt" "io" - "unsafe" + "sort" + "sync" - jsoniter "github.com/json-iterator/go" + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" ) func (s *Set) ToJSON() ([]byte, error) { - buf := bytes.Buffer{} - err := s.ToJSONStream(&buf) - if err != nil { - return nil, err - } - return buf.Bytes(), nil + return json.Marshal((*setContentsV1)(s)) } func (s *Set) ToJSONStream(w io.Writer) error { - stream := writePool.BorrowStream(w) - defer writePool.ReturnStream(stream) + return json.MarshalWrite(w, (*setContentsV1)(s)) +} - var r reusableBuilder +var pool = sync.Pool{ + New: func() any { + return &bytes.Buffer{} + }, +} - stream.WriteObjectStart() - err := s.emitContentsV1(false, stream, &r) - if err != nil { +func writePathKey(enc *jsontext.Encoder, pe PathElement) error { + builder := pool.Get().(*bytes.Buffer) + defer func() { + builder.Reset() + pool.Put(builder) + }() + + if err := serializePathElementBuilder(pe, builder); err != nil { return err } - stream.WriteObjectEnd() - return stream.Flush() -} -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]) + if err := enc.WriteToken(jsontext.String(builder.String())); err != nil { + return err } return nil } -type reusableBuilder struct { - bytes.Buffer -} +type setContentsV1 Set -func (r *reusableBuilder) unsafeString() string { - b := r.Bytes() - return *(*string)(unsafe.Pointer(&b)) +func (s *setContentsV1) MarshalJSONTo(enc *jsontext.Encoder) error { + return s.emitContentsV1(false, enc) } -func (r *reusableBuilder) reset() *bytes.Buffer { - r.Reset() - return &r.Buffer -} - -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 *setContentsV1) 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 := (*setContentsV1)(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 +111,133 @@ 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 + } + if err := om.WriteValue(jsontext.Value("{}")); err != nil { return err } - stream.WriteObjectField(r.unsafeString()) - stream.WriteEmptyObject() + 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 := (*setContentsV1)(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 + } -// 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) + return nil +} - found, _ := readIterV1(iter) - if found == nil { - *s = Set{} +func (s *setContentsV1) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + found, _, err := s.readIterV1(dec) + if err != nil { + return err + } else if found == nil { + *(*Set)(s) = Set{} } else { - *s = *found + *(*Set)(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 (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() { + 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, isChildMember, err := s.readIterV1(parser) + if err != nil { + return nil, false, fmt.Errorf("parsing value as set: %v", err) } - grandchildren, childIsMember := readIterV1(iter) - if childIsMember { + + 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 +} + +// 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/go.mod b/go.mod index f5343b6..bb9f5b5 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 e120aad..70b2d04 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/fields.go b/value/fields.go index 042b048..13d1b54 100644 --- a/value/fields.go +++ b/value/fields.go @@ -17,8 +17,13 @@ limitations under the License. package value import ( + "fmt" + "io" "sort" "strings" + + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" ) // Field is an individual key-value pair. @@ -27,10 +32,123 @@ 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 +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 := valueMarshalJSONTo(enc, f.Value); err != nil { + return err + } + } + enc.WriteToken(jsontext.EndObject) + + 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 { diff --git a/value/reflectcache.go b/value/reflectcache.go index 3b4a402..97162af 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 c1a7c85..0d51374 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 140b990..f6128bb 100644 --- a/value/value.go +++ b/value/value.go @@ -17,21 +17,13 @@ limitations under the License. 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 +76,22 @@ 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()) + return json.Marshal(v.Unstructured(), json.Deterministic(true)) } // ToYAML marshals a value as YAML.