Skip to content

Commit a02c130

Browse files
authored
Merge pull request #32 from Valian/greedy-object-hash
Greedy object hash
2 parents 0a67aad + 4fe7b7f commit a02c130

6 files changed

Lines changed: 1337 additions & 15 deletions

File tree

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Features:
2020
- test
2121
- Escaping of "`/`" (by "`~1`") and "`~`" (by "`~0`")
2222
- Allow usage of `-` for appending things to list (Add and Copy operation)
23+
- Smart list diffing with `object_hash` for efficient patches on collections with unique identifiers
2324

2425
## Getting started
2526

@@ -52,6 +53,36 @@ iex> Jsonpatch.diff(source, destination)
5253
]
5354
```
5455

56+
### Smart List Diffing with `object_hash`
57+
58+
Use `object_hash` to generate efficient patches for lists of objects with unique identifiers, producing minimal operations instead of cascading replacements.
59+
60+
```elixir
61+
iex> original = [
62+
%{id: 1, name: "Alice"},
63+
%{id: 2, name: "Bob"}
64+
]
65+
iex> updated = [
66+
%{id: 99, name: "New"},
67+
%{id: 1, name: "Alice"},
68+
%{id: 2, name: "Bob"}
69+
]
70+
71+
# Traditional pairwise diff - multiple replace operations
72+
# >> Jsonpatch.diff(original, updated)
73+
[
74+
%{op: "add", path: "/2", value: %{id: 2, name: "Bob"}}
75+
%{op: "replace", path: "/0", value: %{id: 99, name: "New"}},
76+
%{op: "replace", path: "/1", value: %{id: 1, name: "Alice"}},
77+
]
78+
79+
# With object_hash - single add operation
80+
iex> Jsonpatch.diff(original, updated, object_hash: fn %{id: id} -> id end)
81+
[
82+
%{op: "add", path: "/0", value: %{id: 99, name: "New"}}
83+
]
84+
```
85+
5586
### Apply patches
5687

5788
```elixir
@@ -69,4 +100,4 @@ iex> Jsonpatch.apply_patch(patch, target)
69100

