Skip to content

Commit afac545

Browse files
authored
Merge pull request #52 from clagraff/master
Allow arrays for CreateMergePatch
2 parents ed7cfba + ce89457 commit afac545

File tree

2 files changed

+215
-23
lines changed

2 files changed

+215
-23
lines changed

merge.go

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package jsonpatch
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
67
"reflect"
@@ -89,6 +90,7 @@ func pruneAryNulls(ary *partialArray) *partialArray {
8990

9091
var errBadJSONDoc = fmt.Errorf("Invalid JSON Document")
9192
var errBadJSONPatch = fmt.Errorf("Invalid JSON Patch")
93+
var errBadMergeTypes = fmt.Errorf("Mismatched JSON Documents")
9294

9395
// MergeMergePatches merges two merge patches together, such that
9496
// applying this resulting merged merge patch to a document yields the same
@@ -160,30 +162,106 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
160162
return json.Marshal(doc)
161163
}
162164

163-
// CreateMergePatch creates a merge patch as specified in http://tools.ietf.org/html/draft-ietf-appsawg-json-merge-patch-07
164-
//
165-
// 'a' is original, 'b' is the modified document. Both are to be given as json encoded content.
166-
// The function will return a mergeable json document with differences from a to b.
167-
//
168-
// An error will be returned if any of the two documents are invalid.
169-
func CreateMergePatch(a, b []byte) ([]byte, error) {
170-
aI := map[string]interface{}{}
171-
bI := map[string]interface{}{}
172-
err := json.Unmarshal(a, &aI)
165+
// resemblesJSONArray indicates whether the byte-slice "appears" to be
166+
// a JSON array or not.
167+
// False-positives are possible, as this function does not check the internal
168+
// structure of the array. It only checks that the outer syntax is present and
169+
// correct.
170+
func resemblesJSONArray(input []byte) bool {
171+
input = bytes.TrimSpace(input)
172+
173+
hasPrefix := bytes.HasPrefix(input, []byte("["))
174+
hasSuffix := bytes.HasSuffix(input, []byte("]"))
175+
176+
return hasPrefix && hasSuffix
177+
}
178+
179+
// CreateMergePatch will return a merge patch document capable of converting
180+
// the original document(s) to the modified document(s).
181+
// The parameters can be bytes of either two JSON Documents, or two arrays of
182+
// JSON documents.
183+
// The merge patch returned follows the specification defined at http://tools.ietf.org/html/draft-ietf-appsawg-json-merge-patch-07
184+
func CreateMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
185+
originalResemblesArray := resemblesJSONArray(originalJSON)
186+
modifiedResemblesArray := resemblesJSONArray(modifiedJSON)
187+
188+
// Do both byte-slices seem like JSON arrays?
189+
if originalResemblesArray && modifiedResemblesArray {
190+
return createArrayMergePatch(originalJSON, modifiedJSON)
191+
}
192+
193+
// Are both byte-slices are not arrays? Then they are likely JSON objects...
194+
if !originalResemblesArray && !modifiedResemblesArray {
195+
return createObjectMergePatch(originalJSON, modifiedJSON)
196+
}
197+
198+
// None of the above? Then return an error because of mismatched types.
199+
return nil, errBadMergeTypes
200+
}
201+
202+
// createObjectMergePatch will return a merge-patch document capable of
203+
// converting the original document to the modified document.
204+
func createObjectMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
205+
originalDoc := map[string]interface{}{}
206+
modifiedDoc := map[string]interface{}{}
207+
208+
err := json.Unmarshal(originalJSON, &originalDoc)
173209
if err != nil {
174210
return nil, errBadJSONDoc
175211
}
176-
err = json.Unmarshal(b, &bI)
212+
213+
err = json.Unmarshal(modifiedJSON, &modifiedDoc)
177214
if err != nil {
178215
return nil, errBadJSONDoc
179216
}
180-
dest, err := getDiff(aI, bI)
217+
218+
dest, err := getDiff(originalDoc, modifiedDoc)
181219
if err != nil {
182220
return nil, err
183221
}
222+
184223
return json.Marshal(dest)
185224
}
186225

