From 544e2dc9327bc4fd63db48ce5677e31c4f4cb092 Mon Sep 17 00:00:00 2001 From: Robert Jandow Date: Mon, 23 Feb 2026 23:18:17 +0100 Subject: [PATCH 1/3] Optimize JSON comparison and patch creation by reducing overhead and improving type handling --- apply.go | 8 ++++---- create.go | 25 ++++++++++++++++++++++-- patch.go | 36 ++++++++++++++++++++-------------- pointer.go | 57 ++++++++++++++++++++++++++++++++++++++++-------------- 4 files changed, 90 insertions(+), 36 deletions(-) diff --git a/apply.go b/apply.go index a7f4406..0626e84 100644 --- a/apply.go +++ b/apply.go @@ -227,15 +227,15 @@ func applyTest(doc interface{}, op Operation) (interface{}, error) { } // jsonEqual compares two JSON-compatible values for equality per RFC 6902 Section 4.6. +// All callers are expected to pass values already produced by encoding/json +// (i.e., numbers are float64, maps are map[string]interface{}, etc.). func jsonEqual(a, b interface{}) bool { - // Normalize through JSON round-trip to ensure consistent types - na := normalizeJSON(a) - nb := normalizeJSON(b) - return reflect.DeepEqual(na, nb) + return reflect.DeepEqual(a, b) } // normalizeJSON normalizes a value by round-tripping through JSON serialization. // This ensures consistent types (e.g., all numbers become float64). +// Used by CreatePatchFromValues to normalize caller-supplied values. func normalizeJSON(v interface{}) interface{} { b, err := json.Marshal(v) if err != nil { diff --git a/create.go b/create.go index a8703b5..6566777 100644 --- a/create.go +++ b/create.go @@ -35,8 +35,29 @@ func CreatePatchFromValues(original, modified interface{}) Patch { // diff recursively computes the differences between two JSON values // and appends the corresponding operations to the patch. func diff(patch *Patch, path string, original, modified interface{}) { - if jsonEqual(original, modified) { - return + // Fast path for primitives — avoids reflect.DeepEqual overhead. + switch o := original.(type) { + case nil: + if modified == nil { + return + } + case bool: + if m, ok := modified.(bool); ok && o == m { + return + } + case float64: + if m, ok := modified.(float64); ok && o == m { + return + } + case string: + if m, ok := modified.(string); ok && o == m { + return + } + default: + // Composite types — fall through to structural comparison + if jsonEqual(original, modified) { + return + } } origObj, origIsObj := original.(map[string]interface{}) diff --git a/patch.go b/patch.go index bc48042..8b2f96c 100644 --- a/patch.go +++ b/patch.go @@ -78,13 +78,21 @@ func (o *Operation) UnmarshalJSON(data []byte) error { return err } + // rejectNull returns an error if the raw JSON value is "null". + // This prevents json.Unmarshal from silently accepting null into a string. + rejectNull := func(raw json.RawMessage, field string) error { + if string(raw) == "null" { + return fmt.Errorf("invalid %q field: must be a string", field) + } + return nil + } + if opRaw, ok := raw["op"]; ok { - var opValue interface{} - if err := json.Unmarshal(opRaw, &opValue); err != nil { - return fmt.Errorf("invalid \"op\" field: %w", err) + if err := rejectNull(opRaw, "op"); err != nil { + return err } - op, ok := opValue.(string) - if !ok { + var op string + if err := json.Unmarshal(opRaw, &op); err != nil { return fmt.Errorf("invalid \"op\" field: must be a string") } o.Op = OpType(op) @@ -92,12 +100,11 @@ func (o *Operation) UnmarshalJSON(data []byte) error { if pathRaw, ok := raw["path"]; ok { o.hasPath = true - var pathValue interface{} - if err := json.Unmarshal(pathRaw, &pathValue); err != nil { - return fmt.Errorf("invalid \"path\" field: %w", err) + if err := rejectNull(pathRaw, "path"); err != nil { + return err } - path, ok := pathValue.(string) - if !ok { + var path string + if err := json.Unmarshal(pathRaw, &path); err != nil { return fmt.Errorf("invalid \"path\" field: must be a string") } o.Path = path @@ -105,12 +112,11 @@ func (o *Operation) UnmarshalJSON(data []byte) error { if fromRaw, ok := raw["from"]; ok { o.hasFrom = true - var fromValue interface{} - if err := json.Unmarshal(fromRaw, &fromValue); err != nil { - return fmt.Errorf("invalid \"from\" field: %w", err) + if err := rejectNull(fromRaw, "from"); err != nil { + return err } - from, ok := fromValue.(string) - if !ok { + var from string + if err := json.Unmarshal(fromRaw, &from); err != nil { return fmt.Errorf("invalid \"from\" field: must be a string") } o.From = from diff --git a/pointer.go b/pointer.go index 745b598..2d57d06 100644 --- a/pointer.go +++ b/pointer.go @@ -1,7 +1,6 @@ package jsonpatch import ( - "encoding/json" "fmt" "strconv" "strings" @@ -39,7 +38,13 @@ func (p Pointer) String() string { if len(p.tokens) == 0 { return "" } + // Pre-estimate capacity to avoid reallocations. + size := 0 + for _, token := range p.tokens { + size += 1 + len(token) // "/" + token (conservative; escaping may add chars) + } var sb strings.Builder + sb.Grow(size) for _, token := range p.tokens { sb.WriteByte('/') sb.WriteString(escapePointerToken(token)) @@ -120,9 +125,10 @@ func (p Pointer) Set(doc interface{}, value interface{}) (interface{}, error) { return value, nil } - parent, err := p.Parent().Evaluate(doc) + parentPtr := p.Parent() + parent, err := parentPtr.Evaluate(doc) if err != nil { - return nil, fmt.Errorf("parent path %q does not exist: %w", p.Parent().String(), err) + return nil, fmt.Errorf("parent path %q does not exist: %w", parentPtr.String(), err) } key := p.Last() @@ -135,7 +141,7 @@ func (p Pointer) Set(doc interface{}, value interface{}) (interface{}, error) { if key == "-" { // Append to the end of the array newArr := append(node, value) - return p.Parent().replaceValue(doc, newArr) + return parentPtr.replaceValue(doc, newArr) } idx, err := resolveArrayIndex(key, len(node)+1) // +1 because we can insert at the end if err != nil { @@ -149,7 +155,7 @@ func (p Pointer) Set(doc interface{}, value interface{}) (interface{}, error) { copy(newArr[:idx], node[:idx]) newArr[idx] = value copy(newArr[idx+1:], node[idx:]) - return p.Parent().replaceValue(doc, newArr) + return parentPtr.replaceValue(doc, newArr) default: return nil, fmt.Errorf("cannot set value in %T", parent) } @@ -162,9 +168,10 @@ func (p Pointer) Remove(doc interface{}) (interface{}, error) { return nil, fmt.Errorf("cannot remove root document") } - parent, err := p.Parent().Evaluate(doc) + parentPtr := p.Parent() + parent, err := parentPtr.Evaluate(doc) if err != nil { - return nil, fmt.Errorf("parent path %q does not exist: %w", p.Parent().String(), err) + return nil, fmt.Errorf("parent path %q does not exist: %w", parentPtr.String(), err) } key := p.Last() @@ -184,10 +191,10 @@ func (p Pointer) Remove(doc interface{}) (interface{}, error) { if idx >= len(node) { return nil, fmt.Errorf("index %d out of bounds for array of length %d", idx, len(node)) } - newArr := make([]interface{}, 0, len(node)-1) - newArr = append(newArr, node[:idx]...) - newArr = append(newArr, node[idx+1:]...) - return p.Parent().replaceValue(doc, newArr) + newArr := make([]interface{}, len(node)-1) + copy(newArr, node[:idx]) + copy(newArr[idx:], node[idx+1:]) + return parentPtr.replaceValue(doc, newArr) default: return nil, fmt.Errorf("cannot remove value from %T", parent) } @@ -267,6 +274,9 @@ func validatePointerToken(raw string) error { // escapePointerToken encodes a token for use in a JSON Pointer string. // Per RFC 6901: ~ is escaped as ~0, / is escaped as ~1. func escapePointerToken(token string) string { + if !strings.ContainsAny(token, "~/") { + return token + } token = strings.ReplaceAll(token, "~", "~0") token = strings.ReplaceAll(token, "/", "~1") return token @@ -276,15 +286,32 @@ func escapePointerToken(token string) string { // Per RFC 6901: ~1 is unescaped to /, ~0 is unescaped to ~. // Order matters: ~1 must be processed before ~0. func unescapePointerToken(token string) string { + if !strings.Contains(token, "~") { + return token + } token = strings.ReplaceAll(token, "~1", "/") token = strings.ReplaceAll(token, "~0", "~") return token } // deepCopy creates a deep copy of a JSON-compatible value. +// It recursively copies maps and slices; primitives (string, float64, bool, nil) +// are immutable and returned as-is. func deepCopy(v interface{}) interface{} { - b, _ := json.Marshal(v) - var out interface{} - _ = json.Unmarshal(b, &out) - return out + switch val := v.(type) { + case map[string]interface{}: + m := make(map[string]interface{}, len(val)) + for k, v := range val { + m[k] = deepCopy(v) + } + return m + case []interface{}: + a := make([]interface{}, len(val)) + for i, v := range val { + a[i] = deepCopy(v) + } + return a + default: + return v + } } From b5bc8c9291ebd6de6c0c2591a4476b275390d1e9 Mon Sep 17 00:00:00 2001 From: Robert Jandow Date: Tue, 24 Feb 2026 11:48:53 +0100 Subject: [PATCH 2/3] Update to Go 1.24 --- .github/workflows/compile_examples.yml | 2 +- .github/workflows/test_go.yml | 2 +- go.mod | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/compile_examples.yml b/.github/workflows/compile_examples.yml index cb464e6..186c9fa 100644 --- a/.github/workflows/compile_examples.yml +++ b/.github/workflows/compile_examples.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v6 with: - go-version: 1.23 + go-version: 1.24 cache-dependency-path: "**/*.sum" - name: Install dependencies run: go mod download diff --git a/.github/workflows/test_go.yml b/.github/workflows/test_go.yml index 229f6c4..a0e575a 100644 --- a/.github/workflows/test_go.yml +++ b/.github/workflows/test_go.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v6 with: - go-version: 1.23 + go-version: 1.24 cache-dependency-path: "**/*.sum" - name: Install dependencies run: go mod download diff --git a/go.mod b/go.mod index 45c398a..1904892 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/robertjndw/go-json-patch -go 1.23.0 +go 1.24.0 From ae91449e096baed8fd31f6a33941344555282616 Mon Sep 17 00:00:00 2001 From: Robert Jandow Date: Tue, 24 Feb 2026 11:49:10 +0100 Subject: [PATCH 3/3] Add performance benchmarks --- README.md | 48 ++++- benchmark_test.go | 540 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 587 insertions(+), 1 deletion(-) create mode 100644 benchmark_test.go diff --git a/README.md b/README.md index e13cb59..353d053 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,52 @@ This library aims for strict RFC 6902 compliance including: - Deep equality comparison for the `test` operation per Section 4.6 - Ignoring unrecognized members in operation objects per Section 4 -## License +## Performance & Benchmarking + +The library includes comprehensive benchmarks covering all major operations at various scales. Contributors can use these to verify performance improvements or regressions. + +### Running Benchmarks + +Run all benchmarks with memory allocation stats: + +```bash +go test -bench=. -benchmem +``` + +### Comparing Performance Changes + +To compare performance before and after a change, use [`benchstat`](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat): +1. Install benchstat: + ```bash + go install golang.org/x/perf/cmd/benchstat@latest + ``` + +2. Collect baseline benchmarks: + ```bash + go test -bench=. -benchmem -count=5 > old.txt + ``` + +3. Make your changes and collect new benchmarks: + ```bash + go test -bench=. -benchmem -count=5 > new.txt + ``` + +4. Compare results: + ```bash + benchstat old.txt new.txt + ``` + +This will show you the performance delta for each benchmark function, including ns/op (time), B/op (memory), and allocs/op changes. + +### Benchmark Coverage + +The benchmarks include: +- **Apply operations**: Single ops, multi-op sequences, large documents, large patches, nested structures, array operations +- **CreatePatch**: Small and large objects, identical documents, arrays, deep nesting, round-trip scenarios +- **Serialization**: DecodePatch, MarshalPatch +- **Pointer operations**: Parsing, evaluation, modification +- **End-to-end**: Realistic complex object scenarios + +## License MIT diff --git a/benchmark_test.go b/benchmark_test.go new file mode 100644 index 0000000..f2f8626 --- /dev/null +++ b/benchmark_test.go @@ -0,0 +1,540 @@ +package jsonpatch + +import ( + "encoding/json" + "fmt" + "strconv" + "testing" +) + +// --------------------------------------------------------------------------- +// Helpers – reusable test fixtures +// --------------------------------------------------------------------------- + +// makeObject builds a flat JSON object with n string keys. +// +// {"key_0":"val_0", "key_1":"val_1", …} +func makeObject(n int) []byte { + m := make(map[string]string, n) + for i := range n { + m["key_"+strconv.Itoa(i)] = "val_" + strconv.Itoa(i) + } + b, _ := json.Marshal(m) + return b +} + +// makeArray builds a JSON array with n integer elements. +// +// [0, 1, 2, …] +func makeArray(n int) []byte { + a := make([]int, n) + for i := range n { + a[i] = i + } + b, _ := json.Marshal(a) + return b +} + +// makeNestedObject builds a deeply-nested JSON object. +// +// {"a":{"a":{"a":… "leaf" …}}} +func makeNestedObject(depth int) []byte { + var inner interface{} = "leaf" + for range depth { + inner = map[string]interface{}{"a": inner} + } + b, _ := json.Marshal(inner) + return b +} + +// modifyObject changes roughly half the keys in the object. +func modifyObject(original []byte) []byte { + var m map[string]interface{} + _ = json.Unmarshal(original, &m) + i := 0 + for k := range m { + if i%2 == 0 { + m[k] = "changed" + } + i++ + } + b, _ := json.Marshal(m) + return b +} + +// --------------------------------------------------------------------------- +// Benchmark: Apply +// --------------------------------------------------------------------------- + +func BenchmarkApply_SingleAdd(b *testing.B) { + doc := []byte(`{"foo":"bar"}`) + patch := []byte(`[{"op":"add","path":"/baz","value":"qux"}]`) + b.ResetTimer() + for b.Loop() { + _, _ = Apply(doc, patch) + } +} + +func BenchmarkApply_SingleReplace(b *testing.B) { + doc := []byte(`{"foo":"bar"}`) + patch := []byte(`[{"op":"replace","path":"/foo","value":"baz"}]`) + b.ResetTimer() + for b.Loop() { + _, _ = Apply(doc, patch) + } +} + +func BenchmarkApply_SingleRemove(b *testing.B) { + doc := []byte(`{"foo":"bar","baz":"qux"}`) + patch := []byte(`[{"op":"remove","path":"/baz"}]`) + b.ResetTimer() + for b.Loop() { + _, _ = Apply(doc, patch) + } +} + +func BenchmarkApply_Move(b *testing.B) { + doc := []byte(`{"foo":{"bar":"baz"},"qux":{"corge":"grault"}}`) + patch := []byte(`[{"op":"move","from":"/foo/bar","path":"/qux/thud"}]`) + b.ResetTimer() + for b.Loop() { + _, _ = Apply(doc, patch) + } +} + +func BenchmarkApply_Copy(b *testing.B) { + doc := []byte(`{"foo":{"bar":"baz"}}`) + patch := []byte(`[{"op":"copy","from":"/foo/bar","path":"/foo/qux"}]`) + b.ResetTimer() + for b.Loop() { + _, _ = Apply(doc, patch) + } +} + +func BenchmarkApply_Test(b *testing.B) { + doc := []byte(`{"foo":"bar"}`) + patch := []byte(`[{"op":"test","path":"/foo","value":"bar"}]`) + b.ResetTimer() + for b.Loop() { + _, _ = Apply(doc, patch) + } +} + +func BenchmarkApply_MultipleOps(b *testing.B) { + doc := []byte(`{"foo":"bar","baz":"qux"}`) + patch := []byte(`[ + {"op":"replace","path":"/foo","value":"new"}, + {"op":"add","path":"/added","value":true}, + {"op":"remove","path":"/baz"}, + {"op":"add","path":"/arr","value":[1,2,3]} + ]`) + b.ResetTimer() + for b.Loop() { + _, _ = Apply(doc, patch) + } +} + +// BenchmarkApply_LargeDocument tests applying a single operation to +// documents of increasing size. +func BenchmarkApply_LargeDocument(b *testing.B) { + sizes := []int{10, 100, 1000} + for _, n := range sizes { + doc := makeObject(n) + patch := []byte(`[{"op":"add","path":"/newkey","value":"newval"}]`) + b.Run(fmt.Sprintf("keys_%d", n), func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _, _ = Apply(doc, patch) + } + }) + } +} + +// BenchmarkApply_LargePatch tests applying many operations to a small document. +func BenchmarkApply_LargePatch(b *testing.B) { + counts := []int{10, 50, 100} + for _, n := range counts { + doc := []byte(`{}`) + ops := make([]Operation, n) + for i := range n { + ops[i], _ = NewOperation(OpAdd, "/key_"+strconv.Itoa(i), "val") + } + patchJSON, _ := json.Marshal(ops) + + b.Run(fmt.Sprintf("ops_%d", n), func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _, _ = Apply(doc, patchJSON) + } + }) + } +} + +// BenchmarkApply_DeepNested benchmarks apply on deeply-nested documents. +func BenchmarkApply_DeepNested(b *testing.B) { + depths := []int{5, 10, 20} + for _, d := range depths { + doc := makeNestedObject(d) + // Build a pointer that reaches the leaf. + path := "" + for range d { + path += "/a" + } + patch := []byte(fmt.Sprintf(`[{"op":"replace","path":"%s","value":"replaced"}]`, path)) + b.Run(fmt.Sprintf("depth_%d", d), func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _, _ = Apply(doc, patch) + } + }) + } +} + +// BenchmarkApply_ArrayInsert benchmarks inserting into arrays of various sizes. +func BenchmarkApply_ArrayInsert(b *testing.B) { + sizes := []int{10, 100, 1000} + for _, n := range sizes { + arr := makeArray(n) + doc := []byte(fmt.Sprintf(`{"items":%s}`, arr)) + patch := []byte(`[{"op":"add","path":"/items/0","value":999}]`) + b.Run(fmt.Sprintf("len_%d", n), func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _, _ = Apply(doc, patch) + } + }) + } +} + +// --------------------------------------------------------------------------- +// Benchmark: ApplyPatch (pre-decoded patch, avoids repeated decode cost) +// --------------------------------------------------------------------------- + +func BenchmarkApplyPatch_PreDecoded(b *testing.B) { + doc := []byte(`{"foo":"bar"}`) + patchJSON := []byte(`[ + {"op":"add","path":"/x","value":1}, + {"op":"replace","path":"/foo","value":"baz"}, + {"op":"add","path":"/y","value":[1,2,3]}, + {"op":"test","path":"/foo","value":"baz"} + ]`) + patch, _ := DecodePatch(patchJSON) + b.ResetTimer() + for b.Loop() { + _, _ = ApplyPatch(doc, patch) + } +} + +// --------------------------------------------------------------------------- +// Benchmark: CreatePatch (diff) +// --------------------------------------------------------------------------- + +func BenchmarkCreatePatch_SmallObject(b *testing.B) { + original := []byte(`{"foo":"bar","baz":"qux"}`) + modified := []byte(`{"foo":"changed","baz":"qux","added":"new"}`) + b.ResetTimer() + for b.Loop() { + _, _ = CreatePatch(original, modified) + } +} + +func BenchmarkCreatePatch_IdenticalObjects(b *testing.B) { + doc := makeObject(100) + b.ResetTimer() + for b.Loop() { + _, _ = CreatePatch(doc, doc) + } +} + +func BenchmarkCreatePatch_LargeObject(b *testing.B) { + sizes := []int{10, 100, 1000} + for _, n := range sizes { + original := makeObject(n) + modified := modifyObject(original) + b.Run(fmt.Sprintf("keys_%d", n), func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _, _ = CreatePatch(original, modified) + } + }) + } +} + +func BenchmarkCreatePatch_ArrayGrow(b *testing.B) { + original := makeArray(50) + modified := makeArray(100) + b.ResetTimer() + for b.Loop() { + _, _ = CreatePatch(original, modified) + } +} + +func BenchmarkCreatePatch_ArrayShrink(b *testing.B) { + original := makeArray(100) + modified := makeArray(50) + b.ResetTimer() + for b.Loop() { + _, _ = CreatePatch(original, modified) + } +} + +func BenchmarkCreatePatch_DeepNested(b *testing.B) { + depths := []int{5, 10, 20} + for _, d := range depths { + original := makeNestedObject(d) + // Change the innermost value. + var doc interface{} + _ = json.Unmarshal(original, &doc) + cur := doc + for i := range d { + m := cur.(map[string]interface{}) + if i == d-1 { + m["a"] = "changed" + } else { + cur = m["a"] + } + } + modified, _ := json.Marshal(doc) + + b.Run(fmt.Sprintf("depth_%d", d), func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _, _ = CreatePatch(original, modified) + } + }) + } +} + +// BenchmarkCreatePatch_RoundTrip measures diff + apply together. +func BenchmarkCreatePatch_RoundTrip(b *testing.B) { + original := makeObject(100) + modified := modifyObject(original) + b.ResetTimer() + for b.Loop() { + patch, _ := CreatePatch(original, modified) + _, _ = ApplyPatch(original, patch) + } +} + +// --------------------------------------------------------------------------- +// Benchmark: DecodePatch +// --------------------------------------------------------------------------- + +func BenchmarkDecodePatch_Small(b *testing.B) { + patchJSON := []byte(`[{"op":"add","path":"/foo","value":"bar"}]`) + b.ResetTimer() + for b.Loop() { + _, _ = DecodePatch(patchJSON) + } +} + +func BenchmarkDecodePatch_Large(b *testing.B) { + counts := []int{10, 50, 100} + for _, n := range counts { + ops := make([]map[string]interface{}, n) + for i := range n { + ops[i] = map[string]interface{}{ + "op": "add", + "path": "/key_" + strconv.Itoa(i), + "value": i, + } + } + patchJSON, _ := json.Marshal(ops) + + b.Run(fmt.Sprintf("ops_%d", n), func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _, _ = DecodePatch(patchJSON) + } + }) + } +} + +// --------------------------------------------------------------------------- +// Benchmark: MarshalPatch +// --------------------------------------------------------------------------- + +func BenchmarkMarshalPatch(b *testing.B) { + counts := []int{1, 10, 50} + for _, n := range counts { + ops := make(Patch, n) + for i := range n { + ops[i], _ = NewOperation(OpAdd, "/key_"+strconv.Itoa(i), "val") + } + b.Run(fmt.Sprintf("ops_%d", n), func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _, _ = MarshalPatch(ops) + } + }) + } +} + +// --------------------------------------------------------------------------- +// Benchmark: ParsePointer +// --------------------------------------------------------------------------- + +func BenchmarkParsePointer_Simple(b *testing.B) { + for b.Loop() { + _, _ = ParsePointer("/foo/bar") + } +} + +func BenchmarkParsePointer_Deep(b *testing.B) { + depths := []int{5, 10, 20} + for _, d := range depths { + ptr := "" + for i := range d { + ptr += "/token" + strconv.Itoa(i) + } + b.Run(fmt.Sprintf("depth_%d", d), func(b *testing.B) { + b.ResetTimer() + for b.Loop() { + _, _ = ParsePointer(ptr) + } + }) + } +} + +func BenchmarkParsePointer_WithEscapes(b *testing.B) { + // "~0" → "~", "~1" → "/" + for b.Loop() { + _, _ = ParsePointer("/foo~0bar/baz~1qux/deep~0~1path") + } +} + +// --------------------------------------------------------------------------- +// Benchmark: Pointer.Evaluate +// --------------------------------------------------------------------------- + +func BenchmarkPointerEvaluate(b *testing.B) { + doc := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "value", + }, + }, + } + ptr, _ := ParsePointer("/a/b/c") + b.ResetTimer() + for b.Loop() { + _, _ = ptr.Evaluate(doc) + } +} + +// --------------------------------------------------------------------------- +// Benchmark: Pointer.Set +// --------------------------------------------------------------------------- + +func BenchmarkPointerSet(b *testing.B) { + ptr, _ := ParsePointer("/a/b/c") + b.ResetTimer() + for b.Loop() { + doc := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "old", + }, + }, + } + _, _ = ptr.Set(doc, "new") + } +} + +// --------------------------------------------------------------------------- +// Benchmark: Pointer.Remove +// --------------------------------------------------------------------------- + +func BenchmarkPointerRemove(b *testing.B) { + ptr, _ := ParsePointer("/a/b/c") + b.ResetTimer() + for b.Loop() { + doc := map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": "value", + }, + }, + } + _, _ = ptr.Remove(doc) + } +} + +// --------------------------------------------------------------------------- +// Benchmark: Operation.UnmarshalJSON +// --------------------------------------------------------------------------- + +func BenchmarkOperationUnmarshal(b *testing.B) { + data := []byte(`{"op":"replace","path":"/foo/bar","value":{"nested":true}}`) + b.ResetTimer() + for b.Loop() { + var op Operation + _ = json.Unmarshal(data, &op) + } +} + +// --------------------------------------------------------------------------- +// Benchmark: End-to-End realistic scenario +// --------------------------------------------------------------------------- + +func BenchmarkEndToEnd_RealisticObject(b *testing.B) { + original := []byte(`{ + "name": "John Doe", + "age": 30, + "email": "john@example.com", + "address": { + "street": "123 Main St", + "city": "Springfield", + "state": "IL", + "zip": "62701" + }, + "phones": [ + {"type": "home", "number": "555-1234"}, + {"type": "work", "number": "555-5678"} + ], + "tags": ["admin", "user"], + "active": true + }`) + + modified := []byte(`{ + "name": "John Doe", + "age": 31, + "email": "john.doe@newdomain.com", + "address": { + "street": "456 Oak Ave", + "city": "Springfield", + "state": "IL", + "zip": "62702" + }, + "phones": [ + {"type": "home", "number": "555-1234"}, + {"type": "work", "number": "555-9999"}, + {"type": "mobile", "number": "555-0000"} + ], + "tags": ["admin", "user", "manager"], + "active": true, + "role": "supervisor" + }`) + + b.Run("CreatePatch", func(b *testing.B) { + for b.Loop() { + _, _ = CreatePatch(original, modified) + } + }) + + b.Run("Apply", func(b *testing.B) { + patch, _ := CreatePatch(original, modified) + patchJSON, _ := MarshalPatch(patch) + b.ResetTimer() + for b.Loop() { + _, _ = Apply(original, patchJSON) + } + }) + + b.Run("RoundTrip", func(b *testing.B) { + for b.Loop() { + patch, _ := CreatePatch(original, modified) + _, _ = ApplyPatch(original, patch) + } + }) +}