70101
## Important sources
71102
- [Official RFC 6902](https://tools.ietf.org/html/rfc6902)
72-
- [Inspiration: python-json-patch](https://github.com/stefankoegl/python-json-patch)
103+
- [Inspiration: python-json-patch](https://github.com/stefankoegl/python-json-patch)

benchmarks/generic_benchmark.exs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
# Jsonpatch Diff Performance Benchmark
2+
# Run with: mix run test/benchmark.exs
3+
4+
defmodule JsonpatchBenchmark do
5+
@doc """
6+
Prepare complex test cases for benchmarking
7+
"""
8+
def prepare_test_cases() do
9+
%{
10+
"Complex Maps - E-commerce Order" => %{
11+
doc: %{
12+
"order_id" => "12345",
13+
"customer" => %{
14+
"name" => "John Doe",
15+
"email" => "john@example.com",
16+
"address" => %{
17+
"street" => "123 Main St",
18+
"city" => "Springfield",
19+
"country" => "USA"
20+
}
21+
},
22+
"items" => %{
23+
"item1" => %{"name" => "Laptop", "price" => 999.99, "quantity" => 1},
24+
"item2" => %{"name" => "Mouse", "price" => 29.99, "quantity" => 2}
25+
},
26+
"status" => "pending",
27+
"total" => 1059.97
28+
},
29+
expected: %{
30+
"order_id" => "12345",
31+
"customer" => %{
32+
"name" => "John Doe",
33+
"email" => "john.doe@example.com",
34+
"address" => %{
35+
"street" => "456 Oak Ave",
36+
"city" => "Springfield",
37+
"country" => "USA",
38+
"zipcode" => "12345"
39+
},
40+
"phone" => "+1-555-0123"
41+
},
42+
"items" => %{
43+
"item1" => %{"name" => "Gaming Laptop", "price" => 1299.99, "quantity" => 1},
44+
"item3" => %{"name" => "Keyboard", "price" => 79.99, "quantity" => 1}
45+
},
46+
"status" => "confirmed",
47+
"total" => 1379.98,
48+
"discount" => 50.00
49+
}
50+
},
51+
"Complex Lists - Task Management" => %{
52+
doc: [
53+
%{
54+
"id" => 1,
55+
"task" => "Write documentation",
56+
"priority" => "high",
57+
"completed" => false
58+
},
59+
%{"id" => 2, "task" => "Fix bug #123", "priority" => "medium", "completed" => true},
60+
%{"id" => 3, "task" => "Review PR", "priority" => "low", "completed" => false},
61+
%{"id" => 4, "task" => "Deploy to staging", "priority" => "high", "completed" => false},
62+
%{"id" => 5, "task" => "Update tests", "priority" => "medium", "completed" => true}
63+
],
64+
expected: [
65+
%{
66+
"id" => 1,
67+
"task" => "Write comprehensive documentation",
68+
"priority" => "high",
69+
"completed" => true
70+
},
71+
%{
72+
"id" => 6,
73+
"task" => "Optimize database queries",
74+
"priority" => "high",
75+
"completed" => false
76+
},
77+
%{"id" => 3, "task" => "Review PR", "priority" => "medium", "completed" => false},
78+
%{"id" => 7, "task" => "Setup monitoring", "priority" => "low", "completed" => false},
79+
%{
80+
"id" => 4,
81+
"task" => "Deploy to production",
82+
"priority" => "critical",
83+
"completed" => false
84+
}
85+
]
86+
},
87+
"Mixed Maps and Lists - Social Media Post" => %{
88+
doc: %{
89+
"post_id" => "abc123",
90+
"content" => "Just had an amazing day!",
91+
"author" => %{
92+
"username" => "johndoe",
93+
"followers" => 1250,
94+
"verified" => false
95+
},
96+
"comments" => [
97+
%{"user" => "alice", "text" => "Great to hear!", "likes" => 5},
98+
%{"user" => "bob", "text" => "Awesome!", "likes" => 3}
99+
],
100+
"tags" => ["happy", "life"],
101+
"metadata" => %{
102+
"created_at" => "2023-01-01T10:00:00Z",
103+
"location" => "New York",
104+
"device" => "mobile"
105+
}
106+
},
107+
expected: %{
108+
"post_id" => "abc123",
109+
"content" => "Just had an absolutely amazing day! #blessed",
110+
"author" => %{
111+
"username" => "johndoe",
112+
"followers" => 1275,
113+
"verified" => true,
114+
"display_name" => "John Doe"
115+
},
116+
"comments" => [
117+
%{"user" => "alice", "text" => "Great to hear! So happy for you!", "likes" => 8},
118+
%{"user" => "charlie", "text" => "Inspiring!", "likes" => 2},
119+
%{"user" => "bob", "text" => "Awesome!", "likes" => 3, "reply_to" => "alice"}
120+
],
121+
"tags" => ["happy", "life", "blessed", "inspiration"],
122+
"metadata" => %{
123+
"created_at" => "2023-01-01T10:00:00Z",
124+
"updated_at" => "2023-01-01T10:15:00Z",
125+
"location" => "New York",
126+
"device" => "mobile",
127+
"engagement_score" => 8.5
128+
},
129+
"reactions" => %{
130+
"likes" => 45,
131+
"shares" => 12,
132+
"hearts" => 23
133+
}
134+
}
135+
},
136+
"Deep Nesting - Configuration Tree" => %{
137+
doc: %{
138+
"application" => %{
139+
"name" => "MyApp",
140+
"version" => "1.0.0",
141+
"modules" => %{
142+
"authentication" => %{
143+
"enabled" => true,
144+
"providers" => %{
145+
"oauth" => %{
146+
"google" => %{"client_id" => "123", "scopes" => ["email", "profile"]},
147+
"github" => %{"client_id" => "456", "scopes" => ["user:email"]}
148+
},
149+
"local" => %{"enabled" => true, "password_policy" => %{"min_length" => 8}}
150+
}
151+
},
152+
"database" => %{
153+
"primary" => %{
154+
"host" => "localhost",
155+
"port" => 5432,
156+
"name" => "myapp_db",
157+
"pool" => %{"size" => 10, "timeout" => 5000}
158+
},
159+
"replica" => %{
160+
"host" => "replica.example.com",
161+
"port" => 5432,
162+
"name" => "myapp_db"
163+
}
164+
}
165+
}
166+
}
167+
},
168+
expected: %{
169+
"application" => %{
170+
"name" => "MyApp",
171+
"version" => "1.1.0",
172+
"modules" => %{
173+
"authentication" => %{
174+
"enabled" => true,
175+
"providers" => %{
176+
"oauth" => %{
177+
"google" => %{
178+
"client_id" => "123",
179+
"scopes" => ["email", "profile", "calendar"]
180+
},
181+
"github" => %{"client_id" => "789", "scopes" => ["user:email", "read:user"]},
182+
"microsoft" => %{"client_id" => "999", "scopes" => ["User.Read"]}
183+
},
184+
"local" => %{
185+
"enabled" => true,
186+
"password_policy" => %{"min_length" => 12, "require_symbols" => true}
187+
},
188+
"saml" => %{
189+
"enabled" => false,
190+
"metadata_url" => "https://sso.example.com/metadata"
191+
}
192+
}
193+
},
194+
"database" => %{
195+
"primary" => %{
196+
"host" => "db.example.com",
197+
"port" => 5432,
198+
"name" => "myapp_production",
199+
"pool" => %{"size" => 20, "timeout" => 10_000, "idle_timeout" => 30_000}
200+
},
201+
"cache" => %{
202+
"host" => "redis.example.com",
203+
"port" => 6379,
204+
"ttl" => 3600
205+
}
206+
},
207+
"monitoring" => %{
208+
"metrics" => %{"enabled" => true, "interval" => 60},
209+
"logging" => %{"level" => "info", "format" => "json"}
210+
}
211+
},
212+
"features" => %{
213+
"feature_flags" => %{"new_ui" => true, "beta_features" => false}
214+
}
215+
}
216+
}
217+
}
218+
}
219+
end
220+
221+
@doc """
222+
Run the benchmark
223+
"""
224+
def run_benchmark() do
225+
Benchee.run(
226+
%{
227+
# I was using it for performance comparision, now faster version is the default one
228+
# "Faster JsonPatch" => fn %{doc: doc, expected: expected} ->
229+
# Jsonpatch.Faster.diff(doc, expected)
230+
# end,
231+
"JsonPatch" => fn %{doc: doc, expected: expected} ->
232+
Jsonpatch.diff(doc, expected)
233+
end
234+
},
235+
inputs: prepare_test_cases(),
236+
warmup: 0.1,
237+
time: 0.5,
238+
memory_time: 0.2,
239+
reduction_time: 0.2,
240+
parallel: 2,
241+
formatters: [
242+
Benchee.Formatters.Console
243+
],
244+
print: [
245+
benchmarking: true,
246+
configuration: false,
247+
fast_warning: false
248+
]
249+
)
250+
end
251+
end
252+
253+
# Run the benchmark
254+
JsonpatchBenchmark.run_benchmark()

0 commit comments

Comments
 (0)