├── 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 | [![Build Status](https://travis-ci.org/grgcombs/JSONTools.svg?branch=master)](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 | --------------------------------------------------------------------------------