From eec52070f1edfa06478739eecc79c9339b83afd4 Mon Sep 17 00:00:00 2001 From: Greg Combs Date: Mon, 13 Oct 2014 14:01:09 -0500 Subject: [PATCH] Fixed bug #3: Empty path strings should be accepted as the pointer to root --- JSONTools.podspec | 2 +- JSONTools/JSONPatch.m | 105 +++++++++++++++++++++++++-- JSONToolsTests/JSONPatchApplyTests.m | 92 ++++++++++++++++++++++- 3 files changed, 187 insertions(+), 12 deletions(-) diff --git a/JSONTools.podspec b/JSONTools.podspec index 4b4e32c..ddf2dae 100644 --- a/JSONTools.podspec +++ b/JSONTools.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "JSONTools" - s.version = "1.0.2" + s.version = "1.0.3" s.summary = "JSON Patch, JSON Pointer, and JSON Schema Validation in Objective-C" s.description = <<-DESC This Objective-C library is a collection of classes and categories that implement diff --git a/JSONTools/JSONPatch.m b/JSONTools/JSONPatch.m index c0faa80..ab6ece2 100644 --- a/JSONTools/JSONPatch.m +++ b/JSONTools/JSONPatch.m @@ -11,6 +11,7 @@ #import "JSONPatchDictionary.h" #import "JSONPatchArray.h" #import "JSONPointer.h" +#import "JSONDeeplyMutable.h" @implementation JSONPatch @@ -30,8 +31,19 @@ + (id)applyPatches:(NSArray *)patches toCollection:(id)collection if (!patchInfo) break; - NSArray *pathKeys = [patchInfo.path componentsSeparatedByString:@"/"]; id object = collection; + + if (patchInfo.path && !patchInfo.path.length) + { + // http://tools.ietf.org/html/rfc6901#section-5 + // The following JSON strings evaluate to the accompanying values: + // "" // the whole document + + result = [self applyRootLevelPatch:patchInfo toCollection:object]; + continue; + } + + NSArray *pathKeys = [patchInfo.path componentsSeparatedByString:@"/"]; NSInteger t = 1; while (YES) { @@ -40,11 +52,12 @@ + (id)applyPatches:(NSArray *)patches toCollection:(id)collection result = @(0); break; } - if (![object isKindOfClass:[NSMutableDictionary class]] && - ![object isKindOfClass:[NSMutableArray class]] && - [object respondsToSelector:@selector(mutableCopy)]) + if ([self collectionNeedsMutableCopy:object]) { - object = [object mutableCopy]; + // If you get here, chances are subsequent/multiple patches won't work correctly. + // You should be sure to pass in a collection via -copyAsDeeplyMutable to ensure + // that this doesn't adversely affect you. + object = [object copyAsDeeplyMutableJSON]; } if ([object isKindOfClass:[NSMutableArray class]]) @@ -65,7 +78,15 @@ + (id)applyPatches:(NSArray *)patches toCollection:(id)collection } break; } - object = (NSMutableArray *)object[index]; + + NSMutableArray *thisObject = object; + NSMutableArray *nextObject = thisObject[index]; + if ([self collectionNeedsMutableCopy:nextObject]) + { + nextObject = [nextObject copyAsDeeplyMutableJSON]; + thisObject[index] = nextObject; + } + object = nextObject; } else if ([object isKindOfClass:[NSMutableDictionary class]]) { @@ -82,7 +103,15 @@ + (id)applyPatches:(NSArray *)patches toCollection:(id)collection } break; } - object = (NSMutableDictionary *)object[component]; + + NSMutableDictionary *thisObject = object; + NSMutableDictionary *nextObject = thisObject[component]; + if ([self collectionNeedsMutableCopy:nextObject]) + { + nextObject = [nextObject copyAsDeeplyMutableJSON]; + thisObject[component] = nextObject; + } + object = nextObject; } } } @@ -91,6 +120,57 @@ + (id)applyPatches:(NSArray *)patches toCollection:(id)collection #pragma mark - Singular Patch ++ (id)applyRootLevelPatch:(JSONPatchInfo *)patchInfo toCollection:(id)collection +{ + if (!collection || + ![collection respondsToSelector:@selector(mutableCopy)]) + { + return nil; + } + + id result = nil; + + switch (patchInfo.op) + { + case JSONPatchOperationGet: + return [collection mutableCopy]; + break; + case JSONPatchOperationTest: + result = @(collection == patchInfo.value || [collection isEqual:patchInfo.value]); + break; + case JSONPatchOperationAdd: + case JSONPatchOperationRemove: + case JSONPatchOperationCopy: + case JSONPatchOperationMove: + case JSONPatchOperationReplace: + { + if (![self isCompatibleCollection:collection toCollection:patchInfo.value] || + ![collection respondsToSelector:@selector(removeAllObjects)]) + { + result = @(NO); + break; + } + + [collection removeAllObjects]; + + if ([collection isKindOfClass:[NSDictionary class]]) + { + [collection addEntriesFromDictionary:patchInfo.value]; + result = @([collection isEqualToDictionary:patchInfo.value]); + } + else if ([collection isKindOfClass:[NSArray class]]) + { + [collection addObjectsFromArray:patchInfo.value]; + result = @([collection isEqualToArray:patchInfo.value]); + } + break; + } + case JSONPatchOperationUndefined: + break; + } + return result; +} + + (id)applyPatch:(JSONPatchInfo *)patchInfo array:(NSMutableArray *)array index:(NSInteger)index collection:(id)collection stop:(BOOL *)stop { BOOL success = NO; @@ -339,4 +419,15 @@ + (BOOL)isCompatibleArrays:(NSArray *)array1 array2:(NSArray *)array2 [array2 isKindOfClass:[NSArray class]]); } ++ (BOOL)collectionNeedsMutableCopy:(id)collection +{ + if (![collection isKindOfClass:[NSMutableDictionary class]] && + ![collection isKindOfClass:[NSMutableArray class]] && + [collection respondsToSelector:@selector(mutableCopy)]) + { + return YES; + } + return NO; +} + @end diff --git a/JSONToolsTests/JSONPatchApplyTests.m b/JSONToolsTests/JSONPatchApplyTests.m index 7e03594..8cff477 100644 --- a/JSONToolsTests/JSONPatchApplyTests.m +++ b/JSONToolsTests/JSONPatchApplyTests.m @@ -82,7 +82,7 @@ - (void)testShouldApplyRemove @"baz": @[@{@"qux": @"hello"}], @"bar": @[@1,@2,@3,@4]} copyAsDeeplyMutableJSON]; - NSMutableDictionary *expected = [obj mutableCopy]; + NSMutableDictionary *expected = [obj copyAsDeeplyMutableJSON]; [expected removeObjectForKey:@"bar"]; [JSONPatch applyPatches:@[@{@"op": @"remove", @"path": @"/bar"}] toCollection:obj]; @@ -100,7 +100,7 @@ - (void)testShouldApplyReplace NSMutableDictionary *obj = [@{@"foo": @1, @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON]; - NSMutableDictionary *expected = [obj mutableCopy]; + NSMutableDictionary *expected = [obj copyAsDeeplyMutableJSON]; expected[@"foo"] = @[@1,@2,@3,@4]; [JSONPatch applyPatches:@[@{@"op": @"replace", @"path": @"/foo", @@ -135,7 +135,7 @@ - (void)testShouldApplyMove { NSMutableDictionary *obj = [@{@"foo": @1, @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON]; - NSMutableDictionary *expected = [obj mutableCopy]; + NSMutableDictionary *expected = [obj copyAsDeeplyMutableJSON]; expected[@"bar"] = @1; [expected removeObjectForKey:@"foo"]; @@ -156,7 +156,7 @@ - (void)testShouldApplyCopy { NSMutableDictionary *obj = [@{@"foo": @1, @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON]; - NSMutableDictionary *expected = [obj mutableCopy]; + NSMutableDictionary *expected = [obj copyAsDeeplyMutableJSON]; expected[@"bar"] = @1; [JSONPatch applyPatches:@[@{@"op": @"copy", @@ -171,4 +171,88 @@ - (void)testShouldApplyCopy XCTAssertEqualObjects(obj, expected, @"Failed to apply copy patch, expected %@, found %@", expected, obj); } +- (void)testShouldApplyMultiplePatches +{ + NSMutableDictionary *obj = [@{@"firstName": @"Albert", + @"contactDetails": @{ + @"phoneNumbers": @[] + } + } copyAsDeeplyMutableJSON]; + + NSArray *patches = @[ + @{@"op": @"replace", + @"path": @"/firstName", + @"value": @"Joachim" + }, + @{@"op": @"add", + @"path": @"/lastName", + @"value": @"Wester" + }, + @{@"op": @"add", + @"path": @"/contactDetails/phoneNumbers/0", + @"value": @{ @"number": @"555-123" } + } + ]; + + NSMutableDictionary *expected = [@{@"firstName": @"Joachim", + @"lastName": @"Wester", + @"contactDetails": @{ + @"phoneNumbers": @[ + @{@"number": @"555-123"} + ] + } + } mutableCopy]; + + [JSONPatch applyPatches:patches toCollection:obj]; + + XCTAssertEqualObjects(obj, expected, @"Failed to apply multiple patches, expected %@, found %@", expected, obj); +} + +/** + * Empty path strings should be accepted as the pointer to root + * https://github.com/grgcombs/JSONTools/issues/3 + * + * JSON Pointer RFC + * http://tools.ietf.org/html/rfc6901#section-5 + * + * The following JSON strings evaluate to the accompanying values: + * + * "" // the whole document + */ +- (void)testShouldAcceptPatchesToRootPointer +{ + NSMutableDictionary *obj = [@{@"foo": @1, + @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON]; + + NSDictionary *expected = @{@"bar": @[@1,@2,@3,@4]}; + id result = [JSONPatch applyPatches:@[@{@"op": @"replace", + @"path": @"", + @"value": @{@"bar": @[@1,@2,@3,@4]}}] toCollection:obj]; + XCTAssertEqualObjects(result, @1, @"The root-level patch should succeed."); + XCTAssertEqualObjects(obj, expected, @"Failed to apply root-level patch, expected %@, found %@", expected, obj); +} + +- (void)testShouldNotPatchIncompatibleTopLevelCollections +{ + NSMutableDictionary *dictionary = [@{@"foo": @1, + @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON]; + NSArray *replacementArray = @[@1,@2,@3,@4]; + + id result = [JSONPatch applyPatches:@[@{@"op": @"replace", + @"path": @"", + @"value": replacementArray}] toCollection:dictionary]; + XCTAssertEqualObjects(result, @0, @"Should have failed to replace a top level dictionary with an array: %@", result); + + NSMutableArray *array = [@[@1,@2,@3,@4] copyAsDeeplyMutableJSON]; + NSDictionary *replacementDictionary = @{@"1": @1, + @"2": @2, + @"3": @3, + @"4": @4}; + + result = [JSONPatch applyPatches:@[@{@"op": @"replace", + @"path": @"", + @"value": replacementDictionary}] toCollection:array]; + XCTAssertEqualObjects(result, @0, @"Should have failed to replace a top level array with a dictionary: %@", result); +} + @end