226+
// createArrayMergePatch will return an array of merge-patch documents capable
227+
// of converting the original document to the modified document for each
228+
// pair of JSON documents provided in the arrays.
229+
// Arrays of mismatched sizes will result in an error.
230+
func createArrayMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
231+
originalDocs := []json.RawMessage{}
232+
modifiedDocs := []json.RawMessage{}
233+
234+
err := json.Unmarshal(originalJSON, &originalDocs)
235+
if err != nil {
236+
return nil, errBadJSONDoc
237+
}
238+
239+
err = json.Unmarshal(modifiedJSON, &modifiedDocs)
240+
if err != nil {
241+
return nil, errBadJSONDoc
242+
}
243+
244+
total := len(originalDocs)
245+
if len(modifiedDocs) != total {
246+
return nil, errBadJSONDoc
247+
}
248+
249+
result := []json.RawMessage{}
250+
for i := 0; i < len(originalDocs); i++ {
251+
original := originalDocs[i]
252+
modified := modifiedDocs[i]
253+
254+
patch, err := createObjectMergePatch(original, modified)
255+
if err != nil {
256+
return nil, err
257+
}
258+
259+
result = append(result, json.RawMessage(patch))
260+
}
261+
262+
return json.Marshal(result)
263+
}
264+
187265
// Returns true if the array matches (must be json types).
188266
// As is idiomatic for go, an empty array is not the same as a nil array.
189267
func matchesArray(a, b []interface{}) bool {

merge_test.go

Lines changed: 125 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,50 @@ func TestMergePatchFailRFCCases(t *testing.T) {
184184

185185
}
186186

187-
func TestMergeReplaceKey(t *testing.T) {
187+
func TestResembleJSONArray(t *testing.T) {
188+
testCases := []struct {
189+
input []byte
190+
expected bool
191+
}{
192+
// Failure cases
193+
{input: []byte(``), expected: false},
194+
{input: []byte(`not an array`), expected: false},
195+
{input: []byte(`{"foo": "bar"}`), expected: false},
196+
{input: []byte(`{"fizz": ["buzz"]}`), expected: false},
197+
{input: []byte(`[bad suffix`), expected: false},
198+
{input: []byte(`bad prefix]`), expected: false},
199+
{input: []byte(`][`), expected: false},
200+
201+
// Valid cases
202+
{input: []byte(`[]`), expected: true},
203+
{input: []byte(`["foo", "bar"]`), expected: true},
204+
{input: []byte(`[["foo", "bar"]]`), expected: true},
205+
{input: []byte(`[not valid syntax]`), expected: true},
206+
207+
// Valid cases with whitespace
208+
{input: []byte(` []`), expected: true},
209+
{input: []byte(`[] `), expected: true},
210+
{input: []byte(` [] `), expected: true},
211+
{input: []byte(` [ ] `), expected: true},
212+
{input: []byte("\t[]"), expected: true},
213+
{input: []byte("[]\n"), expected: true},
214+
{input: []byte("\n\t\r[]"), expected: true},
215+
}
216+
217+
for _, test := range testCases {
218+
result := resemblesJSONArray(test.input)
219+
if result != test.expected {
220+
t.Errorf(
221+
`expected "%t" but received "%t" for case: "%s"`,
222+
test.expected,
223+
result,
224+
string(test.input),
225+
)
226+
}
227+
}
228+
}
229+
230+
func TestCreateMergePatchReplaceKey(t *testing.T) {
188231
doc := `{ "title": "hello", "nested": {"one": 1, "two": 2} }`
189232
pat := `{ "title": "goodbye", "nested": {"one": 2, "two": 2} }`
190233

@@ -201,7 +244,7 @@ func TestMergeReplaceKey(t *testing.T) {
201244
}
202245
}
203246

204-
func TestMergeGetArray(t *testing.T) {
247+
func TestCreateMergePatchGetArray(t *testing.T) {
205248
doc := `{ "title": "hello", "array": ["one", "two"], "notmatch": [1, 2, 3] }`
206249
pat := `{ "title": "hello", "array": ["one", "two", "three"], "notmatch": [1, 2, 3] }`
207250

@@ -218,7 +261,7 @@ func TestMergeGetArray(t *testing.T) {
218261
}
219262
}
220263

221-
func TestMergeGetObjArray(t *testing.T) {
264+
func TestCreateMergePatchGetObjArray(t *testing.T) {
222265
doc := `{ "title": "hello", "array": [{"banana": true}, {"evil": false}], "notmatch": [{"one":1}, {"two":2}, {"three":3}] }`
223266
pat := `{ "title": "hello", "array": [{"banana": false}, {"evil": true}], "notmatch": [{"one":1}, {"two":2}, {"three":3}] }`
224267

@@ -235,7 +278,7 @@ func TestMergeGetObjArray(t *testing.T) {
235278
}
236279
}
237280

238-
func TestMergeDeleteKey(t *testing.T) {
281+
func TestCreateMergePatchDeleteKey(t *testing.T) {
239282
doc := `{ "title": "hello", "nested": {"one": 1, "two": 2} }`
240283
pat := `{ "title": "hello", "nested": {"one": 1} }`
241284

@@ -253,7 +296,7 @@ func TestMergeDeleteKey(t *testing.T) {
253296
}
254297
}
255298

256-
func TestMergeEmptyArray(t *testing.T) {
299+
func TestCreateMergePatchEmptyArray(t *testing.T) {
257300
doc := `{ "array": null }`
258301
pat := `{ "array": [] }`
259302

@@ -288,7 +331,7 @@ func TestCreateMergePatchNil(t *testing.T) {
288331
}
289332
}
290333

291-
func TestMergeObjArray(t *testing.T) {
334+
func TestCreateMergePatchObjArray(t *testing.T) {
292335
doc := `{ "array": [ {"a": {"b": 2}}, {"a": {"b": 3}} ]}`
293336
exp := `{}`
294337

@@ -304,7 +347,78 @@ func TestMergeObjArray(t *testing.T) {
304347
}
305348
}
306349

307-
func TestMergeComplexMatch(t *testing.T) {
350+
func TestCreateMergePatchSameOuterArray(t *testing.T) {
351+
doc := `[{"foo": "bar"}]`
352+
pat := doc
353+
exp := `[{}]`
354+
355+
res, err := CreateMergePatch([]byte(doc), []byte(pat))
356+
357+
if err != nil {
358+
t.Errorf("Unexpected error: %s, %s", err, string(res))
359+
}
360+
361+
if !compareJSON(exp, string(res)) {
362+
t.Fatalf("Outer array was not unmodified")
363+
}
364+
}
365+
366+
func TestCreateMergePatchModifiedOuterArray(t *testing.T) {
367+
doc := `[{"name": "John"}, {"name": "Will"}]`
368+
pat := `[{"name": "Jane"}, {"name": "Will"}]`
369+
exp := `[{"name": "Jane"}, {}]`
370+
371+
res, err := CreateMergePatch([]byte(doc), []byte(pat))
372+
373+
if err != nil {
374+
t.Errorf("Unexpected error: %s, %s", err, string(res))
375+
}
376+
377+
if !compareJSON(exp, string(res)) {
378+
t.Fatalf("Expected %s but received %s", exp, res)
379+
}
380+
}
381+
382+
func TestCreateMergePatchMismatchedOuterArray(t *testing.T) {
383+
doc := `[{"name": "John"}, {"name": "Will"}]`
384+
pat := `[{"name": "Jane"}]`
385+
386+
_, err := CreateMergePatch([]byte(doc), []byte(pat))
387+
388+
if err == nil {
389+
t.Errorf("Expected error due to array length differences but received none")
390+
}
391+
}
392+
393+
func TestCreateMergePatchMismatchedOuterTypes(t *testing.T) {
394+
doc := `[{"name": "John"}]`
395+
pat := `{"name": "Jane"}`
396+
397+
_, err := CreateMergePatch([]byte(doc), []byte(pat))
398+
399+
if err == nil {
400+
t.Errorf("Expected error due to mismatched types but received none")
401+
}
402+
}
403+
404+
func TestCreateMergePatchNoDifferences(t *testing.T) {
405+
doc := `{ "title": "hello", "nested": {"one": 1, "two": 2} }`
406+
pat := doc
407+
408+
exp := `{}`
409+
410+
res, err := CreateMergePatch([]byte(doc), []byte(pat))
411+
412+
if err != nil {
413+
t.Errorf("Unexpected error: %s, %s", err, string(res))
414+
}
415+
416+
if !compareJSON(exp, string(res)) {
417+
t.Fatalf("Key was not replaced")
418+
}
419+
}
420+
421+
func TestCreateMergePatchComplexMatch(t *testing.T) {
308422
doc := `{"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4], "nested": {"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4]} }`
309423
empty := `{}`
310424
res, err := CreateMergePatch([]byte(doc), []byte(doc))
@@ -319,7 +433,7 @@ func TestMergeComplexMatch(t *testing.T) {
319433
}
320434
}
321435

322-
func TestMergeComplexAddAll(t *testing.T) {
436+
func TestCreateMergePatchComplexAddAll(t *testing.T) {
323437
doc := `{"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4], "nested": {"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4]} }`
324438
empty := `{}`
325439
res, err := CreateMergePatch([]byte(empty), []byte(doc))
@@ -333,7 +447,7 @@ func TestMergeComplexAddAll(t *testing.T) {
333447
}
334448
}
335449

336-
func TestMergeComplexRemoveAll(t *testing.T) {
450+
func TestCreateMergePatchComplexRemoveAll(t *testing.T) {
337451
doc := `{"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4], "nested": {"hello": "world","t": true ,"f": false, "n": null,"i": 123,"pi": 3.1416,"a": [1, 2, 3, 4]} }`
338452
exp := `{"a":null,"f":null,"hello":null,"i":null,"n":null,"nested":null,"pi":null,"t":null}`
339453
empty := `{}`
@@ -355,7 +469,7 @@ func TestMergeComplexRemoveAll(t *testing.T) {
355469
*/
356470
}
357471

358-
func TestMergeObjectWithInnerArray(t *testing.T) {
472+
func TestCreateMergePatchObjectWithInnerArray(t *testing.T) {
359473
stateString := `{
360474
"OuterArray": [
361475
{
@@ -379,7 +493,7 @@ func TestMergeObjectWithInnerArray(t *testing.T) {
379493
}
380494
}
381495

382-
func TestMergeReplaceKeyNotEscape(t *testing.T) {
496+
func TestCreateMergePatchReplaceKeyNotEscape(t *testing.T) {
383497
doc := `{ "title": "hello", "nested": {"title/escaped": 1, "two": 2} }`
384498
pat := `{ "title": "goodbye", "nested": {"title/escaped": 2, "two": 2} }`
385499

0 commit comments

Comments
 (0)