├── JSONToolsTests
├── en.lproj
│ └── InfoPlist.strings
├── JSONToolsTests-Info.plist
├── JSONPatchGenerateTests.m
├── JSONSchemaTests.m
├── JSONPointerTests.m
└── JSONPatchApplyTests.m
├── JSONTools
├── JSONTools-Prefix.pch
├── JSONTools.h
├── JSONPatchArray.h
├── JSONPatchDictionary.h
├── JSONSchemaValidator.h
├── JSONPointer.h
├── JSONPatchInfo.h
├── JSONPatch.h
├── NSDictionary+JSONPointer.m
├── NSArray+JSONDeepMutable.m
├── NSDictionary+JSONDeepMutable.m
├── JSONDeeplyMutable.m
├── NSDictionary+JSONPointer.h
├── NSArray+JSONDeepMutable.h
├── NSDictionary+JSONDeepMutable.h
├── JSONDeeplyMutable.h
├── NSArray+JSONPointer.m
├── NSArray+JSONPointer.h
├── JSONPointer.m
├── JSONPatchDictionary.m
├── JSONPatchArray.m
├── JSONPatchInfo.m
├── JSONPatch.m
└── JSONSchemaValidator.m
├── Podfile
├── JSONToolsTests.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcshareddata
│ └── xcschemes
│ │ └── JSONToolsTests.xcscheme
└── project.pbxproj
├── .travis.yml
├── .gitignore
├── JSONTools.podspec
├── LICENSE
└── README.md
/JSONToolsTests/en.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
1 | /* Localized versions of Info.plist keys */
2 |
3 |
--------------------------------------------------------------------------------
/JSONTools/JSONTools-Prefix.pch:
--------------------------------------------------------------------------------
1 | //
2 | // Prefix header
3 | //
4 | // The contents of this file are implicitly included at the beginning of every source file.
5 | //
6 |
7 | #ifdef __OBJC__
8 | @import Foundation;
9 | #endif
10 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | xcodeproj 'JSONToolsTests.xcodeproj'
2 | platform :ios, '7.0'
3 | pod "JSONTools", path: '.'
4 |
5 | target :JSONToolsTests do
6 | pod 'JSON-Schema-Test-Suite', '~> 1.1.2-Pod'
7 | pod 'KiteJSONValidator/KiteJSONResources'
8 | end
9 |
--------------------------------------------------------------------------------
/JSONToolsTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/JSONTools/JSONTools.h:
--------------------------------------------------------------------------------
1 | //
2 | // JSONTools.h
3 | // JSONTools
4 | //
5 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
6 | // See LICENSE.txt for details.
7 |
8 | @import Foundation;
9 |
10 | #import "JSONPointer.h"
11 | #import "JSONPatch.h"
12 | #import "JSONSchemaValidator.h"
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: objective-c
2 | before_script:
3 | - export LANG=en_US.UTF-8
4 | before_install:
5 | - brew update
6 | - brew uninstall xctool
7 | - brew install xctool
8 | - gem update cocoapods
9 | script:
10 | - pod install
11 | - xctool -workspace JSONToolsTests.xcworkspace -scheme JSONToolsTests -sdk iphonesimulator test
12 |
--------------------------------------------------------------------------------
/JSONTools/JSONPatchArray.h:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchArray.h
3 | // inspired by https://github.com/Starcounter-Jack/JSON-Patch
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | #import "JSONTools.h"
11 | #import "JSONPatchInfo.h"
12 |
13 | @interface JSONPatchArray: NSObject
14 | + (id)applyPatchInfo:(JSONPatchInfo *)info object:(NSMutableArray *)object index:(NSInteger)index;
15 | @end
16 |
--------------------------------------------------------------------------------
/JSONTools/JSONPatchDictionary.h:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchDictionary.h
3 | // inspired by https://github.com/Starcounter-Jack/JSON-Patch
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | #import "JSONTools.h"
11 | #import "JSONPatchInfo.h"
12 |
13 | @interface JSONPatchDictionary: NSObject
14 | + (id)applyPatchInfo:(JSONPatchInfo *)info object:(NSMutableDictionary *)object key:(NSString *)key;
15 | @end
16 |
--------------------------------------------------------------------------------
/JSONTools/JSONSchemaValidator.h:
--------------------------------------------------------------------------------
1 | //
2 | // JSONSchemaValidator.h
3 | // JSONTools
4 | //
5 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
6 | // See LICENSE.txt for details.
7 |
8 | #import
9 |
10 | @interface JSONSchemaValidator : KiteJSONValidator
11 |
12 | /**
13 | * Whether to enable the optional 'format' validation for JSON strings (enabled by default).
14 | */
15 | @property (nonatomic,assign,getter = isFormatValidationEnabled) BOOL formatValidationEnabled;
16 |
17 | @end
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # CocoaPods
2 | #
3 | # We recommend against adding the Pods directory to your .gitignore. However
4 | # you should judge for yourself, the pros and cons are mentioned at:
5 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control?
6 | #
7 | # Pods/
8 |
9 | # Xcode
10 | .DS_Store
11 | build/*
12 | *.pbxuser
13 | !default.pbxuser
14 | *.mode1v3
15 | !default.mode1v3
16 | *.mode2v3
17 | !default.mode2v3
18 | *.perspectivev3
19 | !default.perspectivev3
20 | #*.xcworkspace
21 | !default.xcworkspace
22 | xcuserdata
23 | profile
24 | *.moved-aside
25 | .svn
26 | /Builds
27 | xcuserdata/*
28 | /DerivedData
29 | build
30 | *.orig
31 | *.xccheckout
32 |
--------------------------------------------------------------------------------
/JSONToolsTests/JSONToolsTests-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | ${EXECUTABLE_NAME}
9 | CFBundleIdentifier
10 | com.sleestacks.JSONTools.${PRODUCT_NAME:rfc1034identifier}
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundlePackageType
14 | BNDL
15 | CFBundleShortVersionString
16 | 1.0
17 | CFBundleSignature
18 | ????
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/JSONTools.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "JSONTools"
3 | s.version = "1.0.5"
4 | s.summary = "JSON Patch, JSON Pointer, and JSON Schema Validation in Objective-C"
5 | s.description = <<-DESC
6 | This Objective-C library is a collection of classes and categories that implement
7 | three powerful new features (JSON Patch, JSON Pointer, JSON Schema) that work with
8 | JSON data (represented by NSDictionaries and NSArrays in Objective-C).
9 | DESC
10 | s.homepage = "https://github.com/grgcombs/JSONTools"
11 | s.license = "MIT"
12 | s.author = { "Greg Combs" => "gcombs@gmail.com" }
13 | s.platform = :ios
14 | s.ios.deployment_target = "7.0"
15 |
16 | s.source = { :git => "https://github.com/grgcombs/JSONTools.git", :tag => "v#{s.version}" }
17 | s.source_files = "JSONTools/*.{h,m}"
18 | s.dependency "KiteJSONValidator", '~> 0.2.2'
19 |
20 | s.requires_arc = true
21 | end
22 |
--------------------------------------------------------------------------------
/JSONTools/JSONPointer.h:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPointer.h
3 | // based in part on CWJSONPointer by Jonathan Dring (MIT/Copyright (c) 2014)
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | @import Foundation;
11 | #import "NSArray+JSONPointer.h"
12 | #import "NSDictionary+JSONPointer.h"
13 |
14 | @interface JSONPointer : NSObject
15 |
16 | /**
17 | * Implements IETF RFC6901 - JSON Pointer
18 | * @see https://tools.ietf.org/html/rfc6901
19 | *
20 | * The supplied collection (dictionary or array) returns a value corresponding to
21 | * the supplied JSON Pointer reference.
22 | *
23 | * @param collection A collection (either dictionary or array).
24 | *
25 | * @param pointer A string in the form of a JSON Pointer, like "/foo/bar/0" or "#/foo"
26 | *
27 | * @return The pointer's corresponding content value (or nil) in the collection.
28 | */
29 | + (id)valueForCollection:(id)collection withJSONPointer:(NSString *)pointer;
30 |
31 | @end
32 |
--------------------------------------------------------------------------------
/JSONTools/JSONPatchInfo.h:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchInfo.h
3 | // inspired by https://github.com/Starcounter-Jack/JSON-Patch
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | @import Foundation;
11 |
12 | typedef NS_ENUM(NSInteger, JSONPatchOperation)
13 | {
14 | JSONPatchOperationUndefined = -1,
15 | JSONPatchOperationAdd,
16 | JSONPatchOperationReplace,
17 | JSONPatchOperationTest,
18 | JSONPatchOperationRemove,
19 | JSONPatchOperationMove,
20 | JSONPatchOperationCopy,
21 | JSONPatchOperationGet
22 | };
23 |
24 | @interface JSONPatchInfo : NSObject
25 | + (instancetype)newPatchInfoWithDictionary:(NSDictionary *)patch;
26 | - (NSDictionary *)dictionaryRepresentation;
27 | @property (nonatomic,copy,readonly) NSString *path;
28 | @property (nonatomic,copy,readonly) NSString *fromPath; // for move/copy ops
29 | @property (nonatomic,readonly) JSONPatchOperation op;
30 | @property (nonatomic,strong,readonly) id value;
31 | @end
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | JSONTools
3 | Copyright (C) 2014 Gregory Combs [gcombs at gmail]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/JSONTools/JSONPatch.h:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatch.h
3 | // inspired by https://github.com/Starcounter-Jack/JSON-Patch
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | #import "JSONTools.h"
11 |
12 | @interface JSONPatch : NSObject
13 |
14 | /**
15 | * Implements IETF RFC6902 - JSON Patch
16 | * @see https://tools.ietf.org/html/rfc6902
17 | *
18 | * @param patches An array of one or more patch dictionaries in the form of:
19 | * `{"op":"add", "path": "/foo/0/bar", "value": "thing"}`
20 | * - `op` is one of: "add", "remove", "copy", "move", "test", "_get"
21 | * - `path` is a JSON Pointer (RFC 6901) (see JSONPointer.h)
22 | * - `value` is an objective-c object (
23 | *
24 | * @param collection A ***mutable*** dictionary or ***mutable*** array to patch
25 | *
26 | * @return For all but "_get", the result is an NSNumber boolean indicating patch success.
27 | * However, for "_get" operations, the result will be the collection's content
28 | * corresponding to the supplied patch JSON Pointer (i.e. "path")
29 | */
30 | + (id)applyPatches:(NSArray *)patches toCollection:(id)collection;
31 |
32 | + (NSArray *)createPatchesComparingCollectionsOld:(id)oldCollection toNew:(id)newCollection;
33 |
34 | @end
35 |
--------------------------------------------------------------------------------
/JSONTools/NSDictionary+JSONPointer.m:
--------------------------------------------------------------------------------
1 | //
2 | // NSDictionary+JSONPointer.m
3 | // based in part on CWJSONPointer by Jonathan Dring (MIT/Copyright (c) 2014)
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | #import "NSDictionary+JSONPointer.h"
11 | #import "JSONPointer.h"
12 |
13 | @implementation NSDictionary (JSONPointer)
14 |
15 | - (id)valueForJSONPointer:(NSString *)pointer
16 | {
17 | return [JSONPointer valueForCollection:self withJSONPointer:pointer];
18 | }
19 |
20 | - (id)valueForJSONPointerComponent:(NSString *)component
21 | {
22 | component = [self keyForJSONPointerComponent:component];
23 | if (!component)
24 | return nil;
25 |
26 | // Section 4. If value is an object return the referenced property.
27 | return self[component];
28 | }
29 |
30 | - (NSString *)keyForJSONPointerComponent:(NSString *)component
31 | {
32 | if (!component || ![component isKindOfClass:[NSString class]])
33 | return nil;
34 |
35 | //Section 4. Transform any escaped characters, in the order ~1 then ~0.
36 | component = [component stringByReplacingOccurrencesOfString:@"~1" withString:@"/"];
37 | component = [component stringByReplacingOccurrencesOfString:@"~0" withString:@"~"];
38 | return component;
39 | }
40 |
41 | @end
42 |
--------------------------------------------------------------------------------
/JSONTools/NSArray+JSONDeepMutable.m:
--------------------------------------------------------------------------------
1 | //
2 | // NSArray+JSONDeepMutable.m
3 | // JSONTools
4 | //
5 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
6 | // See LICENSE.txt for details.
7 |
8 | #import "NSArray+JSONDeepMutable.h"
9 | #import "JSONDeeplyMutable.h"
10 |
11 | @implementation NSArray (JSONDeepMutable)
12 |
13 | - (NSMutableArray *)copyAsDeeplyMutableJSON
14 | {
15 | return [self copyAsDeeplyMutableJSONWithExceptions:YES];
16 | }
17 |
18 | - (NSMutableArray *)copyAsDeeplyMutableJSONWithExceptions:(BOOL)throwsExceptions
19 | {
20 | NSMutableArray* ret = [[NSMutableArray alloc] init];
21 | for (id oldValue in self)
22 | {
23 | id newCopy = [JSONDeepMutable copyAsDeeplyMutableValue:oldValue throwsExceptions:throwsExceptions];
24 | if (newCopy)
25 | {
26 | [ret addObject:newCopy];
27 | }
28 | }
29 | return ret;
30 | }
31 |
32 | - (NSArray *)copyAsDeeplyImmutableJSONWithExceptions:(BOOL)throwsExceptions
33 | {
34 | NSMutableArray* ret = [[NSMutableArray alloc] init];
35 | for (id oldValue in self)
36 | {
37 | id newCopy = [JSONDeepMutable copyAsDeeplyImmutableValue:oldValue throwsExceptions:throwsExceptions];
38 | if (newCopy)
39 | {
40 | [ret addObject:newCopy];
41 | }
42 | }
43 | return [ret copy];
44 | }
45 |
46 | @end
47 |
--------------------------------------------------------------------------------
/JSONTools/NSDictionary+JSONDeepMutable.m:
--------------------------------------------------------------------------------
1 | //
2 | // NSDictionary+JSONDeepMutable.m
3 | // JSONTools
4 | //
5 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
6 | // See LICENSE.txt for details.
7 |
8 | #import "NSDictionary+JSONDeepMutable.h"
9 | #import "JSONDeeplyMutable.h"
10 |
11 | @implementation NSDictionary (JSONDeepMutable)
12 |
13 | - (NSMutableDictionary *)copyAsDeeplyMutableJSON
14 | {
15 | return [self copyAsDeeplyMutableJSONWithExceptions:YES];
16 | }
17 |
18 | - (NSMutableDictionary *)copyAsDeeplyMutableJSONWithExceptions:(BOOL)throwsExceptions
19 | {
20 | NSMutableDictionary * ret = [[NSMutableDictionary alloc] init];
21 |
22 | [self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
23 | id newCopy = [JSONDeepMutable copyAsDeeplyMutableValue:obj throwsExceptions:throwsExceptions];
24 | if (newCopy)
25 | {
26 | ret[key] = newCopy;
27 | }
28 | }];
29 |
30 | return ret;
31 | }
32 |
33 | - (NSDictionary *)copyAsDeeplyImmutableJSONWithExceptions:(BOOL)throwsExceptions
34 | {
35 | NSMutableDictionary * ret = [[NSMutableDictionary alloc] init];
36 |
37 | [self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
38 | id newCopy = [JSONDeepMutable copyAsDeeplyImmutableValue:obj throwsExceptions:throwsExceptions];
39 | if (newCopy)
40 | {
41 | ret[key] = newCopy;
42 | }
43 | }];
44 |
45 | return [ret copy];
46 | }
47 |
48 | @end
49 |
--------------------------------------------------------------------------------
/JSONTools/JSONDeeplyMutable.m:
--------------------------------------------------------------------------------
1 | //
2 | // JSONDeeplyMutable.m
3 | // JSONTools
4 | //
5 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
6 | // See LICENSE.txt for details.
7 |
8 | #import "JSONDeeplyMutable.h"
9 |
10 | @implementation JSONDeepMutable
11 |
12 | + (id)copyAsDeeplyMutableValue:(id)oldValue throwsExceptions:(BOOL)throwsExceptions
13 | {
14 | id newCopy = nil;
15 |
16 | if ([oldValue respondsToSelector: @selector(copyAsDeeplyMutableJSON)])
17 | {
18 | newCopy = [oldValue copyAsDeeplyMutableJSON];
19 | }
20 | else if ([oldValue conformsToProtocol:@protocol(NSMutableCopying)])
21 | {
22 | newCopy = [oldValue mutableCopy];
23 | }
24 | else if ([oldValue conformsToProtocol:@protocol(NSCopying)])
25 | {
26 | newCopy = [oldValue copy];
27 | }
28 |
29 | if (!newCopy && throwsExceptions)
30 | {
31 | [NSException raise:NSDestinationInvalidException format:@"Object is not mutable or copyable: %@", oldValue];
32 | }
33 |
34 | return newCopy;
35 | }
36 |
37 | + (id)copyAsDeeplyImmutableValue:(id)oldValue throwsExceptions:(BOOL)throwsExceptions
38 | {
39 | id newCopy = nil;
40 |
41 | if ([oldValue respondsToSelector: @selector(copyAsDeeplyImmutableJSONWithExceptions:)])
42 | {
43 | newCopy = [oldValue copyAsDeeplyImmutableJSONWithExceptions:throwsExceptions];
44 | }
45 | else if ([oldValue conformsToProtocol:@protocol(NSCopying)])
46 | {
47 | newCopy = [oldValue copy];
48 | }
49 |
50 | if (!newCopy && throwsExceptions)
51 | {
52 | [NSException raise:NSDestinationInvalidException format:@"Object is not copyable: %@", oldValue];
53 | }
54 |
55 | return newCopy;
56 | }
57 |
58 | @end
--------------------------------------------------------------------------------
/JSONTools/NSDictionary+JSONPointer.h:
--------------------------------------------------------------------------------
1 | //
2 | // NSDictionary+JSONPointer.h
3 | // based in part on CWJSONPointer by Jonathan Dring (MIT/Copyright (c) 2014)
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | @import Foundation;
11 |
12 | @interface NSDictionary (JSONPointer)
13 |
14 | /**
15 | * Returns the receiver's value corresponding to the supplied JSON Pointer reference.
16 | * This is a convenience category for dictionaries to simplify this call:
17 | * `[JSONPointer valueForCollection:self withJSONPointer:pointer]`
18 | *
19 | * @see JSON Pointer RFC 6901 (April 2013)
20 | *
21 | * @param pointer A string in the form of a JSON Pointer, like "/foo/bar/0" or "#/foo"
22 | *
23 | * @return The pointer's corresponding content value (or nil) in the collection.
24 | */
25 | - (id)valueForJSONPointer:(NSString *)pointer;
26 |
27 | /**
28 | * Given a single JSON Pointer component, like "bar" from "/foo/bar/0", return the
29 | * the receiver's corresponding value. In general you should use the JSONPointer
30 | * class methods or valueForJSONPointer: above, as this method limits the scope.
31 | *
32 | * @param component A string in the form of a JSON Pointer component, like "bar".
33 | *
34 | * @return The pointer component's corresponding content value (or nil).
35 | */
36 | - (id)valueForJSONPointerComponent:(NSString *)component;
37 |
38 | /**
39 | * Given a single JSON Pointer component, like "a~1b", return the
40 | * component key after converting escape characters, like "a/b".
41 | *
42 | * @param component A string in the form of a JSON Pointer component.
43 | *
44 | * @return The component key after converting escape characters.
45 | */
46 | - (NSString *)keyForJSONPointerComponent:(NSString *)component;
47 |
48 | @end
49 |
--------------------------------------------------------------------------------
/JSONTools/NSArray+JSONDeepMutable.h:
--------------------------------------------------------------------------------
1 | //
2 | // NSArray+JSONDeepMutable.h
3 | // JSONTools
4 | //
5 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
6 | // See LICENSE.txt for details.
7 |
8 | @import Foundation;
9 |
10 | @interface NSArray (JSONDeepMutable)
11 |
12 | /**
13 | * Recurses into the receiver's contents and makes a (mutable) copy of each value it encounters.
14 | * Throws an exception if any interior value objects aren't copyable in some way. This method
15 | * prefers NSMutableCopying over NSCopying whenever possible.
16 | *
17 | * @return A deeply mutable copy of the receiver's contents.
18 | */
19 | - (NSMutableArray *)copyAsDeeplyMutableJSON;
20 |
21 | /**
22 | * Recurses into the receiver's contents and makes a (mutable) copy of each value it encounters.
23 | * Throws an exception if any interior value objects aren't copyable in some way. This method
24 | * prefers NSMutableCopying over NSCopying whenever possible.
25 | *
26 | * @param throwsExceptions Conditionally throw exceptions if an interior object isn't mutable or copyable,
27 | * otherwise it merely omits that object from the new collection.
28 | *
29 | * @return A deeply mutable copy of the receiver's contents.
30 | */
31 | - (NSMutableArray *)copyAsDeeplyMutableJSONWithExceptions:(BOOL)throwsExceptions;
32 |
33 | /**
34 | * Recurses into the receiver's contents and makes an immutable copy of each value it encounters.
35 | * Throws an exception if any interior value objects aren't copyable in some way. This method
36 | * only ensuring there are no mutable copies of objects.
37 | *
38 | * @param throwsExceptions Conditionally throw exceptions if an interior object isn't copyable,
39 | * otherwise it merely omits that object from the new collection.
40 | *
41 | * @return A deeply immutable copy of the receiver's contents.
42 | */
43 | - (NSArray *)copyAsDeeplyImmutableJSONWithExceptions:(BOOL)throwsExceptions;
44 |
45 | @end
46 |
--------------------------------------------------------------------------------
/JSONTools/NSDictionary+JSONDeepMutable.h:
--------------------------------------------------------------------------------
1 | //
2 | // NSDictionary+JSONDeepMutable.h
3 | // JSONTools
4 | //
5 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
6 | // See LICENSE.txt for details.
7 |
8 | @import Foundation;
9 |
10 | @interface NSDictionary (JSONDeepMutable)
11 |
12 | /**
13 | * Recurses into the receiver's contents and makes a (mutable) copy of each value it encounters.
14 | * Throws an exception if any interior value objects aren't copyable in some way. This method
15 | * prefers NSMutableCopying over NSCopying whenever possible.
16 | *
17 | * @return A deeply mutable copy of the receiver's contents.
18 | */
19 | - (NSMutableDictionary *)copyAsDeeplyMutableJSON;
20 |
21 | /**
22 | * Recurses into the receiver's contents and makes a (mutable) copy of each value it encounters.
23 | * Throws an exception if any interior value objects aren't copyable in some way. This method
24 | * prefers NSMutableCopying over NSCopying whenever possible.
25 | *
26 | * @param throwsExceptions Conditionally throw exceptions if an interior object isn't mutable or copyable,
27 | * otherwise it merely omits that object from the new collection.
28 | *
29 | * @return A deeply mutable copy of the receiver's contents.
30 | */
31 | - (NSMutableDictionary *)copyAsDeeplyMutableJSONWithExceptions:(BOOL)throwsExceptions;
32 |
33 | /**
34 | * Recurses into the receiver's contents and makes an immutable copy of each value it encounters.
35 | * Throws an exception if any interior value objects aren't copyable in some way. This method
36 | * ensures there are no mutable objects in the copy.
37 | *
38 | * @param throwsExceptions Conditionally throw exceptions if an interior object isn't mutable or copyable,
39 | * otherwise it merely omits that object from the new collection.
40 | *
41 | * @return A deeply immutable copy of the receiver's contents.
42 | */
43 | - (NSDictionary *)copyAsDeeplyImmutableJSONWithExceptions:(BOOL)throwsExceptions;
44 |
45 | @end
46 |
--------------------------------------------------------------------------------
/JSONTools/JSONDeeplyMutable.h:
--------------------------------------------------------------------------------
1 | //
2 | // JSONDeeplyMutable.h
3 | // JSONTools
4 | //
5 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
6 | // See LICENSE.txt for details.
7 |
8 | @import Foundation;
9 | #import "NSArray+JSONDeepMutable.h"
10 | #import "NSDictionary+JSONDeepMutable.h"
11 |
12 | @interface JSONDeepMutable : NSObject
13 |
14 | /**
15 | * @private
16 | * Use the NSArray+JSONDeepMutable and NSDictionary+JSONDeepMutable
17 | * categories instead. This is an internal implementation that only operates
18 | * on one instance of a container's content. Throws an exception if any
19 | * interior value objects aren't copyable in some way. This method
20 | * prefers NSMutableCopying over NSCopying whenever possible.
21 | *
22 | * @param oldValue A value object to (mutably) copy.
23 | *
24 | * @param throwsExceptions Conditionally throw exceptions if an interior
25 | * object isn't mutable or copyable, otherwise it
26 | * merely omits that object from the new collection.
27 | *
28 | * @return A (mutable) copy of the object.
29 | */
30 | + (id)copyAsDeeplyMutableValue:(id)oldValue throwsExceptions:(BOOL)throwsExceptions;
31 |
32 |
33 | /**
34 | * @private
35 | * Use the NSArray+JSONDeepMutable and NSDictionary+JSONDeepMutable
36 | * categories instead. This is an internal implementation that only operates
37 | * on one instance of a container's content. Throws an exception if any
38 | * interior value objects aren't copyable in some way. This method
39 | * will ensure there are no mutable copies, only immutable.
40 | *
41 | * @param oldValue A value object to (immutably) copy.
42 | *
43 | * @param throwsExceptions Conditionally throw exceptions if an interior
44 | * object isn't copyable, otherwise it
45 | * merely omits that object from the new collection.
46 | *
47 | * @return An immutable copy of the object.
48 | */
49 | + (id)copyAsDeeplyImmutableValue:(id)oldValue throwsExceptions:(BOOL)throwsExceptions;
50 |
51 | @end
52 |
--------------------------------------------------------------------------------
/JSONTools/NSArray+JSONPointer.m:
--------------------------------------------------------------------------------
1 | //
2 | // NSArray+JSONPointer.m
3 | // based in part on CWJSONPointer by Jonathan Dring (MIT/Copyright (c) 2014)
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | #import "NSArray+JSONPointer.h"
11 | #import "JSONPointer.h"
12 |
13 | @implementation NSArray (JSONPointer)
14 |
15 | - (id)valueForJSONPointer:(NSString *)pointer
16 | {
17 | return [JSONPointer valueForCollection:self withJSONPointer:pointer];
18 | }
19 |
20 | - (id)valueForJSONPointerComponent:(NSString *)component
21 | {
22 | NSInteger index = [self indexForJSONPointerComponent:component];
23 |
24 | if (index == NSNotFound)
25 | {
26 | return nil;
27 | }
28 |
29 | // Section 4. Valid array reference so navigate to object.
30 | return self[index];
31 | }
32 |
33 | - (NSInteger)indexForJSONPointerComponent:(NSString *)component
34 | {
35 | return [self indexForJSONPointerComponent:component allowOutOfBounds:NO];
36 | }
37 |
38 | - (NSInteger)indexForJSONPointerComponent:(NSString *)component allowOutOfBounds:(BOOL)allowOutOfBounds
39 | {
40 | if (!component || ![component isKindOfClass:[NSString class]])
41 | return NSNotFound;
42 |
43 | //Section 4. Transform any escaped characters, in the order ~1 then ~0.
44 | component = [component stringByReplacingOccurrencesOfString:@"~1" withString:@"/"];
45 | component = [component stringByReplacingOccurrencesOfString:@"~0" withString:@"~"];
46 |
47 | // Section 4. Process array objects with ABNF Rule: 0x30/(0x31-39 *(0x30-0x39))
48 |
49 | static NSCharacterSet *numberSet;
50 | static dispatch_once_t onceToken;
51 | dispatch_once(&onceToken, ^{
52 | numberSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789"];
53 | });
54 |
55 | if (![[component stringByTrimmingCharactersInSet:numberSet] isEqualToString:@""])
56 | {
57 | return NSNotFound;
58 | }
59 |
60 | // Section 4. Check for leading zero's
61 | if ([component hasPrefix:@"0"] &&
62 | [component length] > 1)
63 | {
64 | return NSNotFound;
65 | }
66 |
67 | // Section 4. Non-existant array element, terminate evaluation.
68 | if ([component isEqualToString:@"-"])
69 | {
70 | return NSNotFound;
71 | }
72 |
73 | // Avoid any out-of-bounds exceptions, if necessary
74 | NSInteger index = [component integerValue];
75 | if (!allowOutOfBounds &&
76 | self.count <= index)
77 | {
78 | return NSNotFound;
79 | }
80 |
81 | // Section 4. Valid array reference so navigate to object.
82 | return index;
83 | }
84 |
85 | @end
86 |
--------------------------------------------------------------------------------
/JSONToolsTests.xcodeproj/xcshareddata/xcschemes/JSONToolsTests.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
52 |
53 |
54 |
55 |
61 |
62 |
64 |
65 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/JSONTools/NSArray+JSONPointer.h:
--------------------------------------------------------------------------------
1 | //
2 | // NSArray+JSONPointer.h
3 | // based in part on CWJSONPointer by Jonathan Dring (MIT/Copyright (c) 2014)
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | @import Foundation;
11 |
12 | @interface NSArray (JSONPointer)
13 |
14 | /**
15 | * Returns the receiver's value corresponding to the supplied JSON Pointer reference.
16 | * This is a convenience category for arrays to simplify this call:
17 | * `[JSONPointer valueForCollection:self withJSONPointer:pointer]`
18 | *
19 | * @see JSON Pointer RFC 6901 (April 2013)
20 | *
21 | * @param pointer A string in the form of a JSON Pointer, like "/foo/bar/0" or "#/foo"
22 | *
23 | * @return The pointer's corresponding content value (or nil) in the collection.
24 | */
25 | - (id)valueForJSONPointer:(NSString *)pointer;
26 |
27 | /**
28 | * Given a single JSON Pointer component, like "12" from "/foo/bar/12", return the
29 | * the receiver's corresponding value. In general you should use the JSONPointer
30 | * class methods or valueForJSONPointer: above, as this method limits the scope.
31 | *
32 | * @param component A string in the form of a JSON Pointer component, like "12".
33 | *
34 | * @return The pointer component's corresponding content value (or nil).
35 | */
36 | - (id)valueForJSONPointerComponent:(NSString *)component;
37 |
38 | /**
39 | * Given a single JSON Pointer component, like "bar" or "0" from "/foo/bar/0", return the
40 | * the receiver's corresponding array index. In general you should use the JSONPointer
41 | * class methods instead, as this method is limited in the pointer's scope.
42 | *
43 | * @param component A string in the form of a JSON Pointer component, like "bar" or "0".
44 | *
45 | * @return The pointer component's corresponding array index, or NSNotFound.
46 | */
47 | - (NSInteger)indexForJSONPointerComponent:(NSString *)component;
48 |
49 | /**
50 | * Given a single JSON Pointer component, like "bar" or "0" from "/foo/bar/0", return the
51 | * the receiver's corresponding array index. In general you should use the JSONPointer
52 | * class methods instead, as this method is limited in the pointer's scope.
53 | *
54 | * @param component A string in the form of a JSON Pointer component, like "bar" or "0".
55 | * @param allowOutOfBounds A boolean indicating whether to permit out-of-bounds array indexes.
56 | * Such indexes should be treated with care, as they are not "navigable" in the array and
57 | * will trigger exceptions. They are are useful when used in conjunction with JSON Patch, however.
58 | *
59 | * @return The pointer component's corresponding array index, or NSNotFound.
60 | */
61 | - (NSInteger)indexForJSONPointerComponent:(NSString *)component allowOutOfBounds:(BOOL)allowOutOfBounds;
62 |
63 | @end
64 |
--------------------------------------------------------------------------------
/JSONTools/JSONPointer.m:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPointer.m
3 | // based in part on CWJSONPointer by Jonathan Dring (MIT/Copyright (c) 2014)
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | #import "JSONPointer.h"
11 |
12 | @implementation JSONPointer
13 |
14 | + (id)valueForCollection:(id)collection withJSONPointer:(NSString *)pointer
15 | {
16 | if (!pointer || ![pointer isKindOfClass:[NSString class]])
17 | {
18 | return nil;
19 | }
20 |
21 | // JSON Pointer RFC 6901 (April 2013)
22 |
23 | // Section 6: URI fragment evaluation: if a fragment, remove the fragment marker and de-escape.
24 | pointer = [self trimAndUnescapeJSONPointerFragment:pointer];
25 |
26 | // Section 5. Blank Pointer evaluates to complete JSON document.
27 | if ([pointer isEqualToString:@""])
28 | {
29 | return collection;
30 | }
31 |
32 | // Section 3. Token without leading '/' is illegal, terminate evaluation
33 | if (![pointer hasPrefix:@"/"])
34 | {
35 | return nil;
36 | }
37 |
38 | // Section 3. Legal leading '/', strip and continue;
39 | pointer = [pointer substringFromIndex:1];
40 |
41 |
42 | // Section 3. Check for valid character ranges upper and lower limits.
43 | if ([self pointerHasInvalidCharacters:pointer])
44 | {
45 | return nil;
46 | }
47 |
48 | // Section 4. Evaluate the tokens one by one starting with the root.
49 | id object = collection;
50 | NSArray *pointerComponents = [pointer componentsSeparatedByString:@"/"];
51 | for (NSString *component in pointerComponents)
52 | {
53 | object = [self valueForCollection:object withJSONPointerComponent:component];
54 | if (!object || [object isEqual:[NSNull null]])
55 | {
56 | return nil;
57 | }
58 | }
59 | return object;
60 | }
61 |
62 | + (NSString *)trimAndUnescapeJSONPointerFragment:(NSString *)pointer
63 | {
64 | if ([pointer hasPrefix:@"#"])
65 | {
66 | pointer = [pointer substringFromIndex:1];
67 | pointer = [pointer stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
68 | }
69 | return pointer;
70 | }
71 |
72 | + (BOOL)pointerHasInvalidCharacters:(NSString *)pointer
73 | {
74 | static NSCharacterSet *illegalChars;
75 | static dispatch_once_t onceToken;
76 | dispatch_once(&onceToken, ^{
77 | illegalChars = [[NSCharacterSet characterSetWithRange:NSMakeRange(0x0000, 0x10FFFF)] invertedSet];
78 | });
79 |
80 | return ([pointer rangeOfCharacterFromSet:illegalChars].location != NSNotFound);
81 | }
82 |
83 | + (id)valueForCollection:(id)collection withJSONPointerComponent:(NSString *)component
84 | {
85 | if (collection == nil || collection == [NSNull null])
86 | {
87 | // If the object is nil or null, terminate evaluation.
88 | return nil;
89 | }
90 |
91 | if ([collection isKindOfClass:[NSDictionary class]])
92 | {
93 | return [(NSDictionary *)collection valueForJSONPointerComponent:component];
94 | }
95 |
96 | if ([collection isKindOfClass:[NSArray class]])
97 | {
98 | return [(NSArray *)collection valueForJSONPointerComponent:component];
99 | }
100 |
101 | // Unspecified object type, terminate evaluation.
102 | return nil;
103 | }
104 |
105 | @end
106 |
--------------------------------------------------------------------------------
/JSONTools/JSONPatchDictionary.m:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchDictionary.m
3 | // inspired by https://github.com/Starcounter-Jack/JSON-Patch
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | #import "JSONPatchDictionary.h"
11 | #import "JSONPointer.h"
12 |
13 | @implementation JSONPatchDictionary
14 |
15 | + (id)applyPatchInfo:(JSONPatchInfo *)info object:(NSMutableDictionary *)object key:(NSString *)key
16 | {
17 | BOOL success = NO;
18 | switch (info.op)
19 | {
20 | case JSONPatchOperationGet:
21 | return [self getValueForObject:object key:key];
22 | break;
23 | case JSONPatchOperationTest:
24 | return @([self test:object key:key value:info.value]);
25 | break;
26 | case JSONPatchOperationAdd:
27 | success = [self addObject:object key:key value:info.value];
28 | break;
29 | case JSONPatchOperationReplace:
30 | success = [self replaceObject:object key:key value:info.value];
31 | break;
32 | case JSONPatchOperationRemove:
33 | success = [self removeObject:object key:key];
34 | break;
35 | case JSONPatchOperationMove:
36 | case JSONPatchOperationCopy:
37 | case JSONPatchOperationUndefined:
38 | // These are handled in the main patch loop
39 | break;
40 | }
41 | if (!success)
42 | {
43 | return nil;
44 | }
45 | return @(success);
46 | }
47 |
48 | + (BOOL)isDictionary:(NSDictionary *)dictionary andString:(NSString *)string
49 | {
50 | if (!string ||
51 | ![string isKindOfClass:[NSString class]] ||
52 | !dictionary ||
53 | ![dictionary isKindOfClass:[NSDictionary class]])
54 | {
55 | return NO;
56 | }
57 | return YES;
58 | }
59 |
60 | + (BOOL)test:(NSDictionary *)object key:(NSString *)key value:(id)value
61 | {
62 | if (![self isDictionary:object andString:key])
63 | {
64 | return NO;
65 | }
66 | id foundValue = object[key];
67 | return (foundValue == value || [foundValue isEqual:value]);
68 | }
69 |
70 | + (id)getValueForObject:(NSDictionary *)object key:(NSString *)key
71 | {
72 | if (![self isDictionary:object andString:key])
73 | {
74 | return nil;
75 | }
76 | // this.value = object[key] ????
77 | return object[key];
78 | }
79 |
80 | + (BOOL)addObject:(NSMutableDictionary *)object key:(NSString *)key value:(id)value
81 | {
82 | if (![self isDictionary:object andString:key] ||
83 | !value)
84 | {
85 | return NO;
86 | }
87 | object[key] = value;
88 | return YES;
89 | }
90 |
91 | + (BOOL)removeObject:(NSMutableDictionary *)object key:(NSString *)key
92 | {
93 | if (![self isDictionary:object andString:key] ||
94 | !object[key])
95 | {
96 | return NO;
97 | }
98 | [object removeObjectForKey:key];
99 | return YES;
100 | }
101 |
102 | + (BOOL)replaceObject:(NSMutableDictionary *)object key:(NSString *)key value:(id)value
103 | {
104 | if (![self isDictionary:object andString:key] ||
105 | !object[key])
106 | {
107 | return NO;
108 | }
109 | if (!value)
110 | {
111 | return [self removeObject:object key:key];
112 | }
113 | return [self addObject:object key:key value:value];
114 | }
115 |
116 | @end
117 |
--------------------------------------------------------------------------------
/JSONTools/JSONPatchArray.m:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchArray.m
3 | // inspired by https://github.com/Starcounter-Jack/JSON-Patch
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | #import "JSONPatchArray.h"
11 | #import "NSArray+JSONPointer.h"
12 |
13 | @implementation JSONPatchArray
14 |
15 | + (id)applyPatchInfo:(JSONPatchInfo *)info object:(NSMutableArray *)object index:(NSInteger)index
16 | {
17 | BOOL success = NO;
18 | switch (info.op)
19 | {
20 | case JSONPatchOperationGet:
21 | return [self getValueForObject:object index:index];
22 | break;
23 | case JSONPatchOperationTest:
24 | return @([self test:object index:index value:info.value]);
25 | break;
26 | case JSONPatchOperationAdd:
27 | success = [self addObject:object index:index value:info.value];
28 | break;
29 | case JSONPatchOperationReplace:
30 | success = [self replaceObject:object index:index value:info.value];
31 | break;
32 | case JSONPatchOperationRemove:
33 | success = [self removeObject:object index:index];
34 | break;
35 | case JSONPatchOperationMove:
36 | case JSONPatchOperationCopy:
37 | case JSONPatchOperationUndefined:
38 | // These are handled in the main patch loop
39 | break;
40 | }
41 | if (!success)
42 | {
43 | return nil;
44 | }
45 | return @(success);
46 | }
47 |
48 | + (BOOL)isArray:(NSArray *)array andIndex:(NSInteger)index inclusive:(BOOL)isInclusive
49 | {
50 | if (!array ||
51 | ![array isKindOfClass:[NSArray class]])
52 | {
53 | return NO;
54 | }
55 | if (isInclusive) {
56 | return (array.count >= index);
57 | }
58 | return (array.count > index);
59 | }
60 |
61 | + (BOOL)test:(NSArray *)object index:(NSInteger)index value:(id)value
62 | {
63 | if (![self isArray:object andIndex:index inclusive:NO])
64 | {
65 | return NO;
66 | }
67 | id foundValue = object[index];
68 | return (foundValue == value || [foundValue isEqual:value]);
69 | }
70 |
71 | + (id)getValueForObject:(NSArray *)object index:(NSInteger)index
72 | {
73 | if (![self isArray:object andIndex:index inclusive:NO])
74 | {
75 | return nil;
76 | }
77 | return object[index];
78 | }
79 |
80 | + (BOOL)addObject:(NSMutableArray *)object index:(NSInteger)index value:(id)value
81 | {
82 | if (![self isArray:object andIndex:index inclusive:YES] ||
83 | !value)
84 | {
85 | return NO;
86 | }
87 | [object insertObject:value atIndex:index];
88 | return YES;
89 | }
90 |
91 | + (BOOL)removeObject:(NSMutableArray *)object index:(NSInteger)index
92 | {
93 | if (![self isArray:object andIndex:index inclusive:NO])
94 | {
95 | return NO;
96 | }
97 | [object removeObjectAtIndex:index];
98 | return YES;
99 | }
100 |
101 | + (BOOL)replaceObject:(NSMutableArray *)object index:(NSInteger)index value:(id)value
102 | {
103 | if (![self isArray:object andIndex:index inclusive:NO])
104 | {
105 | return NO;
106 | }
107 | if (!value) // replacing an array's item with nothing is the same as deleting it
108 | {
109 | return [self removeObject:object index:index];
110 | }
111 |
112 | [object replaceObjectAtIndex:index withObject:value];
113 | return YES;
114 | }
115 |
116 | @end
117 |
--------------------------------------------------------------------------------
/JSONToolsTests/JSONPatchGenerateTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchGenerateTests.m
3 | // inspired by https://github.com/Starcounter-Jack/JSON-Patch
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | @import XCTest;
11 | #import "JSONPatch.h"
12 | #import "JSONDeeplyMutable.h"
13 |
14 | @interface JSONPatchGenerateTests : XCTestCase
15 |
16 | @end
17 |
18 | @implementation JSONPatchGenerateTests
19 |
20 | - (void)setUp
21 | {
22 | [super setUp];
23 | }
24 |
25 | - (void)tearDown
26 | {
27 | [super tearDown];
28 | }
29 |
30 | - (void)testShouldGenerateAdd
31 | {
32 | NSDictionary *objA = @{@"user": @{@"firstName": @"Albert"}};
33 | NSDictionary *objB = @{@"user": @{@"firstName": @"Albert",
34 | @"lastName": @"Einstein"}};
35 |
36 | NSArray *patches = [JSONPatch createPatchesComparingCollectionsOld:objA toNew:objB];
37 | NSArray *expected = @[@{@"op": @"add",
38 | @"path": @"/user/lastName",
39 | @"value": @"Einstein"}];
40 | XCTAssertEqualObjects(patches, expected, @"Failed to generate add patch, expected %@, found %@", expected, patches);
41 | }
42 |
43 | - (void)testShouldGenerateReplace
44 | {
45 | NSDictionary *objA = @{@"firstName": @"Albert",
46 | @"lastName": @"Einstein"};
47 |
48 | NSDictionary *objB = @{@"firstName": @"Albert",
49 | @"lastName": @"Statham"};
50 |
51 | NSArray *expected = @[@{@"op": @"replace",
52 | @"path": @"/lastName",
53 | @"value": @"Statham"}];
54 | NSArray *patches = [JSONPatch createPatchesComparingCollectionsOld:objA toNew:objB];
55 | XCTAssertEqualObjects(patches, expected, @"Failed to generate replace patch, expected %@, found %@", expected, patches);
56 | }
57 |
58 | - (void)testShouldGenerateReplaceAndAdd
59 | {
60 | NSMutableDictionary *objA = [@{@"firstName": @"Albert",
61 | @"lastName": @"Einstein",
62 | @"phoneNumbers": @[@{@"phone": @"123-4444"}]} copyAsDeeplyMutableJSON];
63 |
64 | NSMutableDictionary *objB = [@{@"firstName": @"Albert",
65 | @"lastName": @"Statham",
66 | @"phoneNumbers": @[@{@"phone": @"123-4453"},
67 | @{@"cell": @"456-3533"}]} copyAsDeeplyMutableJSON];
68 |
69 | NSArray *patches = [JSONPatch createPatchesComparingCollectionsOld:objA toNew:objB];
70 | XCTAssertTrue(patches.count == 3, @"Failed to generate a composite replace/add patch: %@", patches);
71 |
72 | NSNumber *result = [JSONPatch applyPatches:patches toCollection:objA];
73 | XCTAssertNotNil(result, @"Patch apply results should not be nil");
74 | XCTAssertTrue([result boolValue], @"Failed to apply the replace/add patch: %@", patches);
75 | XCTAssertEqualObjects(objA, objB, @"The patched collection should equal that of the opposing collection");
76 | }
77 |
78 | - (void)testShouldGenerateRemove
79 | {
80 | NSDictionary *objA = @{@"firstName": @"Albert",
81 | @"lastName": @"Einstein"};
82 |
83 | NSDictionary *objB = @{@"firstName": @"Albert"};
84 |
85 | NSArray *expected = @[@{@"op": @"remove",
86 | @"path": @"/lastName"}];
87 |
88 | NSArray *patches = [JSONPatch createPatchesComparingCollectionsOld:objA toNew:objB];
89 | XCTAssertEqualObjects(patches, expected, @"Failed to generate remove patch, expected %@, found %@", expected, patches);
90 | }
91 |
92 | @end
93 |
--------------------------------------------------------------------------------
/JSONTools/JSONPatchInfo.m:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchInfo.m
3 | // inspired by https://github.com/Starcounter-Jack/JSON-Patch
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | #import "JSONPatchInfo.h"
11 |
12 | @interface JSONPatchInfo ()
13 | @end
14 |
15 | @implementation JSONPatchInfo
16 |
17 | + (instancetype)newPatchInfoWithDictionary:(NSDictionary *)patch
18 | {
19 | if (!patch ||
20 | ![patch isKindOfClass:[NSDictionary class]])
21 | {
22 | return nil;
23 | }
24 |
25 | JSONPatchOperation patchOp = [self operationForToken:patch[@"op"]];
26 | if (patchOp == JSONPatchOperationUndefined)
27 | {
28 | return nil;
29 | }
30 |
31 | JSONPatchInfo *info = [[JSONPatchInfo alloc] init];
32 | info->_op = patchOp;
33 |
34 | NSString *token = @"path";
35 | if ([self operation:patchOp needsToken:token])
36 | {
37 | NSString *patchPath = patch[token];
38 | if (!patchPath ||
39 | ![patchPath isKindOfClass:[NSString class]])
40 | {
41 | return nil;
42 | }
43 | info->_path = [patchPath copy];
44 | }
45 |
46 | token = @"from";
47 | if ([self operation:patchOp needsToken:token])
48 | {
49 | NSString *fromPath = patch[token];
50 | if (!fromPath ||
51 | ![fromPath isKindOfClass:[NSString class]])
52 | {
53 | return nil;
54 | }
55 | info->_fromPath = fromPath;
56 | }
57 |
58 | token = @"value";
59 | if ([self operation:patchOp needsToken:token])
60 | {
61 | id patchValue = patch[token];
62 | if (!patchValue)
63 | return nil;
64 | info->_value = patchValue;
65 | }
66 |
67 | return info;
68 | }
69 |
70 | + (BOOL)operation:(JSONPatchOperation)op needsToken:(NSString *)token
71 | {
72 | if ([token isEqualToString:@"path"] ||
73 | [token isEqualToString:@"op"])
74 | {
75 | return (op != JSONPatchOperationUndefined);
76 | }
77 |
78 | if ([token isEqualToString:@"from"])
79 | {
80 | return (op == JSONPatchOperationMove ||
81 | op == JSONPatchOperationCopy);
82 | }
83 |
84 | if ([token isEqualToString:@"value"])
85 | {
86 | return (op == JSONPatchOperationAdd ||
87 | op == JSONPatchOperationReplace ||
88 | op == JSONPatchOperationTest);
89 | }
90 |
91 | return NO;
92 | }
93 |
94 | + (JSONPatchOperation)operationForToken:(NSString *)token
95 | {
96 | if (!token || ![token isKindOfClass:[NSString class]])
97 | return JSONPatchOperationUndefined;
98 | if ([token caseInsensitiveCompare:@"add"] == NSOrderedSame)
99 | return JSONPatchOperationAdd;
100 | if ([token caseInsensitiveCompare:@"replace"] == NSOrderedSame)
101 | return JSONPatchOperationReplace;
102 | if ([token caseInsensitiveCompare:@"test"] == NSOrderedSame)
103 | return JSONPatchOperationTest;
104 | if ([token caseInsensitiveCompare:@"remove"] == NSOrderedSame)
105 | return JSONPatchOperationRemove;
106 | if ([token caseInsensitiveCompare:@"move"] == NSOrderedSame)
107 | return JSONPatchOperationMove;
108 | if ([token caseInsensitiveCompare:@"copy"] == NSOrderedSame)
109 | return JSONPatchOperationCopy;
110 | if ([token caseInsensitiveCompare:@"_get"] == NSOrderedSame)
111 | return JSONPatchOperationGet;
112 | return JSONPatchOperationUndefined;
113 | }
114 |
115 | + (NSString *)tokenForOperation:(JSONPatchOperation)operation
116 | {
117 | NSString *token = nil;
118 |
119 | switch (operation) {
120 | case JSONPatchOperationAdd:
121 | token = @"add";
122 | break;
123 | case JSONPatchOperationReplace:
124 | token = @"replace";
125 | break;
126 | case JSONPatchOperationTest:
127 | token = @"test";
128 | break;
129 | case JSONPatchOperationRemove:
130 | token = @"remove";
131 | break;
132 | case JSONPatchOperationMove:
133 | token = @"move";
134 | break;
135 | case JSONPatchOperationCopy:
136 | token = @"copy";
137 | break;
138 | case JSONPatchOperationGet:
139 | token = @"_get";
140 | break;
141 | case JSONPatchOperationUndefined:
142 | token = @"undefined";
143 | break;
144 | }
145 | return token;
146 | }
147 |
148 | - (NSDictionary *)dictionaryRepresentation
149 | {
150 | NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
151 |
152 | NSString *token = [JSONPatchInfo tokenForOperation:self.op];
153 | if (token)
154 | dict[@"op"] = token;
155 | NSString *path = self.path;
156 | if (path)
157 | dict[@"path"] = path;
158 | NSString *from = self.fromPath;
159 | if (from)
160 | dict[@"from"] = from;
161 | id value = self.value;
162 | if (value)
163 | dict[@"value"] = value;
164 | return [dict copy];
165 | }
166 |
167 | - (NSString *)description
168 | {
169 | return [NSString stringWithFormat:@"%@: op=%@; path=%@; from=%@; value=%@",
170 | [super description],
171 | [[self class] tokenForOperation:_op],
172 | _path,
173 | _fromPath,
174 | _value];
175 | }
176 |
177 | - (BOOL)isEqual:(id)obj
178 | {
179 | if (![obj isKindOfClass:[JSONPatchInfo class]])
180 | return NO;
181 |
182 | JSONPatchInfo *other = (JSONPatchInfo *)obj;
183 | BOOL equalOps = (_op == other->_op);
184 | BOOL equalPaths = (_path == other->_path || [_path isEqual:other->_path]);
185 | BOOL equalValues = (_value == other->_value || [_value isEqual:other->_value]);
186 | BOOL equalFroms = (_fromPath == other->_fromPath || [_fromPath isEqual:other->_fromPath]);
187 |
188 | return (equalOps &&
189 | equalPaths &&
190 | equalValues &&
191 | equalFroms);
192 | }
193 |
194 | #define NSUINT_BIT (CHAR_BIT * sizeof(NSUInteger))
195 | #define NSUINTROTATE(val, howmuch) ((((NSUInteger)val) << howmuch) | (((NSUInteger)val) >> (NSUINT_BIT - howmuch)))
196 |
197 | - (NSUInteger)hash
198 | {
199 | NSUInteger current = 31;
200 | current = [self hashForComponentOrNil:_op index:1] ^ current;
201 | current = [self hashForComponentOrNil:[_path hash] index:2] ^ current;
202 | current = [self hashForComponentOrNil:[_value hash] index:3] ^ current;
203 | current = [self hashForComponentOrNil:[_fromPath hash] index:4] ^ current;
204 | return current;
205 | }
206 |
207 | - (NSUInteger)hashForComponentOrNil:(NSUInteger)hash index:(NSUInteger)hashIndex
208 | {
209 | if (hash == 0)
210 | {
211 | // accounts for nil objects
212 | hash = 31;
213 | }
214 | return NSUINTROTATE(hash, NSUINT_BIT / (hashIndex + 1));
215 | }
216 |
217 | @end
218 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | JSON Tools (Objective-C)
2 | =========
3 | by [Gregory Combs](https://github.com/grgcombs)
4 | (MIT License - 2014)
5 |
6 | [](https://travis-ci.org/grgcombs/JSONTools)
7 |
8 | JSON Patch, JSON Pointer, and JSON Schema Validation in Objective-C
9 |
10 | This Objective-C library is a collection of classes and categories that implement three powerful new features (JSON Patch, JSON Pointer, JSON Schema) that work with JSON data (represented by NSDictionaries and NSArrays in Objective-C). Unit tests are included for each component.
11 |
12 | ## To Run the Tests
13 |
14 | To build the test project, be sure to do the following:
15 |
16 | 1. Install CocoaPods (use homebrew) if you haven't already.
17 | 2. Run `pod install` from the command line.
18 | 3. Open the newly created **JSONToolsTests.xcworkspace** document ***not*** the JSONToolsTests.xcodeproj document.
19 | 4. Hit Command-U to run the tests.
20 |
21 | ## Features
22 |
23 | - [JSON Patch](https://tools.ietf.org/html/rfc6902) - IETF RFC6902: Create and apply operation patches (add, remove, copy, move, test, _get) to serially transform JSON Data. ***This functionality was inspired by [Joachim Wester's](https://github.com/Starcounter-Jack) [JavaScript implementation of JSON Patch](https://github.com/Starcounter-Jack/JSON-Patch).***
24 | - Example Patch Copy:
25 |
26 | ```objc
27 |
28 | #import "JSONTools.h"
29 | #import "JSONDeeplyMutable.h"
30 |
31 | - (void)examplePatchCopy
32 | {
33 | NSMutableDictionary *obj = nil;
34 | NSMutableDictionary *expected = nil;
35 | NSDictionary *patch = nil;
36 |
37 | obj = [@{@"foo": @1,
38 | @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON];
39 |
40 | patch = @{@"op": @"copy",
41 | @"from": @"/foo",
42 | @"path": @"/bar"};
43 |
44 | [JSONPatch applyPatches:@[patch] toCollection:obj];
45 |
46 | expected = [@{@"foo": @1,
47 | @"baz": @[@{@"qux": @"hello"}],
48 | @"bar": @1} copyAsDeeplyMutableJSON];
49 | }
50 |
51 | ```
52 |
53 | - Example Patch Generation (JSON Diff):
54 |
55 | ```objc
56 |
57 | #import "JSONTools.h"
58 |
59 | - (void)examplePatchGeneration
60 | {
61 | NSDictionary *objA = nil;
62 | NSDictionary *objB = nil;
63 | NSArray *patches = nil;
64 | NSArray *expected = nil;
65 |
66 | objA = @{@"user": @{@"firstName": @"Albert",
67 | @"lastName": @"Einstein"}};
68 |
69 | objB = @{@"user": @{@"firstName": @"Albert"}};
70 |
71 | patches = [JSONPatch createPatchesComparingCollectionsOld:objA toNew:objB];
72 |
73 | expected = @[@{@"op": @"remove",
74 | @"path": @"/user/lastName"}];
75 |
76 | }
77 |
78 | ```
79 |
80 |
81 | - [JSON Pointer](https://tools.ietf.org/html/rfc6901) - IETF RFC6901: Reference and access values and objects within a hierarchical JSON structure using a concise path pattern notation. ***This functionality is based on [Jonathan Dring's](https://github.com/C-Works) [NSDictionary-CWJSONPointer](https://github.com/C-Works/NSDictionary-CWJSONPointer).***
82 | - Example:
83 |
84 | ```objc
85 |
86 | #import "JSONTools.h"
87 |
88 | - (void)exampleJSONPointer
89 | {
90 | NSDictionary *obj = @{
91 | @"data": @{
92 | @"foo": @[@"bar", @"baz"],
93 | @"bork": @{
94 | @"crud": @"stuff",
95 | @"guts": @"and things"
96 | }
97 | }
98 | };
99 |
100 | NSString *result1 = [_obj valueForJSONPointer: @"/data/foo/1" ];
101 | // Yields -> "baz"
102 |
103 | NSString *result2 = [_obj valueForJSONPointer: @"/data/bork/guts"];
104 | // Yields -> "and things"
105 |
106 | NSDictionary *result3 = [_obj valueForJSONPointer: @"/data/bork"];
107 | // Yields -> {"crud": "stuff","guts": "and things"}
108 | }
109 |
110 | ```
111 |
112 | - [JSON Schema](http://tools.ietf.org/html/draft-zyp-json-schema-04) with [Validation](http://tools.ietf.org/html/draft-fge-json-schema-validation-00) - IETF Draft v4, 2013. ***This functionality is based on [Sam Duke's](https://github.com/samskiter) [KiteJSONValidator](https://github.com/samskiter/KiteJSONValidator)*** but adds additional validations and tests for the JSON-Schema `format` parameter.
113 | - Example #1:
114 |
115 | ```json
116 |
117 | {
118 | "schema": {
119 | "type": "array",
120 | "items": { "$ref": "#/definitions/positiveInteger" },
121 | "definitions": {
122 | "positiveInteger": {
123 | "type": "integer",
124 | "minimum": 0,
125 | "exclusiveMinimum": true
126 | }
127 | }
128 | },
129 | "validData": [0, 1, 2, 3, 4, 5],
130 | "invalidData": [-12, "Abysmal", null, -141]
131 | }
132 | ```
133 |
134 | ```objc
135 |
136 | /*
137 | Assuming that variables are assigned using JSON above:
138 | schema is an NSDictionary
139 | validData and invalidData are NSArrays
140 | */
141 |
142 | BOOL success = NO;
143 | JSONSchemaValidator *validator = [JSONSchemaValidator new];
144 |
145 | success = [validator validateJSONInstance:validData withSchema:schema];
146 | // success == YES, All validData values are positive integers.
147 |
148 | success = [validator validateJSONInstance:invalidData withSchema:schema];
149 | // success == NO, invalidData array isn't comprised of positive integers.
150 |
151 | ```
152 |
153 |
154 | - Example #2:
155 |
156 | ```objc
157 |
158 | NSDictionary *schema = nil;
159 | id testData = nil;
160 | BOOL success = NO;
161 |
162 | JSONSchemaValidator *validator = [JSONSchemaValidator new];
163 | validator.formatValidationEnabled = YES;
164 |
165 | schema = @{@"format": @"date-time"};
166 | testData = @"2000-02-29T08:30:06.283185Z";
167 | success = [validator validateJSONInstance:testData withSchema:schema];
168 | // success == YES, February 2000 had 29 days.
169 |
170 | schema = @{@"format": @"ipv6"};
171 | testData = @"12345::";
172 | success = [validator validateJSONInstance:testData withSchema:schema];
173 | // success == NO, the IPv6 address has out-of-range values.
174 |
175 | ```
176 |
177 |
--------------------------------------------------------------------------------
/JSONToolsTests/JSONSchemaTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // JSONSchemaTests.m
3 | // JSONTools
4 | //
5 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
6 | // See LICENSE.txt for details.
7 |
8 | @import XCTest;
9 | #import "JSONSchemaValidator.h"
10 |
11 | @interface JSONSchemaTests : XCTestCase
12 | @property (nonatomic,strong) JSONSchemaValidator *validator;
13 | @property (nonatomic,strong) NSURL *draft4SpecDirectory;
14 | @end
15 |
16 | @implementation JSONSchemaTests
17 |
18 | - (void)setUp
19 | {
20 | [super setUp];
21 |
22 | _validator = [JSONSchemaValidator new];
23 |
24 | NSBundle *mainBundle = [NSBundle bundleForClass:[self class]];
25 | NSString *bundlePath = [mainBundle pathForResource:@"JSON-Schema-Test-Suite" ofType:@"bundle"];
26 | NSBundle *suiteBundle = [NSBundle bundleWithPath:bundlePath];
27 | _draft4SpecDirectory = [[suiteBundle resourceURL] URLByAppendingPathComponent:@"tests/draft4" isDirectory:YES];
28 |
29 | NSString * directory = [[suiteBundle resourcePath] stringByAppendingPathComponent:@"remotes"];
30 | NSArray * refPaths = [self recursivePathsForResourcesOfType:@"json" inDirectory:directory];
31 | for (NSString * path in refPaths)
32 | {
33 | NSString * fullpath = [directory stringByAppendingPathComponent:path];
34 | NSData * data = [NSData dataWithContentsOfFile:fullpath];
35 | NSURL * url = [NSURL URLWithString:@"http://localhost:1234/"];
36 | url = [NSURL URLWithString:path relativeToURL:url];
37 |
38 | NSError *error = nil;
39 | id schema = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
40 | XCTAssertNil(error, @"Should have a valid JSON object for remote %@", path);
41 | XCTAssertTrue([schema isKindOfClass:[NSDictionary class]], @"JSON object should be a dictionary for remote %@, was %@", path, schema);
42 |
43 | BOOL success = [_validator addRefSchema:schema atURL:url validateSchema:YES];
44 | XCTAssertTrue(success, @"JSON object should be a valid JSON Schema for remote %@", path);
45 | }
46 | }
47 |
48 | - (NSArray *)recursivePathsForResourcesOfType:(NSString *)type inDirectory:(NSString *)directoryPath {
49 | NSMutableArray *filePaths = [[NSMutableArray alloc] init];
50 | NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:directoryPath];
51 |
52 | NSString *filePath;
53 |
54 | while ((filePath = [enumerator nextObject]) != nil) {
55 | if (!type || [[filePath pathExtension] isEqualToString:type]){
56 | [filePaths addObject:filePath];
57 | }
58 | }
59 |
60 | return filePaths;
61 | }
62 |
63 | - (void)tearDown
64 | {
65 | // Put teardown code here. This method is called after the invocation of each test method in the class.
66 | [super tearDown];
67 | }
68 |
69 |
70 | - (void)testAdditionalItems
71 | {
72 | [self runSpecGroupWithName:@"additionalItems"];
73 | }
74 |
75 | - (void)testAdditionalProperties
76 | {
77 | [self runSpecGroupWithName:@"additionalProperties"];
78 | }
79 |
80 | - (void)testAllOf
81 | {
82 | [self runSpecGroupWithName:@"allOf"];
83 | }
84 |
85 | - (void)testAnyOf
86 | {
87 | [self runSpecGroupWithName:@"anyOf"];
88 | }
89 |
90 | - (void)testDefinitions
91 | {
92 | [self runSpecGroupWithName:@"definitions"];
93 | }
94 |
95 | - (void)testDependencies
96 | {
97 | [self runSpecGroupWithName:@"dependencies"];
98 | }
99 |
100 | - (void)testEnum
101 | {
102 | [self runSpecGroupWithName:@"enum"];
103 | }
104 |
105 | - (void)testItems
106 | {
107 | [self runSpecGroupWithName:@"items"];
108 | }
109 |
110 | - (void)testMaximum
111 | {
112 | [self runSpecGroupWithName:@"maximum"];
113 | }
114 |
115 | - (void)testMaxItems
116 | {
117 | [self runSpecGroupWithName:@"maxItems"];
118 | }
119 |
120 | - (void)testMaxLength
121 | {
122 | [self runSpecGroupWithName:@"maxLength"];
123 | }
124 |
125 | - (void)testMaxProperties
126 | {
127 | [self runSpecGroupWithName:@"maxProperties"];
128 | }
129 |
130 | - (void)testMinimum
131 | {
132 | [self runSpecGroupWithName:@"minimum"];
133 | }
134 |
135 | - (void)testMinItems
136 | {
137 | [self runSpecGroupWithName:@"minItems"];
138 | }
139 |
140 | - (void)testMinLength
141 | {
142 | [self runSpecGroupWithName:@"minLength"];
143 | }
144 |
145 | - (void)testMinProperties
146 | {
147 | [self runSpecGroupWithName:@"minProperties"];
148 | }
149 |
150 | - (void)testMultipleOf
151 | {
152 | [self runSpecGroupWithName:@"multipleOf"];
153 | }
154 |
155 | - (void)testNot
156 | {
157 | [self runSpecGroupWithName:@"not"];
158 | }
159 |
160 | - (void)testOneOf
161 | {
162 | [self runSpecGroupWithName:@"oneOf"];
163 | }
164 |
165 | - (void)testPattern
166 | {
167 | [self runSpecGroupWithName:@"pattern"];
168 | }
169 |
170 | - (void)testPatternProperties
171 | {
172 | [self runSpecGroupWithName:@"patternProperties"];
173 | }
174 |
175 | - (void)testProperties
176 | {
177 | [self runSpecGroupWithName:@"properties"];
178 | }
179 |
180 | - (void)testRef
181 | {
182 | [self runSpecGroupWithName:@"ref"];
183 | }
184 |
185 | // Not relying on local web services to run this test
186 | - (void)testRefRemote
187 | {
188 | _validator.formatValidationEnabled = NO;
189 | [self runSpecGroupWithName:@"refRemote"];
190 | }
191 |
192 | - (void)testRequired
193 | {
194 | [self runSpecGroupWithName:@"required"];
195 | }
196 |
197 | - (void)testType
198 | {
199 | [self runSpecGroupWithName:@"type"];
200 | }
201 |
202 | - (void)testUniqueItems
203 | {
204 | [self runSpecGroupWithName:@"uniqueItems"];
205 | }
206 |
207 | - (void)testOptionalBignum
208 | {
209 | [self runSpecGroupWithName:@"optional/bignum"];
210 | }
211 |
212 | - (void)testOptionalFormat
213 | {
214 | _validator.formatValidationEnabled = YES;
215 | [self runSpecGroupWithName:@"optional/format"];
216 | }
217 |
218 | /* This optional test won't ever pass when using NSJSONSerialization
219 | - (void)testOptionalZeroTerminatedFloats
220 | {
221 | [self runSpecSuiteWithName:@"optional/zeroTerminatedFloats"];
222 | }
223 | */
224 |
225 | - (void)testDateInLeapYears
226 | {
227 | JSONSchemaValidator *validator = _validator;
228 | _validator.formatValidationEnabled = YES;
229 | NSDictionary *dateFormat = @{@"format": @"date-time"};
230 |
231 | NSString *testData = @"1963-02-29T08:30:06.283185Z";
232 | BOOL result = [validator validateJSONInstance:testData withSchema:dateFormat];
233 | XCTAssertFalse(result, @"February 1963 didn't have 29 days.");
234 |
235 | testData = @"2000-02-29T08:30:06.283185Z";
236 | result = [validator validateJSONInstance:testData withSchema:dateFormat];
237 | XCTAssertTrue(result, @"February 2000 had 29 days");
238 |
239 | testData = @"2005-07-01T12:00:00-0700";
240 | result = [validator validateJSONInstance:testData withSchema:dateFormat];
241 | XCTAssertTrue(result, @"Validator should accept GMT offsets.");
242 |
243 | }
244 |
245 | - (void)runSpecGroupWithName:(NSString *)groupName
246 | {
247 | NSString *invalidStatus = @"invalid";
248 | NSString *validStatus = @"valid";
249 |
250 | NSArray *group = [self getSpecGroupWithName:groupName];
251 | XCTAssertNotNil(group, @"Couldn't find a spec group with the name '%@'", groupName);
252 |
253 | for (NSDictionary *spec in group)
254 | {
255 | NSDictionary *schema = spec[@"schema"];
256 | NSArray *tests = spec[@"tests"];
257 | NSString *specDescription = spec[@"description"];
258 |
259 | XCTAssertTrue(tests.count > 0, @"Invalid test spec, no tests defined");
260 |
261 | for (NSDictionary *test in tests)
262 | {
263 | NSString *testDescription = test[@"description"];
264 | id testData = test[@"data"];
265 | BOOL result = [_validator validateJSONInstance:testData withSchema:schema];
266 | BOOL desired = [test[@"valid"] boolValue];
267 |
268 | if (result != desired) {
269 | NSString *resultStatus = (result == YES) ? validStatus : invalidStatus;
270 | NSString *expectedStatus = (desired == YES) ? validStatus : invalidStatus;
271 | XCTFail(@"Group: %@; Spec: %@; Test: %@; Result: %@; Expected: %@", groupName, specDescription, testDescription, resultStatus, expectedStatus);
272 | }
273 | }
274 | }
275 | }
276 |
277 | - (NSArray *)getSpecGroupWithName:(NSString *)suiteName
278 | {
279 | NSString *filename = [suiteName stringByAppendingString:@".json"];
280 | NSURL *url = [_draft4SpecDirectory URLByAppendingPathComponent:filename isDirectory:NO];
281 | NSData *data = [NSData dataWithContentsOfURL:url];
282 | NSError *error = nil;
283 | return [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
284 | }
285 |
286 | @end
287 |
--------------------------------------------------------------------------------
/JSONToolsTests/JSONPointerTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPointerTests
3 | // based in part on CWJSONPointer by Jonathan Dring (MIT/Copyright (c) 2014)
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | @import XCTest;
11 | #import "JSONPointer.h"
12 |
13 | @interface JSONPointerTests : XCTestCase
14 | @property (nonatomic,strong) NSDictionary *jsonRFC6901;
15 | @property (nonatomic,strong) NSDictionary *jsonAdditional;
16 | @end
17 |
18 | @implementation JSONPointerTests
19 |
20 | - (void)setUp
21 | {
22 | [super setUp];
23 | _jsonRFC6901 = @{
24 | @"foo" : @[@"bar", @"baz"],
25 | @"" : @0,
26 | @"a/b" : @1,
27 | @"c%d" : @2,
28 | @"e^f" : @3,
29 | @"g|h" : @4,
30 | @"i\\j" : @5,
31 | @"k\"l" : @6,
32 | @" " : @7,
33 | @"m~n" : @8
34 | };
35 |
36 | _jsonAdditional = @{
37 | @"foo": @{
38 | @"bar": @{
39 | @"true": @YES,
40 | @"false": @NO,
41 | @"number": @55,
42 | @"negative": @(-55),
43 | @"string": @"mystring",
44 | @"null": [NSNull null],
45 | @"array": @[@1,@2,@3],
46 | @"object": @{
47 | @"a": @1,
48 | @"b": @2,
49 | @"c": @3
50 | }
51 | }
52 | }
53 | };
54 | }
55 |
56 | - (void)tearDown
57 | {
58 | // Put teardown code here. This method is called after the invocation of each test method in the class.
59 | [super tearDown];
60 | }
61 |
62 | #pragma mark - RFC6901 Specs
63 |
64 | /* RFC6901 String Representations
65 | "" // the whole document
66 | "/foo" ["bar", "baz"]
67 | "/foo/0" "bar"
68 | "/" 0
69 | "/a~1b" 1
70 | "/c%d" 2
71 | "/e^f" 3
72 | "/g|h" 4
73 | "/i\\j" 5
74 | "/k\"l" 6
75 | "/ " 7
76 | "/m~0n" 8
77 | */
78 |
79 | - (void)testRFC6901StringRepresentations
80 | {
81 | NSDictionary *json = _jsonRFC6901;
82 | NSDictionary *tests = @{@"": json,
83 | @"/foo": @[@"bar", @"baz"],
84 | @"/foo/0": @"bar",
85 | @"/": @0,
86 | @"/a~1b": @1,
87 | @"/c%d": @2,
88 | @"/e^f": @3,
89 | @"/g|h": @4,
90 | @"/i\\j": @5,
91 | @"/k\"l": @6,
92 | @"/ ": @7,
93 | @"/m~0n": @8
94 | };
95 |
96 | [tests enumerateKeysAndObjectsUsingBlock: ^(id key, id obj, BOOL *stop) {
97 | XCTAssertEqualObjects([json valueForJSONPointer:key], obj, @"RFC6901 Test '%@' Failed", key);
98 | }];
99 | }
100 |
101 | /* RFC6901 URI Fragment Representations
102 | "#" the whole document
103 | "#/foo" ["bar", "baz"]
104 | "#/foo/0" "bar"
105 | "#/" 0
106 | "#/a~1b" 1
107 | "#/c%25d" 2
108 | "#/e%5Ef" 3
109 | "#/g%7Ch" 4
110 | "#/i%5Cj" 5
111 | "#/k%22l" 6
112 | '#/%20" 7
113 | "#/m~0n" 8
114 | */
115 |
116 | - (void)testRFC6901URIRepresentations
117 | {
118 | NSDictionary *json = _jsonRFC6901;
119 | NSDictionary *tests = @{@"#": json,
120 | @"#/foo": @[@"bar", @"baz"],
121 | @"#/foo/0": @"bar",
122 | @"#/": @0,
123 | @"#/a~1b": @1,
124 | @"#/c%25d": @2,
125 | @"#/e%5Ef": @3,
126 | @"#/g%7Ch": @4,
127 | @"#/i%5Cj": @5,
128 | @"#/k%22l": @6,
129 | @"#/%20": @7,
130 | @"#/m~0n": @8
131 | };
132 |
133 | [tests enumerateKeysAndObjectsUsingBlock: ^(id key, id obj, BOOL *stop) {
134 | XCTAssertEqualObjects([json valueForJSONPointer:key], obj, @"URI Test '%@' Failed", key);
135 | }];
136 | }
137 |
138 | - (void)testRFC6901InvalidReferences
139 | {
140 | NSDictionary *json = _jsonRFC6901;
141 | NSString *prefix = @"Expected nil for invalid";
142 |
143 | XCTAssertNil([json valueForJSONPointer:@"/u110000"], @"%@ character", prefix);
144 | XCTAssertNil([json valueForJSONPointer:@"/c%25d"], @"%@ escaped non-fragment pointer", prefix);
145 | XCTAssertNil([json valueForJSONPointer:@"/foo/00"], @"%@ array reference with leading zero's", prefix);
146 | XCTAssertNil([json valueForJSONPointer:@"/foo/a"], @"%@ array reference", prefix);
147 | }
148 |
149 | #pragma mark - Boolean Values
150 |
151 | - (void)testSuccessForBoolean
152 | {
153 | NSDictionary *json = _jsonAdditional;
154 | NSString *pointer = @"/foo/bar/true";
155 | NSNumber *result = [json valueForJSONPointer:pointer];
156 | [self expectClass:[NSNumber class] forResult:result];
157 | XCTAssertEqual([result boolValue], YES, @"Expected true from pointer %@", pointer);
158 |
159 | pointer = @"/foo/bar/false";
160 | result = [json valueForJSONPointer:pointer];
161 | [self expectBooleanForResult:result];
162 | XCTAssertEqual([result boolValue], NO, @"Expected false from pointer %@", pointer);
163 | }
164 |
165 | - (void)testFailureForBoolean
166 | {
167 | NSDictionary *json = _jsonAdditional;
168 | NSDictionary *tests = @{@"array": @"/foo/bar/array",
169 | @"negative": @"/foo/bar/negative",
170 | @"object": @"/foo/bar/object",
171 | @"number": @"/foo/bar/number",
172 | @"string": @"/foo/bar/string",
173 | @"null": @"/foo/bar/null"};
174 | [tests enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
175 | NSNumber *result = [json valueForJSONPointer:obj];
176 | XCTAssertFalse([self isBooleanResult:result], @"Expected a non-boolean (%@) result for %@", key, obj);
177 | }];
178 | }
179 |
180 | #pragma mark - Number Values
181 |
182 | - (void)testSuccessForNumber
183 | {
184 | NSDictionary *json = _jsonAdditional;
185 | NSNumber *result = [json valueForJSONPointer:@"/foo/bar/number"];
186 | [self expectClass:[NSNumber class] forResult:result];
187 | XCTAssertEqual([result intValue], 55, @"Expected result to equal 55, found %@", result);
188 |
189 | result = [json valueForJSONPointer:@"/foo/bar/negative"];
190 | [self expectClass:[NSNumber class] forResult:result];
191 | XCTAssertEqual([result intValue], -55, @"Expected result to equal -55, found %@", result);
192 | }
193 |
194 | - (void)testFailureForNumber
195 | {
196 | NSDictionary *json = _jsonAdditional;
197 | NSDictionary *tests = @{@"string": @"/foo/bar/string",
198 | @"array": @"/foo/bar/array",
199 | @"object": @"/foo/bar/object",
200 | @"null": @"/foo/bar/null"};
201 | [tests enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
202 | NSNumber *result = [json valueForJSONPointer:obj];
203 | XCTAssertFalse([result isKindOfClass:[NSNumber class]], @"Expected a non-number result, found %@", result);
204 | }];
205 | }
206 |
207 | #pragma mark - String Values
208 |
209 | - (void)testSuccessForString
210 | {
211 | NSDictionary *json = _jsonAdditional;
212 | NSString *result = [json valueForJSONPointer:@"/foo/bar/string"];
213 | [self expectClass:[NSString class] forResult:result];
214 | XCTAssertEqualObjects(result, @"mystring", @"Expected a matching string result.");
215 | }
216 |
217 | - (void)testFailureForString
218 | {
219 | NSDictionary *json = _jsonAdditional;
220 | NSDictionary *tests = @{@"array": @"/foo/bar/array",
221 | @"bool": @"/foo/bar/true",
222 | @"number": @"/foo/bar/number",
223 | @"object": @"/foo/bar/object",
224 | @"null": @"/foo/bar/null"};
225 | [tests enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
226 | NSString *result = [json valueForJSONPointer:obj];
227 | XCTAssertFalse([result isKindOfClass:[NSString class]], @"Expected a non-string result, found %@", result);
228 | }];
229 | }
230 |
231 | #pragma mark - Array Values
232 |
233 | - (void)testSuccessForArray
234 | {
235 | NSDictionary *json = _jsonAdditional;
236 | NSArray *array = @[@1,
237 | @2,
238 | @3];
239 | NSArray *result = [json valueForJSONPointer:@"/foo/bar/array"];
240 | [self expectClass:[NSArray class] forResult:result];
241 | XCTAssertEqualObjects(result, array, @"Expected a matching array result, found %@", result);
242 | }
243 |
244 | - (void)testFailureForArray
245 | {
246 | NSDictionary *json = _jsonAdditional;
247 | NSDictionary *tests = @{@"string": @"/foo/bar/string",
248 | @"bool": @"/foo/bar/true",
249 | @"number": @"/foo/bar/number",
250 | @"object": @"/foo/bar/object",
251 | @"null": @"/foo/bar/null"};
252 | [tests enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
253 | NSArray *result = [json valueForJSONPointer:obj];
254 | XCTAssertFalse([result isKindOfClass:[NSArray class]], @"Expected a non-array result, found %@", result);
255 | }];
256 | }
257 |
258 | #pragma mark - Dictionary Values
259 |
260 | - (void)testSuccessForDictionary
261 | {
262 | NSDictionary *json = _jsonAdditional;
263 | NSDictionary *dictionary = @{@"a": @1,
264 | @"b": @2,
265 | @"c": @3};
266 | NSDictionary *result = [json valueForJSONPointer:@"/foo/bar/object"];
267 | [self expectClass:[NSDictionary class] forResult:result];
268 | XCTAssertEqualObjects(result, dictionary, @"Expected a matching dictionary result, found %@", result);
269 | }
270 |
271 | - (void)testFailureForDictionary
272 | {
273 | NSDictionary *json = _jsonAdditional;
274 | NSDictionary *tests = @{@"array": @"/foo/bar/array",
275 | @"bool": @"/foo/bar/true",
276 | @"number": @"/foo/bar/number",
277 | @"string": @"/foo/bar/string",
278 | @"null": @"/foo/bar/null"};
279 | [tests enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
280 | NSDictionary *result = [json valueForJSONPointer:obj];
281 | XCTAssertFalse([result isKindOfClass:[NSDictionary class]], @"Expected a non-dictionary result, found %@", result);
282 | }];
283 | }
284 |
285 | #pragma mark - Utilities
286 |
287 | - (void)expectClass:(Class)class forResult:(id)result
288 | {
289 | XCTAssertTrue([result isKindOfClass:class], @"Expected an %@ result, found %@", class, [result class]);
290 | }
291 |
292 | - (void)expectBooleanForResult:(id)result
293 | {
294 | XCTAssertTrue([self isBooleanResult:result], @"Expected a boolean result, found %@", result);
295 | }
296 |
297 | - (BOOL)isBooleanResult:(NSNumber *)result
298 | {
299 | return ([result isKindOfClass:[NSNumber class]] &&
300 | (result.intValue == 0 || result.intValue == 1));
301 | }
302 |
303 | @end
304 |
--------------------------------------------------------------------------------
/JSONToolsTests/JSONPatchApplyTests.m:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchApplyTests.m
3 | // inspired by https://github.com/Starcounter-Jack/JSON-Patch
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | @import XCTest;
11 | #import "JSONPatch.h"
12 | #import "JSONDeeplyMutable.h"
13 |
14 | @interface JSONPatchApplyTests : XCTestCase
15 |
16 | @end
17 |
18 | @implementation JSONPatchApplyTests
19 |
20 | - (void)setUp
21 | {
22 | [super setUp];
23 | }
24 |
25 | - (void)tearDown
26 | {
27 | [super tearDown];
28 | }
29 |
30 | - (void)testShouldApplyAdd
31 | {
32 | NSMutableDictionary *initial = [@{@"foo": @1,
33 | @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON];
34 | NSMutableDictionary *obj = [initial mutableCopy];
35 | NSMutableDictionary *expected = [initial mutableCopy];
36 |
37 |
38 | expected[@"bar"] = @[@1,@2,@3,@4];
39 | [JSONPatch applyPatches:@[@{@"op": @"add",
40 | @"path": @"/bar",
41 | @"value": @[@1, @2, @3, @4]}] toCollection:obj];
42 | XCTAssertEqualObjects(obj, expected, @"Failed to apply add patch, expected %@, found %@", expected, obj);
43 |
44 |
45 |
46 | expected[@"baz"][0] = @{@"qux": @"hello",
47 | @"foo": @"world"};
48 | [JSONPatch applyPatches:@[@{@"opp": @"add",
49 | @"path": @"/baz/0/foo",
50 | @"value": @"world"}] toCollection:obj];
51 | XCTAssertEqualObjects(obj, expected, @"Failed to apply add patch, expected %@, found %@", expected, obj);
52 |
53 |
54 |
55 | obj = [initial mutableCopy];
56 | expected = [initial mutableCopy];
57 | expected[@"bar"] = @YES;
58 | [JSONPatch applyPatches:@[@{@"op": @"add",
59 | @"path": @"/bar",
60 | @"value": @YES}] toCollection:obj];
61 | XCTAssertEqualObjects(obj, expected, @"Failed to apply add patch, expected %@, found %@", expected, obj);
62 |
63 |
64 | expected[@"bar"] = @NO;
65 | [JSONPatch applyPatches:@[@{@"op": @"add",
66 | @"path": @"/bar",
67 | @"value": @NO}] toCollection:obj];
68 | XCTAssertEqualObjects(obj, expected, @"Failed to apply add patch, expected %@, found %@", expected, obj);
69 |
70 | obj = [initial mutableCopy];
71 | expected = [initial mutableCopy];
72 | expected[@"bar"] = [NSNull null];
73 | [JSONPatch applyPatches:@[@{@"op": @"add",
74 | @"path": @"/bar",
75 | @"value": [NSNull null]}] toCollection:obj];
76 | XCTAssertEqualObjects(obj, expected, @"Failed to apply add patch, expected %@, found %@", expected, obj);
77 | }
78 |
79 | - (void)testShouldApplyRemove
80 | {
81 | NSMutableDictionary *obj = [@{@"foo": @1,
82 | @"baz": @[@{@"qux": @"hello"}],
83 | @"bar": @[@1,@2,@3,@4]} copyAsDeeplyMutableJSON];
84 |
85 | NSMutableDictionary *expected = [obj copyAsDeeplyMutableJSON];
86 | [expected removeObjectForKey:@"bar"];
87 | [JSONPatch applyPatches:@[@{@"op": @"remove",
88 | @"path": @"/bar"}] toCollection:obj];
89 | XCTAssertEqualObjects(obj, expected, @"Failed to apply remove patch, expected %@, found %@", expected, obj);
90 |
91 | expected = [@{@"foo": @1,
92 | @"baz": @[@{}]} copyAsDeeplyMutableJSON];
93 | [JSONPatch applyPatches:@[@{@"op": @"remove",
94 | @"path": @"/baz/0/qux"}] toCollection:obj];
95 | XCTAssertEqualObjects(obj, expected, @"Failed to apply remove patch, expected %@, found %@", expected, obj);
96 | }
97 |
98 | - (void)testShouldApplyReplace
99 | {
100 | NSMutableDictionary *obj = [@{@"foo": @1,
101 | @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON];
102 |
103 | NSMutableDictionary *expected = [obj copyAsDeeplyMutableJSON];
104 | expected[@"foo"] = @[@1,@2,@3,@4];
105 | [JSONPatch applyPatches:@[@{@"op": @"replace",
106 | @"path": @"/foo",
107 | @"value": @[@1,@2,@3,@4]}] toCollection:obj];
108 | XCTAssertEqualObjects(obj, expected, @"Failed to apply replace patch, expected %@, found %@", expected, obj);
109 |
110 | expected[@"baz"][0][@"qux"] = @"world";
111 | [JSONPatch applyPatches:@[@{@"op": @"replace",
112 | @"path": @"/baz/0/qux",
113 | @"value": @"world"}] toCollection:obj];
114 | XCTAssertEqualObjects(obj, expected, @"Failed to apply replace patch, expected %@, found %@", expected, obj);
115 | }
116 |
117 | - (void)testShouldApplyReplaceInNestedArray
118 | {
119 | NSMutableDictionary *obj = [@{@"list": @[@{@"id": @1},
120 | @{@"id": @2},
121 | @{@"id": @3}]} copyAsDeeplyMutableJSON];
122 | NSDictionary *patch = @{
123 | @"op": @"replace",
124 | @"path": @"/list/1",
125 | @"value": @{@"id": @4}
126 | };
127 |
128 | NSDictionary *expected = @{@"list": @[@{@"id": @1},
129 | @{@"id": @4},
130 | @{@"id": @3}]};
131 |
132 | [JSONPatch applyPatches:@[patch] toCollection:obj];
133 |
134 | NSDictionary *resultRoot = [obj copyAsDeeplyImmutableJSONWithExceptions:NO];
135 | NSArray *resultArray = resultRoot[@"list"];
136 |
137 | XCTAssertTrue([resultRoot isEqualToDictionary:expected], @"Failed to apply nested array replace patch, expected %@, found %@", expected, obj);
138 | XCTAssertEqualObjects(resultArray, expected[@"list"], @"Failed to apply nested array replace patch, expected %@, found %@", expected, obj);
139 | }
140 |
141 | - (void)testShouldApplyTest
142 | {
143 | NSDictionary *obj = @{@"foo": @{@"bar": @[@1,@2,@5,@4]}};
144 | NSDictionary *testObj = @{@"bar": @[@1,@2,@5,@4]};
145 | NSNumber *result = [JSONPatch applyPatches:@[@{@"op": @"test",
146 | @"path": @"/foo",
147 | @"value": testObj}] toCollection:obj];
148 | XCTAssertNotNil(result, @"Failed to apply test patch, expected a non-nil test result");
149 | XCTAssertTrue([result boolValue], @"Failed to apply test patch, expected TRUE, found %@", result);
150 |
151 | result = [JSONPatch applyPatches:@[@{@"op": @"test",
152 | @"path": @"/foo",
153 | @"value": @[@1,@2]}] toCollection:obj];
154 | XCTAssertNotNil(result, @"Failed to apply test patch, expected a non-nil test result");
155 | XCTAssertFalse([result boolValue], @"Failed to apply test patch, expected FALSE, found %@", result);
156 | }
157 |
158 | - (void)testShouldApplyMove
159 | {
160 | NSMutableDictionary *obj = [@{@"foo": @1,
161 | @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON];
162 | NSMutableDictionary *expected = [obj copyAsDeeplyMutableJSON];
163 |
164 | expected[@"bar"] = @1;
165 | [expected removeObjectForKey:@"foo"];
166 | [JSONPatch applyPatches:@[@{@"op": @"move",
167 | @"from": @"/foo",
168 | @"path": @"/bar"}] toCollection:obj];
169 | XCTAssertEqualObjects(obj, expected, @"Failed to apply move patch, expected %@, found %@", expected, obj);
170 |
171 | [expected[@"baz"][0] removeAllObjects];
172 | [expected[@"baz"] addObject:@"hello"];
173 | [JSONPatch applyPatches:@[@{@"op": @"move",
174 | @"from": @"/baz/0/qux",
175 | @"path": @"/baz/1"}] toCollection:obj];
176 | XCTAssertEqualObjects(obj, expected, @"Failed to apply move patch, expected %@, found %@", expected, obj);
177 | }
178 |
179 | - (void)testShouldApplyCopy
180 | {
181 | NSMutableDictionary *obj = [@{@"foo": @1,
182 | @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON];
183 | NSMutableDictionary *expected = [obj copyAsDeeplyMutableJSON];
184 |
185 | expected[@"bar"] = @1;
186 | [JSONPatch applyPatches:@[@{@"op": @"copy",
187 | @"from": @"/foo",
188 | @"path": @"/bar"}] toCollection:obj];
189 | XCTAssertEqualObjects(obj, expected, @"Failed to apply copy patch, expected %@, found %@", expected, obj);
190 |
191 | [expected[@"baz"] addObject:@"hello"];
192 | [JSONPatch applyPatches:@[@{@"op": @"copy",
193 | @"from": @"/baz/0/qux",
194 | @"path": @"/baz/1"}] toCollection:obj];
195 | XCTAssertEqualObjects(obj, expected, @"Failed to apply copy patch, expected %@, found %@", expected, obj);
196 | }
197 |
198 | - (void)testShouldApplyMultiplePatches
199 | {
200 | NSMutableDictionary *obj = [@{@"firstName": @"Albert",
201 | @"contactDetails": @{
202 | @"phoneNumbers": @[]
203 | }
204 | } copyAsDeeplyMutableJSON];
205 |
206 | NSArray *patches = @[
207 | @{@"op": @"replace",
208 | @"path": @"/firstName",
209 | @"value": @"Joachim"
210 | },
211 | @{@"op": @"add",
212 | @"path": @"/lastName",
213 | @"value": @"Wester"
214 | },
215 | @{@"op": @"add",
216 | @"path": @"/contactDetails/phoneNumbers/0",
217 | @"value": @{ @"number": @"555-123" }
218 | }
219 | ];
220 |
221 | NSMutableDictionary *expected = [@{@"firstName": @"Joachim",
222 | @"lastName": @"Wester",
223 | @"contactDetails": @{
224 | @"phoneNumbers": @[
225 | @{@"number": @"555-123"}
226 | ]
227 | }
228 | } mutableCopy];
229 |
230 | [JSONPatch applyPatches:patches toCollection:obj];
231 |
232 | XCTAssertEqualObjects(obj, expected, @"Failed to apply multiple patches, expected %@, found %@", expected, obj);
233 | }
234 |
235 | /**
236 | * Empty path strings should be accepted as the pointer to root
237 | * https://github.com/grgcombs/JSONTools/issues/3
238 | *
239 | * JSON Pointer RFC
240 | * http://tools.ietf.org/html/rfc6901#section-5
241 | *
242 | * The following JSON strings evaluate to the accompanying values:
243 | *
244 | * "" // the whole document
245 | */
246 | - (void)testShouldAcceptPatchesToRootPointer
247 | {
248 | NSMutableDictionary *obj = [@{@"foo": @1,
249 | @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON];
250 |
251 | NSDictionary *expected = @{@"bar": @[@1,@2,@3,@4]};
252 | id result = [JSONPatch applyPatches:@[@{@"op": @"replace",
253 | @"path": @"",
254 | @"value": @{@"bar": @[@1,@2,@3,@4]}}] toCollection:obj];
255 | XCTAssertEqualObjects(result, @1, @"The root-level patch should succeed.");
256 | XCTAssertEqualObjects(obj, expected, @"Failed to apply root-level patch, expected %@, found %@", expected, obj);
257 | }
258 |
259 | - (void)testShouldNotPatchIncompatibleTopLevelCollections
260 | {
261 | NSMutableDictionary *dictionary = [@{@"foo": @1,
262 | @"baz": @[@{@"qux": @"hello"}]} copyAsDeeplyMutableJSON];
263 | NSArray *replacementArray = @[@1,@2,@3,@4];
264 |
265 | id result = [JSONPatch applyPatches:@[@{@"op": @"replace",
266 | @"path": @"",
267 | @"value": replacementArray}] toCollection:dictionary];
268 | XCTAssertEqualObjects(result, @0, @"Should have failed to replace a top level dictionary with an array: %@", result);
269 |
270 | NSMutableArray *array = [@[@1,@2,@3,@4] copyAsDeeplyMutableJSON];
271 | NSDictionary *replacementDictionary = @{@"1": @1,
272 | @"2": @2,
273 | @"3": @3,
274 | @"4": @4};
275 |
276 | result = [JSONPatch applyPatches:@[@{@"op": @"replace",
277 | @"path": @"",
278 | @"value": replacementDictionary}] toCollection:array];
279 | XCTAssertEqualObjects(result, @0, @"Should have failed to replace a top level array with a dictionary: %@", result);
280 | }
281 |
282 | @end
283 |
--------------------------------------------------------------------------------
/JSONTools/JSONPatch.m:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatch.m
3 | // inspired by https://github.com/Starcounter-Jack/JSON-Patch
4 | //
5 | // JSONTools
6 | //
7 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
8 | // See LICENSE.txt for details.
9 |
10 | #import "JSONPatch.h"
11 | #import "JSONPatchDictionary.h"
12 | #import "JSONPatchArray.h"
13 | #import "JSONPointer.h"
14 | #import "JSONDeeplyMutable.h"
15 |
16 | @implementation JSONPatch
17 |
18 | + (id)applyPatches:(NSArray *)patches toCollection:(id)collection
19 | {
20 | if (!collection ||
21 | ![collection respondsToSelector:@selector(mutableCopy)])
22 | {
23 | return nil;
24 | }
25 |
26 | id result = nil;
27 |
28 | for (NSDictionary *patch in patches)
29 | {
30 | JSONPatchInfo *patchInfo = [JSONPatchInfo newPatchInfoWithDictionary:patch];
31 | if (!patchInfo)
32 | break;
33 |
34 | id object = collection;
35 |
36 | if (patchInfo.path && !patchInfo.path.length)
37 | {
38 | // http://tools.ietf.org/html/rfc6901#section-5
39 | // The following JSON strings evaluate to the accompanying values:
40 | // "" // the whole document
41 |
42 | result = [self applyRootLevelPatch:patchInfo toCollection:object];
43 | continue;
44 | }
45 |
46 | NSArray *pathKeys = [patchInfo.path componentsSeparatedByString:@"/"];
47 | NSInteger t = 1;
48 |
49 | while (YES) {
50 | // path not found, fail this
51 | if (!object || [object isKindOfClass:NSNull.class]) {
52 | result = @(0);
53 | break;
54 | }
55 | if ([self collectionNeedsMutableCopy:object])
56 | {
57 | // If you get here, chances are subsequent/multiple patches won't work correctly.
58 | // You should be sure to pass in a collection via -copyAsDeeplyMutable to ensure
59 | // that this doesn't adversely affect you.
60 | object = [object copyAsDeeplyMutableJSON];
61 | }
62 |
63 | if ([object isKindOfClass:[NSMutableArray class]])
64 | {
65 | NSString *component = pathKeys[t];
66 | NSInteger index = [(NSMutableArray *)object indexForJSONPointerComponent:component allowOutOfBounds:YES];
67 | if (index == NSNotFound)
68 | break;
69 |
70 | t++;
71 |
72 | if (t >= pathKeys.count)
73 | {
74 | BOOL stop = NO;
75 | result = [self applyPatch:patchInfo array:object index:index collection:collection stop:&stop];
76 | if (stop) {
77 | return result;
78 | }
79 | break;
80 | }
81 |
82 | NSMutableArray *thisObject = object;
83 | NSMutableArray *nextObject = thisObject[index];
84 | if ([self collectionNeedsMutableCopy:nextObject])
85 | {
86 | nextObject = [nextObject copyAsDeeplyMutableJSON];
87 | thisObject[index] = nextObject;
88 | }
89 | object = nextObject;
90 | }
91 | else if ([object isKindOfClass:[NSMutableDictionary class]])
92 | {
93 | NSString *component = [(NSMutableDictionary *)object keyForJSONPointerComponent:pathKeys[t]];
94 |
95 | t++;
96 |
97 | if (t >= pathKeys.count)
98 | {
99 | BOOL stop = NO;
100 | result = [self applyPatch:patchInfo dictionary:object key:component collection:collection stop:&stop];
101 | if (stop) {
102 | return result;
103 | }
104 | break;
105 | }
106 |
107 | NSMutableDictionary *thisObject = object;
108 | NSMutableDictionary *nextObject = thisObject[component];
109 | if ([self collectionNeedsMutableCopy:nextObject])
110 | {
111 | nextObject = [nextObject copyAsDeeplyMutableJSON];
112 | thisObject[component] = nextObject;
113 | }
114 | object = nextObject;
115 | }
116 | }
117 | }
118 | return result;
119 | }
120 |
121 | #pragma mark - Singular Patch
122 |
123 | + (id)applyRootLevelPatch:(JSONPatchInfo *)patchInfo toCollection:(id)collection
124 | {
125 | if (!collection ||
126 | ![collection respondsToSelector:@selector(mutableCopy)])
127 | {
128 | return nil;
129 | }
130 |
131 | id result = nil;
132 |
133 | switch (patchInfo.op)
134 | {
135 | case JSONPatchOperationGet:
136 | return [collection mutableCopy];
137 | break;
138 | case JSONPatchOperationTest:
139 | result = @(collection == patchInfo.value || [collection isEqual:patchInfo.value]);
140 | break;
141 | case JSONPatchOperationAdd:
142 | case JSONPatchOperationRemove:
143 | case JSONPatchOperationCopy:
144 | case JSONPatchOperationMove:
145 | case JSONPatchOperationReplace:
146 | {
147 | if (![self isCompatibleCollection:collection toCollection:patchInfo.value] ||
148 | ![collection respondsToSelector:@selector(removeAllObjects)])
149 | {
150 | result = @(NO);
151 | break;
152 | }
153 |
154 | [collection removeAllObjects];
155 |
156 | if ([collection isKindOfClass:[NSDictionary class]])
157 | {
158 | [collection addEntriesFromDictionary:patchInfo.value];
159 | result = @([collection isEqualToDictionary:patchInfo.value]);
160 | }
161 | else if ([collection isKindOfClass:[NSArray class]])
162 | {
163 | [collection addObjectsFromArray:patchInfo.value];
164 | result = @([collection isEqualToArray:patchInfo.value]);
165 | }
166 | break;
167 | }
168 | case JSONPatchOperationUndefined:
169 | break;
170 | }
171 | return result;
172 | }
173 |
174 | + (id)applyPatch:(JSONPatchInfo *)patchInfo array:(NSMutableArray *)array index:(NSInteger)index collection:(id)collection stop:(BOOL *)stop
175 | {
176 | BOOL success = NO;
177 | switch (patchInfo.op)
178 | {
179 | case JSONPatchOperationGet:
180 | case JSONPatchOperationTest:
181 | *stop = YES;
182 | return [JSONPatchArray applyPatchInfo:patchInfo object:array index:index];
183 | break;
184 | case JSONPatchOperationAdd:
185 | case JSONPatchOperationReplace:
186 | case JSONPatchOperationRemove:
187 | success = ([[JSONPatchArray applyPatchInfo:patchInfo object:array index:index] boolValue]);
188 | break;
189 | case JSONPatchOperationCopy:
190 | success = [self applyCopyPatch:patchInfo toCollection:collection];
191 | break;
192 | case JSONPatchOperationMove:
193 | success = [self applyMovePatch:patchInfo toCollection:collection];
194 | break;
195 | case JSONPatchOperationUndefined:
196 | break;
197 | }
198 | if (!success)
199 | return nil;
200 | return @(success);
201 | }
202 |
203 | + (id)applyPatch:(JSONPatchInfo *)patchInfo dictionary:(NSMutableDictionary *)dictionary key:(NSString *)key collection:(id)collection stop:(BOOL *)stop
204 | {
205 | BOOL success = NO;
206 | switch (patchInfo.op)
207 | {
208 | case JSONPatchOperationGet:
209 | case JSONPatchOperationTest:
210 | *stop = YES;
211 | return [JSONPatchDictionary applyPatchInfo:patchInfo object:dictionary key:key];
212 | break;
213 | case JSONPatchOperationAdd:
214 | case JSONPatchOperationReplace:
215 | case JSONPatchOperationRemove:
216 | success = ([[JSONPatchDictionary applyPatchInfo:patchInfo object:dictionary key:key] boolValue]);
217 | break;
218 | case JSONPatchOperationCopy:
219 | success = [self applyCopyPatch:patchInfo toCollection:collection];
220 | break;
221 | case JSONPatchOperationMove:
222 | success = [self applyMovePatch:patchInfo toCollection:collection];
223 | break;
224 | case JSONPatchOperationUndefined:
225 | break;
226 | }
227 | if (!success)
228 | return nil;
229 | return @(success);
230 | }
231 |
232 | #pragma mark - Aggregated Operations
233 |
234 | + (BOOL)applyCopyPatch:(JSONPatchInfo *)patchInfo toCollection:(id)collection
235 | {
236 | id fromValue = [self applyPatches:@[@{@"op": @"_get",
237 | @"path": patchInfo.fromPath}] toCollection:collection];
238 | if (!fromValue) {
239 | return NO;
240 | }
241 | id toResult = [self applyPatches:@[@{@"op": @"add",
242 | @"path": patchInfo.path,
243 | @"value": fromValue}] toCollection:collection];
244 | return (toResult != NULL);
245 | }
246 |
247 | + (BOOL)applyMovePatch:(JSONPatchInfo *)patchInfo toCollection:(id)collection
248 | {
249 | id fromValue = [self applyPatches:@[@{@"op": @"_get",
250 | @"path": patchInfo.fromPath}] toCollection:collection];
251 | if (!fromValue)
252 | {
253 | return NO;
254 | }
255 | id removeResult = [self applyPatches:@[@{@"op": @"remove",
256 | @"path": patchInfo.fromPath}] toCollection:collection];
257 | id toResult = [self applyPatches:@[@{@"op": @"add",
258 | @"path": patchInfo.path,
259 | @"value": fromValue}] toCollection:collection];
260 | return (toResult != NULL &&
261 | removeResult != NULL);
262 | }
263 |
264 | #pragma mark - Patch Generation
265 |
266 | + (NSArray *)createPatchesComparingCollectionsOld:(id)oldCollection toNew:(id)newCollection
267 | {
268 | return [self compareOldCollection:oldCollection toNew:newCollection path:@""];
269 | }
270 |
271 | + (NSArray *)compareOldCollection:(id)oldCollection toNew:(id)newCollection path:(NSString *)path
272 | {
273 | if ([self isCompatibleDictionaries:oldCollection dict2:newCollection])
274 | {
275 | return [self compareOldDictionary:oldCollection toNew:newCollection path:path];
276 | }
277 | if ([self isCompatibleArrays:oldCollection array2:newCollection])
278 | {
279 | return [self compareOldArray:oldCollection toNew:newCollection path:path];
280 | }
281 | return nil;
282 | }
283 |
284 | + (NSArray *)compareValue:(id)oldValue toNew:(id)newValue path:(NSString *)path replacement:(BOOL *)hasReplacementPtr
285 | {
286 | NSMutableArray *patches = [[NSMutableArray alloc] init];
287 |
288 | if ([self isCompatibleDictionaries:oldValue dict2:newValue])
289 | {
290 | NSArray *subPatches = [self compareOldDictionary:oldValue toNew:newValue path:path];
291 | if (subPatches.count) {
292 | [patches addObjectsFromArray:subPatches];
293 | }
294 | }
295 | else if ([self isCompatibleArrays:oldValue array2:newValue])
296 | {
297 | NSArray *subPatches = [self compareOldArray:oldValue toNew:newValue path:path];
298 | if (subPatches.count) {
299 | [patches addObjectsFromArray:subPatches];
300 | }
301 | }
302 | else if (oldValue &&
303 | newValue &&
304 | ![oldValue isEqual:newValue])
305 | {
306 | *hasReplacementPtr = YES;
307 | [patches addObject:@{@"op": @"replace",
308 | @"path": path,
309 | @"value": newValue}];
310 | }
311 | return patches;
312 | }
313 |
314 | + (NSArray *)compareOldDictionary:(NSDictionary *)oldDict toNew:(NSDictionary *)newDict path:(NSString *)path
315 | {
316 | NSMutableArray *patches = [[NSMutableArray alloc] init];
317 | __block BOOL changed = NO;
318 | __block BOOL deleted = NO;
319 |
320 | [oldDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
321 | NSString *escapedKey = [key stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
322 | NSString *escapedPath = [path stringByAppendingFormat:@"/%@", escapedKey];
323 | id newValue = newDict[key];
324 |
325 | if (!newValue) {
326 | [patches addObject:@{@"op": @"remove",
327 | @"path": escapedPath}];
328 | deleted = YES;
329 | }
330 | else
331 | {
332 | NSArray *subPatches = [self compareValue:obj toNew:newValue path:escapedPath replacement:&changed];
333 | if (subPatches.count) {
334 | [patches addObjectsFromArray:subPatches];
335 | }
336 | }
337 | }];
338 |
339 | if (!deleted &&
340 | newDict.count == oldDict.count)
341 | {
342 | return patches;
343 | }
344 |
345 | [newDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
346 | NSString *escapedKey = [key stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
347 | NSString *escapedPath = [path stringByAppendingFormat:@"/%@", escapedKey];
348 | id oldValue = oldDict[key];
349 | if (!oldValue)
350 | {
351 | [patches addObject:@{@"op": @"add",
352 | @"path": escapedPath,
353 | @"value": obj}];
354 | }
355 | }];
356 |
357 | return patches;
358 | }
359 |
360 | + (NSArray *)compareOldArray:(NSArray *)oldArray toNew:(NSArray *)newArray path:(NSString *)path
361 | {
362 | NSMutableArray *patches = [[NSMutableArray alloc] init];
363 | NSUInteger oldCount = [oldArray count];
364 | NSUInteger newCount = [newArray count];
365 | NSUInteger maxCount = MAX(oldCount, newCount);
366 | NSInteger index = maxCount - 1;
367 | while (index >= 0)
368 | {
369 | NSString *indexPath = [path stringByAppendingFormat:@"/%lu", (unsigned long)index];
370 | BOOL changes = NO;
371 |
372 | if (index < oldCount &&
373 | index < newCount)
374 | {
375 | id oldValue = oldArray[index];
376 | id newValue = newArray[index];
377 | NSArray *subPatches = [self compareValue:oldValue toNew:newValue path:indexPath replacement:&changes];
378 | if (subPatches.count)
379 | {
380 | [patches addObjectsFromArray:subPatches];
381 | }
382 | }
383 | else if (index < newCount)
384 | {
385 | id newValue = newArray[index];
386 | [patches addObject:@{@"op": @"add",
387 | @"path": indexPath,
388 | @"value": newValue}];
389 | }
390 | else if (index < oldCount)
391 | {
392 | [patches addObject:@{@"op": @"remove",
393 | @"path": indexPath}];
394 | }
395 | index--;
396 | }
397 | return patches;
398 | }
399 |
400 | + (BOOL)isCompatibleCollection:(id)collection toCollection:(id)otherCollection
401 | {
402 | return ([self isCompatibleDictionaries:collection dict2:otherCollection] ||
403 | [self isCompatibleArrays:collection array2:otherCollection]);
404 | }
405 |
406 | + (BOOL)isCompatibleDictionaries:(NSDictionary *)dict1 dict2:(NSDictionary *)dict2
407 | {
408 | if (!dict1 || !dict2)
409 | return NO;
410 | return ([dict1 isKindOfClass:[NSDictionary class]] &&
411 | [dict2 isKindOfClass:[NSDictionary class]]);
412 | }
413 |
414 | + (BOOL)isCompatibleArrays:(NSArray *)array1 array2:(NSArray *)array2
415 | {
416 | if (!array1 || !array2)
417 | return NO;
418 | return ([array1 isKindOfClass:[NSArray class]] &&
419 | [array2 isKindOfClass:[NSArray class]]);
420 | }
421 |
422 | + (BOOL)collectionNeedsMutableCopy:(id)collection
423 | {
424 | if (![collection isKindOfClass:[NSMutableDictionary class]] &&
425 | ![collection isKindOfClass:[NSMutableArray class]] &&
426 | [collection respondsToSelector:@selector(mutableCopy)])
427 | {
428 | return YES;
429 | }
430 | return NO;
431 | }
432 |
433 | @end
434 |
--------------------------------------------------------------------------------
/JSONToolsTests.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 37A5004F18FC3FC700A15B0F /* JSONPatchApplyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 37A5004E18FC3FC700A15B0F /* JSONPatchApplyTests.m */; };
11 | 37A5005318FC9E0200A15B0F /* JSONPatchGenerateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 37A5005218FC9E0200A15B0F /* JSONPatchGenerateTests.m */; };
12 | 37BDF4CF18FAF1DD00BF9705 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37BDF4CE18FAF1DD00BF9705 /* XCTest.framework */; };
13 | 37BDF4D018FAF1DD00BF9705 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37BDF4C018FAF1DD00BF9705 /* Foundation.framework */; };
14 | 37BDF4D218FAF1DD00BF9705 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 37BDF4D118FAF1DD00BF9705 /* UIKit.framework */; };
15 | 37BDF4DB18FAF1DD00BF9705 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 37BDF4D918FAF1DD00BF9705 /* InfoPlist.strings */; };
16 | 37BDF4DD18FAF1DD00BF9705 /* JSONPointerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 37BDF4DC18FAF1DD00BF9705 /* JSONPointerTests.m */; };
17 | 37C8BA2D19106B74004D3E23 /* JSONSchemaTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 37C8BA2C19106B74004D3E23 /* JSONSchemaTests.m */; };
18 | 5C2AB08280C6430AA079590E /* libPods-JSONToolsTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 39FDE358F51848868E4334BA /* libPods-JSONToolsTests.a */; };
19 | 697BDB73120B462A829D7D82 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B35914385F39418EB4099917 /* libPods.a */; };
20 | /* End PBXBuildFile section */
21 |
22 | /* Begin PBXFileReference section */
23 | 2A04D7DA4D577E304B768C24 /* Pods-JSONToolsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JSONToolsTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-JSONToolsTests/Pods-JSONToolsTests.debug.xcconfig"; sourceTree = ""; };
24 | 37A5004E18FC3FC700A15B0F /* JSONPatchApplyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONPatchApplyTests.m; sourceTree = ""; };
25 | 37A5005218FC9E0200A15B0F /* JSONPatchGenerateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONPatchGenerateTests.m; sourceTree = ""; };
26 | 37B7C5F919DCB7CD00395110 /* Pods.release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; };
27 | 37B7C5FA19DCB7E000395110 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; };
28 | 37BDF4C018FAF1DD00BF9705 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
29 | 37BDF4CD18FAF1DD00BF9705 /* JSONToolsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JSONToolsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
30 | 37BDF4CE18FAF1DD00BF9705 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
31 | 37BDF4D118FAF1DD00BF9705 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; };
32 | 37BDF4D818FAF1DD00BF9705 /* JSONToolsTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "JSONToolsTests-Info.plist"; sourceTree = ""; };
33 | 37BDF4DA18FAF1DD00BF9705 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
34 | 37BDF4DC18FAF1DD00BF9705 /* JSONPointerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JSONPointerTests.m; sourceTree = ""; };
35 | 37C8BA2C19106B74004D3E23 /* JSONSchemaTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONSchemaTests.m; sourceTree = ""; };
36 | 39FDE358F51848868E4334BA /* libPods-JSONToolsTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-JSONToolsTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
37 | 8A736BF437437DE4CA84B315 /* Pods-JSONToolsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JSONToolsTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-JSONToolsTests/Pods-JSONToolsTests.release.xcconfig"; sourceTree = ""; };
38 | 8BAA2AFED5A38753C8438FB8 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = ""; };
39 | B35914385F39418EB4099917 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; };
40 | EA415B43DA52B212F752F99D /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = ""; };
41 | /* End PBXFileReference section */
42 |
43 | /* Begin PBXFrameworksBuildPhase section */
44 | 37BDF4CA18FAF1DD00BF9705 /* Frameworks */ = {
45 | isa = PBXFrameworksBuildPhase;
46 | buildActionMask = 2147483647;
47 | files = (
48 | 37BDF4CF18FAF1DD00BF9705 /* XCTest.framework in Frameworks */,
49 | 37BDF4D218FAF1DD00BF9705 /* UIKit.framework in Frameworks */,
50 | 37BDF4D018FAF1DD00BF9705 /* Foundation.framework in Frameworks */,
51 | 697BDB73120B462A829D7D82 /* libPods.a in Frameworks */,
52 | 5C2AB08280C6430AA079590E /* libPods-JSONToolsTests.a in Frameworks */,
53 | );
54 | runOnlyForDeploymentPostprocessing = 0;
55 | };
56 | /* End PBXFrameworksBuildPhase section */
57 |
58 | /* Begin PBXGroup section */
59 | 2F23DB1222AFCB8158F59DB7 /* Pods */ = {
60 | isa = PBXGroup;
61 | children = (
62 | 8BAA2AFED5A38753C8438FB8 /* Pods.debug.xcconfig */,
63 | EA415B43DA52B212F752F99D /* Pods.release.xcconfig */,
64 | 2A04D7DA4D577E304B768C24 /* Pods-JSONToolsTests.debug.xcconfig */,
65 | 8A736BF437437DE4CA84B315 /* Pods-JSONToolsTests.release.xcconfig */,
66 | );
67 | name = Pods;
68 | sourceTree = "";
69 | };
70 | 37BDF4B418FAF1DD00BF9705 = {
71 | isa = PBXGroup;
72 | children = (
73 | 37BDF4D618FAF1DD00BF9705 /* JSONToolsTests */,
74 | 37BDF4BF18FAF1DD00BF9705 /* Frameworks */,
75 | 37BDF4BE18FAF1DD00BF9705 /* Products */,
76 | 37B7C5F919DCB7CD00395110 /* Pods.release.xcconfig */,
77 | 37B7C5FA19DCB7E000395110 /* Pods.debug.xcconfig */,
78 | 2F23DB1222AFCB8158F59DB7 /* Pods */,
79 | );
80 | sourceTree = "";
81 | };
82 | 37BDF4BE18FAF1DD00BF9705 /* Products */ = {
83 | isa = PBXGroup;
84 | children = (
85 | 37BDF4CD18FAF1DD00BF9705 /* JSONToolsTests.xctest */,
86 | );
87 | name = Products;
88 | sourceTree = "";
89 | };
90 | 37BDF4BF18FAF1DD00BF9705 /* Frameworks */ = {
91 | isa = PBXGroup;
92 | children = (
93 | 37BDF4C018FAF1DD00BF9705 /* Foundation.framework */,
94 | 37BDF4CE18FAF1DD00BF9705 /* XCTest.framework */,
95 | 37BDF4D118FAF1DD00BF9705 /* UIKit.framework */,
96 | B35914385F39418EB4099917 /* libPods.a */,
97 | 39FDE358F51848868E4334BA /* libPods-JSONToolsTests.a */,
98 | );
99 | name = Frameworks;
100 | sourceTree = "";
101 | };
102 | 37BDF4D618FAF1DD00BF9705 /* JSONToolsTests */ = {
103 | isa = PBXGroup;
104 | children = (
105 | 37BDF4DC18FAF1DD00BF9705 /* JSONPointerTests.m */,
106 | 37A5004E18FC3FC700A15B0F /* JSONPatchApplyTests.m */,
107 | 37A5005218FC9E0200A15B0F /* JSONPatchGenerateTests.m */,
108 | 37C8BA2C19106B74004D3E23 /* JSONSchemaTests.m */,
109 | 37BDF4D718FAF1DD00BF9705 /* Supporting Files */,
110 | );
111 | path = JSONToolsTests;
112 | sourceTree = "";
113 | };
114 | 37BDF4D718FAF1DD00BF9705 /* Supporting Files */ = {
115 | isa = PBXGroup;
116 | children = (
117 | 37BDF4D818FAF1DD00BF9705 /* JSONToolsTests-Info.plist */,
118 | 37BDF4D918FAF1DD00BF9705 /* InfoPlist.strings */,
119 | );
120 | name = "Supporting Files";
121 | sourceTree = "";
122 | };
123 | /* End PBXGroup section */
124 |
125 | /* Begin PBXNativeTarget section */
126 | 37BDF4CC18FAF1DD00BF9705 /* JSONToolsTests */ = {
127 | isa = PBXNativeTarget;
128 | buildConfigurationList = 37BDF4E318FAF1DD00BF9705 /* Build configuration list for PBXNativeTarget "JSONToolsTests" */;
129 | buildPhases = (
130 | E5FDDCD39D4A4E08B2DE1D8B /* Check Pods Manifest.lock */,
131 | 37BDF4C918FAF1DD00BF9705 /* Sources */,
132 | 37BDF4CA18FAF1DD00BF9705 /* Frameworks */,
133 | 37BDF4CB18FAF1DD00BF9705 /* Resources */,
134 | DE6B5C667CBA48DEB9CE40FD /* Copy Pods Resources */,
135 | );
136 | buildRules = (
137 | );
138 | dependencies = (
139 | );
140 | name = JSONToolsTests;
141 | productName = JSONToolsTests;
142 | productReference = 37BDF4CD18FAF1DD00BF9705 /* JSONToolsTests.xctest */;
143 | productType = "com.apple.product-type.bundle.unit-test";
144 | };
145 | /* End PBXNativeTarget section */
146 |
147 | /* Begin PBXProject section */
148 | 37BDF4B518FAF1DD00BF9705 /* Project object */ = {
149 | isa = PBXProject;
150 | attributes = {
151 | CLASSPREFIX = JSON;
152 | LastUpgradeCheck = 0510;
153 | ORGANIZATIONNAME = Sleestacks;
154 | };
155 | buildConfigurationList = 37BDF4B818FAF1DD00BF9705 /* Build configuration list for PBXProject "JSONToolsTests" */;
156 | compatibilityVersion = "Xcode 3.2";
157 | developmentRegion = English;
158 | hasScannedForEncodings = 0;
159 | knownRegions = (
160 | en,
161 | );
162 | mainGroup = 37BDF4B418FAF1DD00BF9705;
163 | productRefGroup = 37BDF4BE18FAF1DD00BF9705 /* Products */;
164 | projectDirPath = "";
165 | projectRoot = "";
166 | targets = (
167 | 37BDF4CC18FAF1DD00BF9705 /* JSONToolsTests */,
168 | );
169 | };
170 | /* End PBXProject section */
171 |
172 | /* Begin PBXResourcesBuildPhase section */
173 | 37BDF4CB18FAF1DD00BF9705 /* Resources */ = {
174 | isa = PBXResourcesBuildPhase;
175 | buildActionMask = 2147483647;
176 | files = (
177 | 37BDF4DB18FAF1DD00BF9705 /* InfoPlist.strings in Resources */,
178 | );
179 | runOnlyForDeploymentPostprocessing = 0;
180 | };
181 | /* End PBXResourcesBuildPhase section */
182 |
183 | /* Begin PBXShellScriptBuildPhase section */
184 | DE6B5C667CBA48DEB9CE40FD /* Copy Pods Resources */ = {
185 | isa = PBXShellScriptBuildPhase;
186 | buildActionMask = 2147483647;
187 | files = (
188 | );
189 | inputPaths = (
190 | );
191 | name = "Copy Pods Resources";
192 | outputPaths = (
193 | );
194 | runOnlyForDeploymentPostprocessing = 0;
195 | shellPath = /bin/sh;
196 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-JSONToolsTests/Pods-JSONToolsTests-resources.sh\"\n";
197 | showEnvVarsInLog = 0;
198 | };
199 | E5FDDCD39D4A4E08B2DE1D8B /* Check Pods Manifest.lock */ = {
200 | isa = PBXShellScriptBuildPhase;
201 | buildActionMask = 2147483647;
202 | files = (
203 | );
204 | inputPaths = (
205 | );
206 | name = "Check Pods Manifest.lock";
207 | outputPaths = (
208 | );
209 | runOnlyForDeploymentPostprocessing = 0;
210 | shellPath = /bin/sh;
211 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
212 | showEnvVarsInLog = 0;
213 | };
214 | /* End PBXShellScriptBuildPhase section */
215 |
216 | /* Begin PBXSourcesBuildPhase section */
217 | 37BDF4C918FAF1DD00BF9705 /* Sources */ = {
218 | isa = PBXSourcesBuildPhase;
219 | buildActionMask = 2147483647;
220 | files = (
221 | 37A5004F18FC3FC700A15B0F /* JSONPatchApplyTests.m in Sources */,
222 | 37C8BA2D19106B74004D3E23 /* JSONSchemaTests.m in Sources */,
223 | 37A5005318FC9E0200A15B0F /* JSONPatchGenerateTests.m in Sources */,
224 | 37BDF4DD18FAF1DD00BF9705 /* JSONPointerTests.m in Sources */,
225 | );
226 | runOnlyForDeploymentPostprocessing = 0;
227 | };
228 | /* End PBXSourcesBuildPhase section */
229 |
230 | /* Begin PBXVariantGroup section */
231 | 37BDF4D918FAF1DD00BF9705 /* InfoPlist.strings */ = {
232 | isa = PBXVariantGroup;
233 | children = (
234 | 37BDF4DA18FAF1DD00BF9705 /* en */,
235 | );
236 | name = InfoPlist.strings;
237 | sourceTree = "";
238 | };
239 | /* End PBXVariantGroup section */
240 |
241 | /* Begin XCBuildConfiguration section */
242 | 37BDF4DE18FAF1DD00BF9705 /* Debug */ = {
243 | isa = XCBuildConfiguration;
244 | buildSettings = {
245 | ALWAYS_SEARCH_USER_PATHS = NO;
246 | CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
247 | CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
248 | CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES;
249 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
250 | CLANG_CXX_LIBRARY = "libc++";
251 | CLANG_ENABLE_MODULES = YES;
252 | CLANG_ENABLE_OBJC_ARC = YES;
253 | CLANG_WARN_BOOL_CONVERSION = YES;
254 | CLANG_WARN_CONSTANT_CONVERSION = YES;
255 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
256 | CLANG_WARN_EMPTY_BODY = YES;
257 | CLANG_WARN_ENUM_CONVERSION = YES;
258 | CLANG_WARN_INT_CONVERSION = YES;
259 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
260 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
261 | GCC_C_LANGUAGE_STANDARD = gnu11;
262 | GCC_OPTIMIZATION_LEVEL = 0;
263 | GCC_PREPROCESSOR_DEFINITIONS = (
264 | "DEBUG=1",
265 | "$(inherited)",
266 | );
267 | GCC_SYMBOLS_PRIVATE_EXTERN = NO;
268 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
269 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
270 | GCC_WARN_SHADOW = YES;
271 | GCC_WARN_UNDECLARED_SELECTOR = YES;
272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
273 | GCC_WARN_UNUSED_FUNCTION = YES;
274 | GCC_WARN_UNUSED_VARIABLE = YES;
275 | IPHONEOS_DEPLOYMENT_TARGET = 7.1;
276 | ONLY_ACTIVE_ARCH = YES;
277 | SDKROOT = iphoneos;
278 | };
279 | name = Debug;
280 | };
281 | 37BDF4DF18FAF1DD00BF9705 /* Release */ = {
282 | isa = XCBuildConfiguration;
283 | buildSettings = {
284 | ALWAYS_SEARCH_USER_PATHS = NO;
285 | CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES;
286 | CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES;
287 | CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES;
288 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
289 | CLANG_CXX_LIBRARY = "libc++";
290 | CLANG_ENABLE_MODULES = YES;
291 | CLANG_ENABLE_OBJC_ARC = YES;
292 | CLANG_WARN_BOOL_CONVERSION = YES;
293 | CLANG_WARN_CONSTANT_CONVERSION = YES;
294 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
295 | CLANG_WARN_EMPTY_BODY = YES;
296 | CLANG_WARN_ENUM_CONVERSION = YES;
297 | CLANG_WARN_INT_CONVERSION = YES;
298 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
299 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
300 | ENABLE_NS_ASSERTIONS = NO;
301 | GCC_C_LANGUAGE_STANDARD = gnu11;
302 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
303 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
304 | GCC_WARN_SHADOW = YES;
305 | GCC_WARN_UNDECLARED_SELECTOR = YES;
306 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
307 | GCC_WARN_UNUSED_FUNCTION = YES;
308 | GCC_WARN_UNUSED_VARIABLE = YES;
309 | IPHONEOS_DEPLOYMENT_TARGET = 7.1;
310 | ONLY_ACTIVE_ARCH = YES;
311 | SDKROOT = iphoneos;
312 | VALIDATE_PRODUCT = YES;
313 | };
314 | name = Release;
315 | };
316 | 37BDF4E418FAF1DD00BF9705 /* Debug */ = {
317 | isa = XCBuildConfiguration;
318 | baseConfigurationReference = 2A04D7DA4D577E304B768C24 /* Pods-JSONToolsTests.debug.xcconfig */;
319 | buildSettings = {
320 | FRAMEWORK_SEARCH_PATHS = (
321 | "$(SDKROOT)/Developer/Library/Frameworks",
322 | "$(inherited)",
323 | "$(DEVELOPER_FRAMEWORKS_DIR)",
324 | );
325 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
326 | GCC_PREFIX_HEADER = "JSONTools/JSONTools-Prefix.pch";
327 | INFOPLIST_FILE = "JSONToolsTests/JSONToolsTests-Info.plist";
328 | ONLY_ACTIVE_ARCH = YES;
329 | PRODUCT_NAME = "$(TARGET_NAME)";
330 | WRAPPER_EXTENSION = xctest;
331 | };
332 | name = Debug;
333 | };
334 | 37BDF4E518FAF1DD00BF9705 /* Release */ = {
335 | isa = XCBuildConfiguration;
336 | baseConfigurationReference = 8A736BF437437DE4CA84B315 /* Pods-JSONToolsTests.release.xcconfig */;
337 | buildSettings = {
338 | FRAMEWORK_SEARCH_PATHS = (
339 | "$(SDKROOT)/Developer/Library/Frameworks",
340 | "$(inherited)",
341 | "$(DEVELOPER_FRAMEWORKS_DIR)",
342 | );
343 | GCC_PRECOMPILE_PREFIX_HEADER = YES;
344 | GCC_PREFIX_HEADER = "JSONTools/JSONTools-Prefix.pch";
345 | INFOPLIST_FILE = "JSONToolsTests/JSONToolsTests-Info.plist";
346 | ONLY_ACTIVE_ARCH = YES;
347 | PRODUCT_NAME = "$(TARGET_NAME)";
348 | WRAPPER_EXTENSION = xctest;
349 | };
350 | name = Release;
351 | };
352 | /* End XCBuildConfiguration section */
353 |
354 | /* Begin XCConfigurationList section */
355 | 37BDF4B818FAF1DD00BF9705 /* Build configuration list for PBXProject "JSONToolsTests" */ = {
356 | isa = XCConfigurationList;
357 | buildConfigurations = (
358 | 37BDF4DE18FAF1DD00BF9705 /* Debug */,
359 | 37BDF4DF18FAF1DD00BF9705 /* Release */,
360 | );
361 | defaultConfigurationIsVisible = 0;
362 | defaultConfigurationName = Release;
363 | };
364 | 37BDF4E318FAF1DD00BF9705 /* Build configuration list for PBXNativeTarget "JSONToolsTests" */ = {
365 | isa = XCConfigurationList;
366 | buildConfigurations = (
367 | 37BDF4E418FAF1DD00BF9705 /* Debug */,
368 | 37BDF4E518FAF1DD00BF9705 /* Release */,
369 | );
370 | defaultConfigurationIsVisible = 0;
371 | defaultConfigurationName = Release;
372 | };
373 | /* End XCConfigurationList section */
374 | };
375 | rootObject = 37BDF4B518FAF1DD00BF9705 /* Project object */;
376 | }
377 |
--------------------------------------------------------------------------------
/JSONTools/JSONSchemaValidator.m:
--------------------------------------------------------------------------------
1 | //
2 | // JSONSchemaValidator.m
3 | // JSONTools
4 | //
5 | // Copyright (C) 2014 Gregory Combs [gcombs at gmail]
6 | // See LICENSE.txt for details.
7 |
8 | #import "JSONSchemaValidator.h"
9 | //#include
10 |
11 | @interface JSONSchemaValidator ()
12 | @property (nonatomic,readonly) NSCalendar *isoCalendar;
13 | @end
14 |
15 | @implementation JSONSchemaValidator
16 |
17 | - (id)init
18 | {
19 | self = [super init];
20 | if (self)
21 | {
22 | _formatValidationEnabled = YES;
23 | }
24 | return self;
25 | }
26 |
27 | - (BOOL)_validateJSONString:(NSString *)jsonString withSchemaDict:(NSDictionary *)schema
28 | {
29 | NSNumber *validationNumber = schema[@"maxLength"];
30 | if (validationNumber &&
31 | ![self validateJSONString:jsonString maximumLength:validationNumber])
32 | {
33 | return NO;
34 | }
35 |
36 | validationNumber = schema[@"minLength"];
37 | if (validationNumber &&
38 | ![self validateJSONString:jsonString minimumLength:validationNumber])
39 | {
40 | return NO;
41 | }
42 |
43 | NSString *validationString = schema[@"pattern"];
44 | if (validationString &&
45 | ![self validateJSONString:jsonString pattern:validationString])
46 | {
47 | return NO;
48 | }
49 |
50 | if (self.isFormatValidationEnabled)
51 | {
52 | validationString = schema[@"format"];
53 | if (validationString &&
54 | ![self validateJSONString:jsonString format:validationString])
55 | {
56 | return NO;
57 | }
58 | }
59 |
60 | return YES;
61 | }
62 |
63 | - (BOOL)validateJSONString:(NSString *)jsonString maximumLength:(NSNumber *)maxLength
64 | {
65 | if (maxLength && [maxLength respondsToSelector:@selector(intValue)])
66 | {
67 | //A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword.
68 | if (jsonString.length > maxLength.intValue)
69 | {
70 | return NO;
71 | }
72 | }
73 | return YES;
74 | }
75 |
76 | - (BOOL)validateJSONString:(NSString *)jsonString minimumLength:(NSNumber *)minLength
77 | {
78 | if (minLength && [minLength respondsToSelector:@selector(intValue)])
79 | {
80 | //A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword.
81 | if (jsonString.length < minLength.intValue)
82 | {
83 | return NO;
84 | }
85 | }
86 | return YES;
87 | }
88 |
89 | - (BOOL)validateJSONString:(NSString *)jsonString pattern:(NSString *)pattern
90 | {
91 | if (pattern && [pattern isKindOfClass:[NSString class]])
92 | {
93 | //A string instance is considered valid if the regular expression matches the instance successfully. Recall: regular expressions are not implicitly anchored.
94 | //This string SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect.
95 | //NOTE: this regex uses ICU which has some differences to ECMA-262 (such as look-behind)
96 | NSError *error = nil;
97 | NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&error];
98 | if (!error && regex)
99 | {
100 | NSRange matchRange = [regex rangeOfFirstMatchInString:jsonString options:0 range:NSMakeRange(0, jsonString.length)];
101 | if (matchRange.location == NSNotFound ||
102 | matchRange.length == 0)
103 | {
104 | //A string instance is considered valid if the regular expression matches the instance successfully. Recall: regular expressions are not implicitly anchored.
105 | return NO;
106 | }
107 | }
108 | }
109 | return YES;
110 | }
111 |
112 | - (BOOL)validateJSONString:(NSString *)jsonString format:(NSString *)format
113 | {
114 | static NSArray * formatTypes;
115 | static dispatch_once_t onceToken;
116 | dispatch_once(&onceToken, ^{
117 | formatTypes = @[@"date-time", @"email", @"hostname", @"ipv4", @"ipv6", @"uri"];
118 | });
119 |
120 | if (!format ||
121 | ![formatTypes containsObject:format])
122 | {
123 | return YES;
124 | }
125 | if (!jsonString ||
126 | ![jsonString isKindOfClass:[NSString class]])
127 | {
128 | return NO; // with a known format type, the non-string doesn't conform
129 | }
130 |
131 | if ([format isEqualToString:@"hostname"])
132 | {
133 | return [self validateJSONStringAsHostname:jsonString];
134 | }
135 |
136 | if ([format isEqualToString:@"email"])
137 | {
138 | return [self validateJSONStringAsEmail:jsonString];
139 | }
140 |
141 | if ([format isEqualToString:@"uri"])
142 | {
143 | return [self validateJSONStringAsURI:jsonString];
144 | }
145 |
146 | if ([format isEqualToString:@"ipv4"])
147 | {
148 | return [self validateJSONStringAsIPv4:jsonString];
149 | }
150 |
151 | if ([format isEqualToString:@"ipv6"])
152 | {
153 | return [self validateJSONStringAsIPv6:jsonString];
154 | }
155 |
156 | if ([format isEqualToString:@"date-time"])
157 | {
158 | return [self validateJSONStringAsDateTime:jsonString];
159 | }
160 |
161 | return YES;
162 | }
163 |
164 | - (BOOL)validateJSONStringAsHostname:(NSString *)jsonString
165 | {
166 | /* RFC 1034, Section 3.1 */
167 |
168 | if (!jsonString.length)
169 | {
170 | /*
171 | One label is reserved, and that is the null (i.e., zero length) label used for the root
172 | */
173 | return NO;
174 | }
175 |
176 | if (jsonString.length > 255)
177 | {
178 | /*
179 | To simplify implementations, the total number of octets that represent a
180 | domain name (i.e., the sum of all label octets and label lengths) is
181 | limited to 255.
182 | */
183 | return NO;
184 | }
185 |
186 | if (![jsonString canBeConvertedToEncoding:NSASCIIStringEncoding])
187 | {
188 | /*
189 | ... domain name comparisons for all present domain functions are done in a
190 | case-insensitive manner, assuming an ASCII character set, and a high
191 | order zero bit.
192 | */
193 | return NO;
194 | }
195 |
196 | for (NSString *component in [jsonString componentsSeparatedByString:@"."])
197 | {
198 | /* Each node has a label, which is zero to 63 octets in length */
199 | if (component.length > 63)
200 | {
201 | return NO;
202 | }
203 | }
204 |
205 | static NSRegularExpression *rfc1034Check = nil;
206 | static dispatch_once_t onceToken;
207 | dispatch_once(&onceToken, ^{
208 | NSString *pattern = @"^([a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?\\.)*[a-zA-Z0-9]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?$";
209 | NSError *error = NULL;
210 | rfc1034Check = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
211 | NSParameterAssert(rfc1034Check != NULL && !error);
212 | });
213 |
214 | NSRange inputRange = NSMakeRange(0, jsonString.length);
215 | NSUInteger numberOfMatches = [rfc1034Check numberOfMatchesInString:jsonString options:0 range:inputRange];
216 |
217 | return (numberOfMatches == 1);
218 | }
219 |
220 | - (BOOL)validateJSONStringAsEmail:(NSString *)jsonString
221 | {
222 | /* RFC 5322, Section 3.4.1 */
223 |
224 | if (!jsonString.length)
225 | {
226 | return NO;
227 | }
228 |
229 | static NSRegularExpression *emailCheck = nil;
230 | static dispatch_once_t onceToken;
231 | dispatch_once(&onceToken, ^{
232 | //NSString *pattern = @"^[+\\w\\.\\-']+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*(\\.[a-zA-Z]{2,})+$";
233 | NSString *pattern = @"^([\\!#\\$%&'\\*\\+/\\=?\\^`\{\\|\\}~a-zA-Z0-9_-]+[\\.]?)+[\\!#\\$%&'\\*\\+/\\=?\\^`\{\\|\\}~a-zA-Z0-9_-]+@{1}((([0-9A-Za-z_-]+)([\\.]{1}[0-9A-Za-z_-]+)*\\.{1}([A-Za-z]){1,6})|(([0-9]{1,3}[\\.]{1}){3}([0-9]{1,3}){1}))$";
234 | NSError *error = NULL;
235 | emailCheck = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
236 | NSParameterAssert(emailCheck != NULL && !error);
237 | });
238 | NSRange inputRange = NSMakeRange(0, jsonString.length);
239 | NSUInteger numberOfMatches = [emailCheck numberOfMatchesInString:jsonString options:0 range:inputRange];
240 |
241 | return (numberOfMatches == 1);
242 | }
243 |
244 | - (BOOL)validateJSONStringAsURI:(NSString *)jsonString
245 | {
246 | /* RFC 3986 */
247 |
248 | if (!jsonString.length)
249 | {
250 | return NO;
251 | }
252 |
253 | static NSRegularExpression *uriCheck = nil;
254 | static dispatch_once_t onceToken;
255 | dispatch_once(&onceToken, ^{
256 | NSString *pattern = @"^([a-zA-Z][a-zA-Z0-9+-.]*):((/\\/(((([a-zA-Z0-9\\-._~!$&'()*+,;=':]|(%[0-9a-fA-F]{2}))*)@)?((\\[((((([0-9a-fA-F]{1,4}:){6}|(::([0-9a-fA-F]{1,4}:){5})|(([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:){4})|((([0-9a-fA-F]{1,4}:)?[0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:){3})|((([0-9a-fA-F]{1,4}:){0,2}[0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:){2})|((([0-9a-fA-F]{1,4}:){0,3}[0-9a-fA-F]{1,4})?::[0-9a-fA-F]{1,4}:)|((([0-9a-fA-F]{1,4}:){0,4}[0-9a-fA-F]{1,4})?::))((([0-9a-fA-F]{1,4}):([0-9a-fA-F]{1,4}))|(([0-9]|(1[0-9]{2})|(2[0-4][0-9])|(25[0-5]))\\.([0-9]|(1[0-9]{2})|(2[0-4][0-9])|(25[0-5]))\\.([0-9]|(1[0-9]{2})|(2[0-4][0-9])|(25[0-5]))\\.([0-9]|(1[0-9]{2})|(2[0-4][0-9])|(25[0-5])))))|((([0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4})?::[0-9a-fA-F]{1,4})|((([0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4})?::))|(v[0-9a-fA-F]+\\.[a-zA-Z0-9\\-._~!$&'()*+,;=':]+))\\])|(([0-9]|(1[0-9]{2})|(2[0-4][0-9])|(25[0-5]))\\.([0-9]|(1[0-9]{2})|(2[0-4][0-9])|(25[0-5]))\\.([0-9]|(1[0-9]{2})|(2[0-4][0-9])|(25[0-5]))\\.([0-9]|(1[0-9]{2})|(2[0-4][0-9])|(25[0-5])))|(([a-zA-Z0-9\\-._~!$&'()*+,;=']|(%[0-9a-fA-F]{2}))*))(:[0-9]*)?)((\\/([a-zA-Z0-9\\-._~!$&'()*+,;=':@]|(%[0-9a-fA-F]{2}))*)*))|(\\/?(([a-zA-Z0-9\\-._~!$&'()*+,;=':@]|(%[0-9a-fA-F]{2}))+(\\/([a-zA-Z0-9\\-._~!$&'()*+,;=':@]|(%[0-9a-fA-F]{2}))*)*)?))(\\?(([a-zA-Z0-9\\-._~!$&'()*+,;=':@\\/?]|(%[0-9a-fA-F]{2}))*))?((#(([a-zA-Z0-9\\-._~!$&'()*+,;=':@\\/?]|(%[0-9a-fA-F]{2}))*)))?$";
257 | NSError *error = NULL;
258 | uriCheck = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
259 | NSParameterAssert(uriCheck != NULL && !error);
260 | });
261 | NSRange inputRange = NSMakeRange(0, jsonString.length);
262 | NSUInteger numberOfMatches = [uriCheck numberOfMatchesInString:jsonString options:0 range:inputRange];
263 |
264 | return (numberOfMatches == 1);
265 | }
266 |
267 | - (BOOL)validateJSONStringAsIPv4:(NSString *)jsonString
268 | {
269 | /* RFC 2673, Section 3.2 */
270 |
271 | if (!jsonString.length)
272 | {
273 | return NO;
274 | }
275 |
276 | static NSRegularExpression *ipv4check = nil;
277 | static dispatch_once_t onceToken;
278 | dispatch_once(&onceToken, ^{
279 | NSString *pattern = @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
280 | NSError *error = NULL;
281 | ipv4check = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
282 | NSParameterAssert(ipv4check != NULL && !error);
283 | });
284 | NSRange inputRange = NSMakeRange(0, jsonString.length);
285 | NSUInteger numberOfMatches = [ipv4check numberOfMatchesInString:jsonString options:0 range:inputRange];
286 |
287 | return (numberOfMatches == 1);
288 | }
289 | - (BOOL)validateJSONStringAsIPv6:(NSString *)jsonString
290 | {
291 | /* RFC 2373, Section 2.2 */
292 |
293 | if (!jsonString.length)
294 | {
295 | return NO;
296 | }
297 |
298 | static NSRegularExpression *ipv6check = nil;
299 | static dispatch_once_t onceToken;
300 | dispatch_once(&onceToken, ^{
301 | NSString *pattern = @"^(^(([0-9A-F]{1,4}(((:[0-9A-F]{1,4}){5}::[0-9A-F]{1,4})|((:[0-9A-F]{1,4}){4}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,1})|((:[0-9A-F]{1,4}){3}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,2})|((:[0-9A-F]{1,4}){2}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,3})|(:[0-9A-F]{1,4}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,4})|(::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,5})|(:[0-9A-F]{1,4}){7}))$|^(::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,6})$)|^::$)|^((([0-9A-F]{1,4}(((:[0-9A-F]{1,4}){3}::([0-9A-F]{1,4}){1})|((:[0-9A-F]{1,4}){2}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,1})|((:[0-9A-F]{1,4}){1}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,2})|(::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,3})|((:[0-9A-F]{1,4}){0,5})))|([:]{2}[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,4})):|::)((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{0,2})\\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{0,2})$$";
302 |
303 | NSError *error = NULL;
304 | ipv6check = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
305 | NSParameterAssert(ipv6check != NULL && !error);
306 | });
307 | NSRange inputRange = NSMakeRange(0, jsonString.length);
308 | NSUInteger numberOfMatches = [ipv6check numberOfMatchesInString:jsonString options:0 range:inputRange];
309 |
310 | return (numberOfMatches == 1);
311 | }
312 |
313 | #define DATETIME_WITH_EASY_WAY 1
314 |
315 | - (BOOL)validateJSONStringAsDateTime:(NSString *)jsonString
316 | {
317 | /* RFC 3339, Section 5.6 */
318 |
319 | #if DATETIME_WITH_EASY_WAY
320 | NSDate *date = [self dateForRFC3339DateTimeString:jsonString];
321 | return (date != NULL);
322 | #else
323 |
324 | static NSRegularExpression *dateCheck = nil;
325 | static dispatch_once_t onceToken;
326 | dispatch_once(&onceToken, ^{
327 | NSString *pattern = @"^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-4][0-9]|5[0-9]):([0-5][0-9]|60)(\\.[0-9]+)?(Z|([+-][01][0-9]|2[0-3]):([0-4][0-9]|5[0-9]))$";
328 |
329 | NSError *error = NULL;
330 | dateCheck = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:&error];
331 | NSParameterAssert(dateCheck != NULL && !error);
332 | });
333 | NSRange inputRange = NSMakeRange(0, jsonString.length);
334 | NSTextCheckingResult *match = [dateCheck firstMatchInString:jsonString options:0 range:inputRange];
335 | if (!match)
336 | return NO;
337 |
338 | @try {
339 | NSDateComponents *components = [[NSDateComponents alloc] init];
340 | components.calendar = [self isoCalendar];
341 |
342 | components.year = [self componentForDate:jsonString index:1 match:match];
343 | if (components.year == -1)
344 | return NO;
345 | components.month = [self componentForDate:jsonString index:2 match:match];
346 | if (components.month == -1)
347 | return NO;
348 | components.day = [self componentForDate:jsonString index:3 match:match];
349 | if (![self isValidDay:components.day forMonth:components.month year:components.year])
350 | return NO;
351 | components.hour = [self componentForDate:jsonString index:4 match:match];
352 | if (components.hour == -1)
353 | return NO;
354 | components.minute = [self componentForDate:jsonString index:5 match:match];
355 | if (components.minute == -1)
356 | return NO;
357 | components.second = [self componentForDate:jsonString index:6 match:match];
358 | if (components.second == -1)
359 | return NO;
360 | if ([self componentForDate:jsonString index:7 match:match] == -1) // msec
361 | return NO;
362 | NSInteger tzHour = [self componentForDate:jsonString index:8 match:match];
363 | if (tzHour == -1)
364 | return NO;
365 | NSInteger tzMin = [self componentForDate:jsonString index:9 match:match];
366 | if (tzMin == -1)
367 | return NO;
368 | NSInteger secondsFromGMT = (tzHour * 60 * 60) + (tzMin * 60);
369 | components.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:secondsFromGMT];
370 | if (!components.date)
371 | return NO;
372 |
373 | components.timeZone = nil; // clear it because this part grabs a named timezone instead of an offset
374 | NSDateComponents *newComponents = [[self isoCalendar] components:(NSCalendarUnitCalendar |
375 | NSCalendarUnitYear |
376 | NSCalendarUnitMonth |
377 | NSCalendarUnitDay |
378 | NSCalendarUnitHour |
379 | NSCalendarUnitMinute |
380 | NSCalendarUnitSecond) // not timezone
381 | fromDate:components.date];
382 | if (![newComponents isEqual:components])
383 | return NO;
384 | }
385 | @catch (NSException *exception) {
386 | return NO;
387 | }
388 | return YES;
389 | #endif
390 | }
391 |
392 | #if DATETIME_WITH_EASY_WAY
393 | - (NSDate *)dateForRFC3339DateTimeString:(NSString *)dateString
394 | {
395 | static NSDateFormatter * rfc3339_1 = nil;
396 | static NSDateFormatter * rfc3339_2 = nil;
397 | static dispatch_once_t onceToken;
398 | dispatch_once(&onceToken, ^{
399 | NSLocale * posixLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
400 |
401 | rfc3339_1 = [[NSDateFormatter alloc] init];
402 | rfc3339_1.locale = posixLocale;
403 | rfc3339_1.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSZ";
404 | rfc3339_1.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
405 |
406 | rfc3339_2 = [[NSDateFormatter alloc] init];
407 | rfc3339_2.locale = posixLocale;
408 | rfc3339_2.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZZZ";
409 | rfc3339_2.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
410 | });
411 |
412 | NSDate *date = [rfc3339_1 dateFromString:dateString];
413 | if (!date)
414 | date = [rfc3339_2 dateFromString:dateString];
415 |
416 | /* This would be awesome/faster, but it fails to 'fail' on invalid leap year days (Feb 29 1963 returns Mar 1)
417 |
418 | struct tm sometime;
419 | const char *formatString = "%Y-%m-%dT%H:%M:%S %z";
420 | const char *cDate = [dateString cStringUsingEncoding:NSASCIIStringEncoding];
421 | strptime(cDate, formatString, &sometime);
422 | date = [NSDate dateWithTimeIntervalSince1970: mktime(&sometime)];
423 | */
424 |
425 | return date;
426 | }
427 |
428 |
429 | #else
430 |
431 | - (NSCalendar *)isoCalendar
432 | {
433 | static NSCalendar *isoCalendar = nil;
434 | static dispatch_once_t onceToken;
435 | dispatch_once(&onceToken, ^{
436 | isoCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSISO8601Calendar];
437 | });
438 | return isoCalendar;
439 | }
440 |
441 | - (BOOL)isValidDay:(NSUInteger)day forMonth:(NSUInteger)month year:(NSUInteger)year
442 | {
443 | NSCalendar *isoCalendar = [self isoCalendar];
444 | NSDateComponents *components = [[NSDateComponents alloc] init];
445 | components.year = year;
446 | components.month = month;
447 | components.calendar = isoCalendar;
448 | components.day = 1;
449 | NSDate *date = [components date];
450 | NSRange range = [isoCalendar rangeOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitMonth forDate:date];
451 | return NSLocationInRange(day, range);
452 | }
453 |
454 | - (NSInteger)componentForDate:(NSString *)dateString index:(unsigned int)index match:(NSTextCheckingResult *)dateResult
455 | {
456 | if (dateResult.numberOfRanges <= index)
457 | return -1;
458 | NSRange matchRange = [dateResult rangeAtIndex:index];
459 | if (matchRange.location == NSNotFound ||
460 | matchRange.length == 0)
461 | return 0;
462 | NSString *componentString = [dateString substringWithRange:matchRange];
463 | return [componentString integerValue];
464 | }
465 |
466 | #endif
467 |
468 | @end
469 |
--------------------------------------------------------------------------------