Skip to content

Commit

Permalink
Fixed bug #3: Empty path strings should be accepted as the pointer to…
Browse files Browse the repository at this point in the history
… root
  • Loading branch information
grgcombs committed Oct 13, 2014
1 parent a0d835a commit eec5207
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 12 deletions.
2 changes: 1 addition & 1 deletion JSONTools.podspec
Original file line number Diff line number Diff line change
@@ -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
Expand Down
105 changes: 98 additions & 7 deletions JSONTools/JSONPatch.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#import "JSONPatchDictionary.h"
#import "JSONPatchArray.h"
#import "JSONPointer.h"
#import "JSONDeeplyMutable.h"

@implementation JSONPatch

Expand All @@ -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) {
Expand All @@ -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]])
Expand All @@ -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]])
{
Expand All @@ -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;
}
}
}
Expand All @@ -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;
Expand Down Expand Up @@ -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
92 changes: 88 additions & 4 deletions JSONToolsTests/JSONPatchApplyTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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",
Expand Down Expand Up @@ -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"];
Expand All @@ -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",
Expand All @@ -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

0 comments on commit eec5207

Please sign in to comment.