From a18663fee5d846892566232656da07beca509ad4 Mon Sep 17 00:00:00 2001 From: Werner Altewischer Date: Wed, 7 Mar 2018 16:46:30 +0100 Subject: [PATCH 1/5] - Updated project to recommended settings - Added MacOSX as platform to pod spec - Added Nullability annotations - Added error propagation to be able to inspect the reason of validation failure --- KiteJSONValidator.podspec | 6 +- KiteJSONValidator.xcodeproj/project.pbxproj | 57 ++- .../xcschemes/KiteJSONResources.xcscheme | 15 +- .../xcschemes/KiteJSONValidatorTests.xcscheme | 15 +- Resources/KiteJSONValidator-Info.plist | 2 +- Sources/KiteJSONValidator.h | 24 +- Sources/KiteJSONValidator.m | 373 ++++++++++++++---- Tests/KiteJSONValidatorTests.m | 4 +- Tests/Tests-Info.plist | 2 +- 9 files changed, 381 insertions(+), 117 deletions(-) diff --git a/KiteJSONValidator.podspec b/KiteJSONValidator.podspec index dcf5b5a..414146c 100644 --- a/KiteJSONValidator.podspec +++ b/KiteJSONValidator.podspec @@ -15,9 +15,9 @@ Pod::Spec.new do |s| s.homepage = "https://github.com/samskiter/KiteJSONValidator" s.license = "MIT" s.authors = { "Sam Duke" => "samskiter@users.noreply.github.com" } - s.version = "0.2.2" - s.source = { :git => "https://github.com/samskiter/KiteJSONValidator.git", :tag => "v#{s.version}"} - s.platform = :ios, '7.0' + s.version = "0.2.3" + s.source = { :git => "https://github.com/samskiter/KiteJSONValidator.git", :tag => "v#{s.version}" } + s.platforms = { :ios => '7.0', :osx => '10.9' } s.requires_arc = true s.source_files = "Sources/*.{h,m}" s.xcconfig = { diff --git a/KiteJSONValidator.xcodeproj/project.pbxproj b/KiteJSONValidator.xcodeproj/project.pbxproj index 21939bf..c77124f 100644 --- a/KiteJSONValidator.xcodeproj/project.pbxproj +++ b/KiteJSONValidator.xcodeproj/project.pbxproj @@ -204,7 +204,7 @@ isa = PBXProject; attributes = { CLASSPREFIX = Kite; - LastUpgradeCheck = 0610; + LastUpgradeCheck = 0920; ORGANIZATIONNAME = KiteJSONValidator; }; buildConfigurationList = 67B6B112188C32E800E1630A /* Build configuration list for PBXProject "KiteJSONValidator" */; @@ -301,6 +301,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = "Resources/KiteJSONValidator-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.kitejsonvalidator.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = KiteJSONValidator; SKIP_INSTALL = YES; WRAPPER_EXTENSION = bundle; @@ -313,6 +314,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = "Resources/KiteJSONValidator-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.kitejsonvalidator.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = KiteJSONValidator; SKIP_INSTALL = YES; WRAPPER_EXTENSION = bundle; @@ -322,14 +324,36 @@ 67B6B113188C32E800E1630A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_MULTIPLE_DEFINITION_TYPES_FOR_SELECTOR = YES; GCC_WARN_STRICT_SELECTOR_MATCH = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; @@ -339,14 +363,35 @@ 67B6B114188C32E800E1630A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_MULTIPLE_DEFINITION_TYPES_FOR_SELECTOR = YES; GCC_WARN_STRICT_SELECTOR_MATCH = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; }; @@ -400,7 +445,8 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "Tests/Tests-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + PRODUCT_BUNDLE_IDENTIFIER = "samskiter.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = xctest; }; @@ -448,7 +494,8 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = "Tests/Tests-Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 7.0; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + PRODUCT_BUNDLE_IDENTIFIER = "samskiter.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; VALIDATE_PRODUCT = YES; WRAPPER_EXTENSION = xctest; diff --git a/KiteJSONValidator.xcodeproj/xcshareddata/xcschemes/KiteJSONResources.xcscheme b/KiteJSONValidator.xcodeproj/xcshareddata/xcschemes/KiteJSONResources.xcscheme index 19b2efd..3929af3 100644 --- a/KiteJSONValidator.xcodeproj/xcshareddata/xcschemes/KiteJSONResources.xcscheme +++ b/KiteJSONValidator.xcodeproj/xcshareddata/xcschemes/KiteJSONResources.xcscheme @@ -1,6 +1,6 @@ + language = "" + shouldUseLaunchSchemeArgsEnv = "YES"> + + + language = "" + shouldUseLaunchSchemeArgsEnv = "YES"> @@ -53,15 +54,19 @@ + + CFBundleIconFile CFBundleIdentifier - com.kitejsonvalidator.${PRODUCT_NAME:rfc1034identifier} + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName diff --git a/Sources/KiteJSONValidator.h b/Sources/KiteJSONValidator.h index b94ddcf..06d8c88 100644 --- a/Sources/KiteJSONValidator.h +++ b/Sources/KiteJSONValidator.h @@ -8,11 +8,13 @@ #import +NS_ASSUME_NONNULL_BEGIN + @protocol KiteJSONSchemaRefDelegate; @interface KiteJSONValidator : NSObject -@property (nonatomic, weak) id delegate; +@property (nonatomic, weak, nullable) id delegate; /** Validates json against a draft4 schema. @@ -22,9 +24,9 @@ @param schemaData The draft4 JSON schema to validate against @return Whether the json is validated. */ --(BOOL)validateJSONData:(NSData*)jsonData withSchemaData:(NSData*)schemaData; --(BOOL)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema; --(BOOL)validateJSONInstance:(id)json withSchemaData:(NSData*)schemaData; +-(BOOL)validateJSONData:(NSData*)jsonData withSchemaData:(NSData*)schemaData error:(NSError **)error; +-(BOOL)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema error:(NSError **)error; +-(BOOL)validateJSONInstance:(id)json withSchemaData:(NSData*)schemaData error:(NSError **)error; //TODO:add an interface to add a schema with a key, allowing a schema to only be validated once and then reused /** @@ -35,7 +37,7 @@ @return Whether the reference schema was successfully added. */ --(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url; +-(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url error:(NSError **)error; /** Used for adding an ENTIRE document to the list of reference schemas - the URL should therefore be fragmentless. @@ -46,7 +48,7 @@ @return Whether the reference schema was successfully added. */ --(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BOOL)shouldValidateSchema; +-(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BOOL)shouldValidateSchema error:(NSError **)error; /** Used for adding an ENTIRE document to the list of reference schemas - the URL should therefore be fragmentless. @@ -56,7 +58,7 @@ @return Whether the reference schema was successfully added. */ --(BOOL)addRefSchema:(NSDictionary*)schema atURL:(NSURL*)url; +-(BOOL)addRefSchema:(NSDictionary*)schema atURL:(NSURL*)url error:(NSError **)error; /** Used for adding an ENTIRE document to the list of reference schemas - the URL should therefore be fragmentless. @@ -67,13 +69,15 @@ @return Whether the reference schema was successfully added. */ --(BOOL)addRefSchema:(NSDictionary *)schema atURL:(NSURL *)url validateSchema:(BOOL)shouldValidateSchema; +-(BOOL)addRefSchema:(NSDictionary *)schema atURL:(NSURL *)url validateSchema:(BOOL)shouldValidateSchema error:(NSError **)error; @end @protocol KiteJSONSchemaRefDelegate --(NSData*)schemaValidator:(KiteJSONValidator*)validator requiresSchemaDataForRefURL:(NSURL*)refURL; --(NSDictionary*)schemaValidator:(KiteJSONValidator*)validator requiresSchemaForRefURL:(NSURL*)refURL; +-(nullable NSData*)schemaValidator:(KiteJSONValidator*)validator requiresSchemaDataForRefURL:(NSURL*)refURL; +-(nullable NSDictionary*)schemaValidator:(KiteJSONValidator*)validator requiresSchemaForRefURL:(NSURL*)refURL; @end + +NS_ASSUME_NONNULL_END diff --git a/Sources/KiteJSONValidator.m b/Sources/KiteJSONValidator.m index fdca952..6a8859c 100644 --- a/Sources/KiteJSONValidator.m +++ b/Sources/KiteJSONValidator.m @@ -35,7 +35,7 @@ -(id)init NSDictionary *rootSchema = [self rootSchema]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-variable" - BOOL success = [self addRefSchema:rootSchema atURL:rootURL validateSchema:NO]; + BOOL success = [self addRefSchema:rootSchema atURL:rootURL validateSchema:NO error:nil]; #pragma clang diagnostic pop NSAssert(success == YES, @"Unable to add the root schema!", nil); } @@ -43,38 +43,50 @@ -(id)init return self; } --(BOOL)addRefSchema:(NSDictionary *)schema atURL:(NSURL *)url validateSchema:(BOOL)shouldValidateSchema +-(BOOL)addRefSchema:(NSDictionary *)schema atURL:(NSURL *)url validateSchema:(BOOL)shouldValidateSchema error:(NSError **)error { - NSError * error; //We convert to data in order to protect ourselves against a cyclic structure and ensure we have valid JSON - NSData * schemaData = [NSJSONSerialization dataWithJSONObject:schema options:0 error:&error]; - if (error != nil) { + NSError *detailedError = nil; + NSData * schemaData = [NSJSONSerialization dataWithJSONObject:schema options:0 error:&detailedError]; + if (schemaData == nil) { + if (error) { + *error = [self validationErrorWithDescription:@"Schema data is not valid JSON data" forURL:url detailedError:detailedError]; + } return NO; } - return [self addRefSchemaData:schemaData atURL:url validateSchema:shouldValidateSchema]; + return [self addRefSchemaData:schemaData atURL:url validateSchema:shouldValidateSchema error:error]; } --(BOOL)addRefSchema:(NSDictionary*)schema atURL:(NSURL*)url +-(BOOL)addRefSchema:(NSDictionary*)schema atURL:(NSURL*)url error:(NSError **)error { - return [self addRefSchema:schema atURL:url validateSchema:YES]; + return [self addRefSchema:schema atURL:url validateSchema:YES error:error]; } --(BOOL)addRefSchemaData:(NSData *)schemaData atURL:(NSURL *)url +-(BOOL)addRefSchemaData:(NSData *)schemaData atURL:(NSURL *)url error:(NSError **)error { - return [self addRefSchemaData:schemaData atURL:url validateSchema:YES]; + return [self addRefSchemaData:schemaData atURL:url validateSchema:YES error:error]; } --(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BOOL)shouldValidateSchema +-(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BOOL)shouldValidateSchema error:(NSError **)error { if (!schemaData || ![schemaData isKindOfClass:[NSData class]]) { + if (error) { + *error = [self validationErrorWithDescription:@"Schema data is nil or no instance of NSData" forURL:url detailedError:nil]; + } return NO; } - NSError * error = nil; - id schema = [NSJSONSerialization JSONObjectWithData:schemaData options:0 error:&error]; - if (error != nil) { + NSError*detailedError = nil; + id schema = [NSJSONSerialization JSONObjectWithData:schemaData options:0 error:&detailedError]; + if (schema == nil) { + if (error) { + *error = [self validationErrorWithDescription:@"Schema data could not be converted to json object" forURL:url detailedError:detailedError]; + } return NO; } else if (![schema isKindOfClass:[NSDictionary class]]) { + if (error) { + *error = [self validationErrorWithDescription:@"Schema data does not have a root level dictionary" forURL:url detailedError:nil]; + } return NO; } @@ -83,7 +95,9 @@ -(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BO if (!url || !schema) { - //NSLog(@"Invalid schema for URL (%@): %@", url, schema); + if (error) { + *error = [self validationErrorWithDescription:@"URL or schema is not defined" forURL:nil detailedError:nil]; + } return NO; } url = [self urlWithoutFragment:url]; @@ -94,9 +108,14 @@ -(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BO NSDictionary *root = [self rootSchema]; if (![root isEqualToDictionary:schema]) { - BOOL isValidSchema = [self validateJSON:schema withSchemaDict:root]; + BOOL isValidSchema = [self validateJSON:schema withSchemaDict:root error:&detailedError]; NSAssert(isValidSchema == YES, @"Invalid schema", nil); - if (!isValidSchema) return NO; + if (!isValidSchema) { + if (error) { + *error = [self validationErrorWithDescription:@"Supplied schema is not valid according to the root level schema specification" forURL:url detailedError:detailedError]; + } + return NO; + } } else { @@ -177,11 +196,14 @@ -(NSURL*)urlWithoutFragment:(NSURL*)url return [NSURL URLWithString:refString]; } --(BOOL)validateJSON:(id)json withSchemaAtReference:(NSString*)refString +-(BOOL)validateJSON:(id)json withSchemaAtReference:(NSString*)refString error:(NSError **)error { NSURL * refURI = [NSURL URLWithString:refString relativeToURL:self.resolutionStack.lastObject]; if (!refURI) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Reference could not be converted to a correct URL. Failed ref='%@', base url='%@'", refString, self.resolutionStack.lastObject] forURL:nil detailedError:nil]; + } return NO; } @@ -208,6 +230,9 @@ -(BOOL)validateJSON:(id)json withSchemaAtReference:(NSString*)refString } if (!schema) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"No schema could be resolved for URL: %@", refURI] forURL:refURI detailedError:nil]; + } return NO; } @@ -228,10 +253,13 @@ -(BOOL)validateJSON:(id)json withSchemaAtReference:(NSString*)refString } if (!schema) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Schema fragment could be resolved for pointer: %@", pointerComponents] forURL:refURI detailedError:nil]; + } return NO; } } - BOOL result = [self _validateJSON:json withSchemaDict:schema]; + BOOL result = [self _validateJSON:json withSchemaDict:schema error:error]; if (newDocument) { [self removeResolution]; } @@ -264,9 +292,8 @@ -(void)removeResolution [self.schemaStack removeLastObject]; } --(BOOL)validateJSONInstance:(id)json withSchemaData:(NSData*)schemaData +-(BOOL)validateJSONInstance:(id)json withSchemaData:(NSData*)schemaData error:(NSError **)error { - NSError * error = nil; NSString * jsonKey = nil; if (![NSJSONSerialization isValidJSONObject:json]) { #ifdef DEBUG @@ -275,19 +302,26 @@ -(BOOL)validateJSONInstance:(id)json withSchemaData:(NSData*)schemaData json = @{jsonKey : json}; // schema = @{@"properties" : @{@"debugInvalidTopTypeKey" : schema}}; #else + if (error) { + *error = [self validationErrorWithDescription:@"Supplied object is not a valid JSON object" forURL:nil detailedError:nil]; + } return NO; #endif } - NSData * jsonData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; - if (error != nil) { + NSError * detailedError = nil; + NSData * jsonData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&detailedError]; + if (jsonData == nil) { + if (error) { + *error = [self validationErrorWithDescription:@"Supplied object could not be converted to json data" forURL:nil detailedError:detailedError]; + } return NO; } - return [self validateJSONData:jsonData withKey:jsonKey withSchemaData:schemaData]; + return [self validateJSONData:jsonData withKey:jsonKey withSchemaData:schemaData error:error]; } --(BOOL)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema; +-(BOOL)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema error:(NSError **)error { - NSError * error = nil; + NSError * detailedError = nil; NSString * jsonKey = nil; if (![NSJSONSerialization isValidJSONObject:json]) { #ifdef DEBUG @@ -296,55 +330,72 @@ -(BOOL)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema; json = @{jsonKey : json}; // schema = @{@"properties" : @{@"debugInvalidTopTypeKey" : schema}}; #else + if (error) { + *error = [self validationErrorWithDescription:@"Supplied object is not a valid JSON object" forURL:nil detailedError:nil]; + } return NO; #endif } - NSData * jsonData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; - if (error != nil) { + NSData * jsonData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&detailedError]; + if (jsonData == nil) { + if (error) { + *error = [self validationErrorWithDescription:@"Supplied object could not be converted to json data" forURL:nil detailedError:detailedError]; + } return NO; } - NSData * schemaData = [NSJSONSerialization dataWithJSONObject:schema options:0 error:&error]; - if (error != nil) { + NSData * schemaData = [NSJSONSerialization dataWithJSONObject:schema options:0 error:&detailedError]; + if (schemaData == nil) { + if (error) { + *error = [self validationErrorWithDescription:@"Supplied schema dictionary is not valid json data" forURL:nil detailedError:detailedError]; + } return NO; } - return [self validateJSONData:jsonData withKey:jsonKey withSchemaData:schemaData]; + return [self validateJSONData:jsonData withKey:jsonKey withSchemaData:schemaData error:error]; } --(BOOL)validateJSONData:(NSData*)jsonData withSchemaData:(NSData*)schemaData +-(BOOL)validateJSONData:(NSData*)jsonData withSchemaData:(NSData*)schemaData error:(NSError **)error { - return [self validateJSONData:jsonData withKey:nil withSchemaData:schemaData]; + return [self validateJSONData:jsonData withKey:nil withSchemaData:schemaData error:error]; } --(BOOL)validateJSONData:(NSData*)jsonData withKey:(NSString*)key withSchemaData:(NSData*)schemaData +-(BOOL)validateJSONData:(NSData*)jsonData withKey:(NSString*)key withSchemaData:(NSData*)schemaData error:(NSError **)error { - NSError * error = nil; - id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; - if (error != nil) { + NSError * detailedError = nil; + id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&detailedError]; + if (json == nil) { + if (error) { + *error = [self validationErrorWithDescription:@"Supplied jsonData could not be decoded as valid JSON object" forURL:nil detailedError:detailedError]; + } return NO; } if (key != nil) { json = json[key]; } - id schema = [NSJSONSerialization JSONObjectWithData:schemaData options:0 error:&error]; - if (error != nil) { - return NO; - } - if (![schema isKindOfClass:[NSDictionary class]]) { + id schema = [NSJSONSerialization JSONObjectWithData:schemaData options:0 error:&detailedError]; + if (schema == nil) { + if (error) { + *error = [self validationErrorWithDescription:@"Supplied schemaData could not be decoded as valid JSON object" forURL:nil detailedError:detailedError]; + } return NO; } - if (![self validateJSON:json withSchemaDict:schema]) { + if (![self validateJSON:json withSchemaDict:schema error:&detailedError]) { + if (error) { + *error = [self validationErrorWithDescription:@"Supplied schema is not a valid schema" forURL:nil detailedError:detailedError]; + } return NO; } return YES; } --(BOOL)validateJSON:(id)json withSchemaDict:(NSDictionary *)schema +-(BOOL)validateJSON:(id)json withSchemaDict:(NSDictionary *)schema error:(NSError **)error { @synchronized(self) { if (!schema || ![schema isKindOfClass:[NSDictionary class]]) { - //NSLog(@"No schema specified, or incorrect data type: %@", schema); + if (error) { + *error = [self validationErrorWithDescription:@"Supplied schema does not have a root level dictionary" forURL:nil detailedError:nil]; + } return NO; } @@ -356,13 +407,21 @@ -(BOOL)validateJSON:(id)json withSchemaDict:(NSDictionary *)schema self.validationStack = [NSMutableArray new]; self.resolutionStack = [NSMutableArray new]; self.schemaStack = [NSMutableArray new]; + + NSError *detailedError = nil; [self setResolutionString:@"#" forSchema:schema]; - if (![self _validateJSON:schema withSchemaDict:self.rootSchema]) { + if (![self _validateJSON:schema withSchemaDict:self.rootSchema error:&detailedError]) { + if (error) { + *error = [self validationErrorWithDescription:@"Supplied schema is not valid according to the root level schema specification" forURL:nil detailedError:detailedError]; + } return NO; //error: invalid schema } - if (![self _validateJSON:json withSchemaDict:schema]) { + if (![self _validateJSON:json withSchemaDict:schema error:&detailedError]) { + if (error) { + *error = [self validationErrorWithDescription:@"Supplied json is not valid according to the supplied schema specification" forURL:nil detailedError:detailedError]; + } return NO; } @@ -371,12 +430,15 @@ -(BOOL)validateJSON:(id)json withSchemaDict:(NSDictionary *)schema } } --(BOOL)_validateJSON:(id)json withSchemaDict:(NSDictionary *)schema +-(BOOL)_validateJSON:(id)json withSchemaDict:(NSDictionary *)schema error:(NSError **)error { NSParameterAssert(schema != nil); //check stack for JSON and schema //push to stack the json and the schema. if (![self pushToStackJSON:json forSchema:schema]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Could not push json object to stack: %@", json] forURL:nil detailedError:nil]; + } return NO; } BOOL newResolution = NO; @@ -384,7 +446,7 @@ -(BOOL)_validateJSON:(id)json withSchemaDict:(NSDictionary *)schema if (resolutionValue) { newResolution = [self setResolutionString:resolutionValue forSchema:schema]; } - BOOL result = [self __validateJSON:json withSchemaDict:schema]; + BOOL result = [self __validateJSON:json withSchemaDict:schema error:error]; //pop from the stacks if (newResolution) { [self removeResolution]; @@ -393,7 +455,7 @@ -(BOOL)_validateJSON:(id)json withSchemaDict:(NSDictionary *)schema return result; } --(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema +-(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema error:(NSError **)error { //TODO: synonyms (potentially in higher level too) @@ -426,22 +488,25 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema if (schema[@"$ref"]) { if (![schema[@"$ref"] isKindOfClass:[NSString class]]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"$ref entry is not a valid string: %@", schema[@"$ref"]] forURL:nil detailedError:nil]; + } return NO; } - return [self validateJSON:json withSchemaAtReference:schema[@"$ref"]]; + return [self validateJSON:json withSchemaAtReference:schema[@"$ref"] error:error]; } NSString *type = nil; SEL typeValidator = nil; if ([json isKindOfClass:[NSArray class]]) { type = @"array"; - typeValidator = @selector(_validateJSONArray:withSchemaDict:); + typeValidator = @selector(_validateJSONArray:withSchemaDict:error:); } else if ([json isKindOfClass:[NSNumber class]]) { NSParameterAssert(strcmp( [@YES objCType], @encode(char) ) == 0); if (strcmp( [json objCType], @encode(char) ) == 0) { type = @"boolean"; } else { - typeValidator = @selector(_validateJSONNumeric:withSchemaDict:); + typeValidator = @selector(_validateJSONNumeric:withSchemaDict:error:); double num = [json doubleValue]; if ((num - floor(num)) == 0.0) { type = @"integer"; @@ -453,12 +518,15 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema type = @"null"; } else if ([json isKindOfClass:[NSDictionary class]]) { type = @"object"; - typeValidator = @selector(_validateJSONObject:withSchemaDict:); + typeValidator = @selector(_validateJSONObject:withSchemaDict:error:); } else if ([json isKindOfClass:[NSString class]]) { type = @"string"; - typeValidator = @selector(_validateJSONString:withSchemaDict:); + typeValidator = @selector(_validateJSONString:withSchemaDict:error:); } else { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object is not one of the valid types according to the JSON schema spec: %@: %@", NSStringFromClass(json), json] forURL:nil detailedError:nil]; + } return NO; // the schema is not one of the valid types. } @@ -470,6 +538,9 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema if ([keyword isEqualToString:@"enum"]) { //An instance validates successfully against this keyword if its value is equal to one of the elements in this keyword's array value. if (![schemaItem containsObject:json]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object is not valid according to the allowed items for this enumeration (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:nil]; + } return NO; } } else if ([keyword isEqualToString:@"type"]) { @@ -478,37 +549,66 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema continue; } if (![schemaItem isEqualToString:type]) { - return NO; + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object type is not valid according to the specified type by the schema (type, schemaItem): (%@, %@)", type, schemaItem] forURL:nil detailedError:nil]; + } + return NO; } } else { //array if (![schemaItem containsObject:type]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object type for array is not valid according to the specified type by the schema (type, schemaItem): (%@, %@)", type, schemaItem] forURL:nil detailedError:nil]; + } return NO; } } } else if ([keyword isEqualToString:@"allOf"]) { for (NSDictionary * subSchema in schemaItem) { - if (![self _validateJSON:json withSchemaDict:subSchema]) { return NO; } + if (![self _validateJSON:json withSchemaDict:subSchema error:error]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was not valid to allOf the specified schemas (json, schemaItem): (%@,%@)", json, schemaItem] forURL:nil detailedError:nil]; + } + return NO; + } } } else if ([keyword isEqualToString:@"anyOf"]) { BOOL anySuccess = NO; for (NSDictionary * subSchema in schemaItem) { - if ([self _validateJSON:json withSchemaDict:subSchema]) { + if ([self _validateJSON:json withSchemaDict:subSchema error:error]) { anySuccess = YES; break; } } if (!anySuccess) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was not valid to anyOf the specified schemas (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:nil]; + } return NO; } } else if ([keyword isEqualToString:@"oneOf"]) { int passes = 0; for (NSDictionary * subSchema in schemaItem) { - if ([self _validateJSON:json withSchemaDict:subSchema]) { passes++; } - if (passes > 1) { return NO; } + if ([self _validateJSON:json withSchemaDict:subSchema error:error]) { passes++; } + if (passes > 1) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was valid to more than exactly oneOf the specified schemas (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:nil]; + } + return NO; + } + } + if (passes != 1) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was valid to less than exactly oneOf the specified schemas (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:nil]; + } + return NO; } - if (passes != 1) { return NO; } } else if ([keyword isEqualToString:@"not"]) { - if ([self _validateJSON:json withSchemaDict:schemaItem]) { return NO; } + if ([self _validateJSON:json withSchemaDict:schemaItem error:error]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was valid to the specified schema while it should not be as specified by 'not' (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:nil]; + } + return NO; + } } else if ([keyword isEqualToString:@"definitions"]) { } @@ -517,8 +617,12 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema if (typeValidator != nil) { IMP imp = [self methodForSelector:typeValidator]; - BOOL (*func)(id, SEL, id, id) = (BOOL(*)(id, SEL, id, id))imp; - if (!func(self, typeValidator, json, schema)) { + NSError *detailedError = nil; + BOOL (*func)(id, SEL, id, id, id*) = (BOOL(*)(id, SEL, id, id, id*))imp; + if (!func(self, typeValidator, json, schema, &detailedError)) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was not valid according to the type specification (json, schema): (%@, %@)", json, schema] forURL:nil detailedError:detailedError]; + } return NO; } } @@ -527,7 +631,7 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema } //for number and integer --(BOOL)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictionary*)schema +-(BOOL)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictionary*)schema error:(NSError **)error { static NSArray * numericKeywords; static dispatch_once_t onceToken; @@ -536,6 +640,9 @@ -(BOOL)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictionary*) }); if (!schema || ![schema isKindOfClass:[NSDictionary class]]) { + if (error) { + *error = [self validationErrorWithDescription:@"Specified schema was not a valid dictionary" forURL:nil detailedError:nil]; + } return NO; } @@ -547,17 +654,26 @@ -(BOOL)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictionary*) //A numeric instance is valid against "multipleOf" if the result of the division of the instance by this keyword's value is an integer. double divResult = [jsonNumber doubleValue] / [schemaItem doubleValue]; if ((divResult - floor(divResult)) != 0.0) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Number '%@' is not a multiple of '%@'", jsonNumber, schemaItem] forURL:nil detailedError:nil]; + } return NO; } } else if ([keyword isEqualToString:@"maximum"]) { if ([schema[@"exclusiveMaximum"] isKindOfClass:[NSNumber class]] && [schema[@"exclusiveMaximum"] boolValue] == YES) { if (!([jsonNumber doubleValue] < [schemaItem doubleValue])) { //if "exclusiveMaximum" has boolean value true, the instance is valid if it is strictly lower than the value of "maximum". + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Number '%@' is not < '%@'", jsonNumber, schemaItem] forURL:nil detailedError:nil]; + } return NO; } } else { if (!([jsonNumber doubleValue] <= [schemaItem doubleValue])) { //if "exclusiveMaximum" is not present, or has boolean value false, then the instance is valid if it is lower than, or equal to, the value of "maximum" + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Number '%@' is not <= '%@'", jsonNumber, schemaItem] forURL:nil detailedError:nil]; + } return NO; } } @@ -565,11 +681,17 @@ -(BOOL)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictionary*) if ([schema[@"exclusiveMinimum"] isKindOfClass:[NSNumber class]] && [schema[@"exclusiveMinimum"] boolValue] == YES) { if (!([jsonNumber doubleValue] > [schemaItem doubleValue])) { //if "exclusiveMinimum" is present and has boolean value true, the instance is valid if it is strictly greater than the value of "minimum". + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Number '%@' is not > '%@'", jsonNumber, schemaItem] forURL:nil detailedError:nil]; + } return NO; } } else { if (!([jsonNumber doubleValue] >= [schemaItem doubleValue])) { //if "exclusiveMinimum" is not present, or has boolean value false, then the instance is valid if it is greater than, or equal to, the value of "minimum" + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Number '%@' is not >= '%@'", jsonNumber, schemaItem] forURL:nil detailedError:nil]; + } return NO; } } @@ -579,7 +701,7 @@ -(BOOL)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictionary*) return YES; } --(BOOL)_validateJSONString:(NSString*)jsonString withSchemaDict:(NSDictionary*)schema +-(BOOL)_validateJSONString:(NSString*)jsonString withSchemaDict:(NSDictionary*)schema error:(NSError **)error { static NSArray * stringKeywords; static dispatch_once_t onceToken; @@ -599,23 +721,36 @@ -(BOOL)_validateJSONString:(NSString*)jsonString withSchemaDict:(NSDictionary*)s // Go read this if you care: http://www.objc.io/issue-9/unicode.html (See Common Pitfalls - Length) NSInteger realLength = [jsonString lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; - if (!(realLength <= [schemaItem integerValue])) { return NO; } + if (!(realLength <= [schemaItem integerValue])) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Length of string '%@' is not <= '%@'", jsonString, schemaItem] forURL:nil detailedError:nil]; + } + return NO; + } } else if ([keyword isEqualToString:@"minLength"]) { //A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword. NSInteger realLength = [jsonString lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; - if (!(realLength >= [schemaItem intValue])) { return NO; } + if (!(realLength >= [schemaItem intValue])) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Length of string '%@' is not >= '%@'", jsonString, schemaItem] forURL:nil detailedError:nil]; + } + return NO; + } } else if ([keyword isEqualToString:@"pattern"]) { //A string instance is considered valid if the regular expression matches the instance successfully. Recall: regular expressions are not implicitly anchored. //This string SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. //NOTE: this regex uses ICU which has some differences to ECMA-262 (such as look-behind) - NSError * error; - NSRegularExpression * regex = [NSRegularExpression regularExpressionWithPattern:schemaItem options:0 error:&error]; - if (error) { + NSError * regexError = nil; + NSRegularExpression * regex = [NSRegularExpression regularExpressionWithPattern:schemaItem options:0 error:®exError]; + if (regex == nil) { continue; } if (NSEqualRanges([regex rangeOfFirstMatchInString:jsonString options:0 range:NSMakeRange(0, jsonString.length)], NSMakeRange(NSNotFound, 0))) { //A string instance is considered valid if the regular expression matches the instance successfully. Recall: regular expressions are not implicitly anchored. + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Pattern '%@' does not match string '%@'", schemaItem, jsonString] forURL:nil detailedError:nil]; + } return NO; } } @@ -624,7 +759,7 @@ -(BOOL)_validateJSONString:(NSString*)jsonString withSchemaDict:(NSDictionary*)s return YES; } --(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary*)schema +-(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary*)schema error:(NSError **)error { static NSArray * objectKeywords; static dispatch_once_t onceToken; @@ -632,21 +767,35 @@ -(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary* objectKeywords = @[@"maxProperties", @"minProperties", @"required", @"properties", @"patternProperties", @"additionalProperties", @"dependencies"]; }); BOOL doneProperties = NO; + NSError *detailedError = nil; for (NSString * keyword in objectKeywords) { id schemaItem = schema[keyword]; if (schemaItem != nil) { if ([keyword isEqualToString:@"maxProperties"]) { //An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. - if ((NSInteger)[jsonDict count] > [schemaItem integerValue]) { return NO; /*invalid JSON dict*/ } + if ((NSInteger)[jsonDict count] > [schemaItem integerValue]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Item count in json dict '%@' exceeds schema limit: %@", jsonDict, schemaItem] forURL:nil detailedError:nil]; + } + return NO; /*invalid JSON dict*/ + } } else if ([keyword isEqualToString:@"minProperties"]) { //An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword. - if ((NSInteger)[jsonDict count] < [schemaItem integerValue]) { return NO; /*invalid JSON dict*/ } + if ((NSInteger)[jsonDict count] < [schemaItem integerValue]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Property count in json dict '%@' is less than min schema limit: %@", jsonDict, schemaItem] forURL:nil detailedError:nil]; + } + return NO; /*invalid JSON dict*/ + } } else if ([keyword isEqualToString:@"required"]) { NSArray * requiredArray = schemaItem; for (NSObject * requiredProp in requiredArray) { NSString * requiredPropStr = (NSString*)requiredProp; if (![jsonDict valueForKey:requiredPropStr]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Json dict '%@' does not contain required property: %@", jsonDict, requiredPropStr] forURL:nil detailedError:nil]; + } return NO; //required not present. invalid JSON dict. } } @@ -680,9 +829,9 @@ -(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary* for (NSString * regexString in pp) { //Each property name of this object SHOULD be a valid regular expression, according to the ECMA 262 regular expression dialect. //NOTE: this regex uses ICU which has some differences to ECMA-262 (such as look-behind) - NSError * error; - NSRegularExpression * regex = [NSRegularExpression regularExpressionWithPattern:regexString options:0 error:&error]; - if (error) { + NSError * regexError; + NSRegularExpression * regex = [NSRegularExpression regularExpressionWithPattern:regexString options:0 error:®exError]; + if (regex == nil) { continue; } for (NSString * m in allKeys) { @@ -704,6 +853,9 @@ -(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary* //Because we have built a set of schemas/keys up (rather than down), the following test is equivalent to the requirement: //Validation of the instance succeeds if, after these two steps, set "s" is empty. if (testSchemas.count < allKeys.count) { + if (error) { + *error = [self validationErrorWithDescription:@"There are invalid properties left" forURL:nil detailedError:nil]; + } return NO; } } else { @@ -727,7 +879,10 @@ -(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary* for (NSString * property in [testSchemas keyEnumerator]) { NSArray * subschemas = testSchemas[property]; for (NSDictionary * subschema in subschemas) { - if (![self _validateJSON:jsonDict[property] withSchemaDict:subschema]) { + if (![self _validateJSON:jsonDict[property] withSchemaDict:subschema error:&detailedError]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Property '%@' is not valid according to schema: %@", property, subschema] forURL:nil detailedError:detailedError]; + } return NO; } } @@ -745,7 +900,10 @@ -(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary* NSDictionary * schemaDependency = dependency; //For all (name, schema) pair of schema dependencies, if the instance has a property by this name, then it must also validate successfully against the schema. //Note that this is the instance itself which must validate successfully, not the value associated with the property name. - if (![self _validateJSON:jsonDict withSchemaDict:schemaDependency]) { + if (![self _validateJSON:jsonDict withSchemaDict:schemaDependency error:&detailedError]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON dict '%@' is not valid according to schema dependency: %@", jsonDict, schemaDependency] forURL:nil detailedError:detailedError]; + } return NO; } } else if ([dependency isKindOfClass:[NSArray class]]) { @@ -753,6 +911,9 @@ -(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary* //For each (name, propertyset) pair of property dependencies, if the instance has a property by this name, then it must also have properties with the same names as propertyset. NSSet * propertySet = [NSSet setWithArray:propertyDependency]; if (![propertySet isSubsetOfSet:properties]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Property set %@ is not a subset of set: %@", propertySet, properties] forURL:nil detailedError:detailedError]; + } return NO; } } @@ -763,7 +924,7 @@ -(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary* return YES; } --(BOOL)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*)schema +-(BOOL)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*)schema error:(NSError **)error { static NSArray * arrayKeywords; static dispatch_once_t onceToken; @@ -772,6 +933,7 @@ -(BOOL)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*)sche }); BOOL doneItems = NO; + NSError *detailedError = nil; for (NSString * keyword in arrayKeywords) { id schemaItem = schema[keyword]; if (schemaItem != nil) { @@ -790,20 +952,32 @@ -(BOOL)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*)sche id child = jsonArray[index]; if ([items isKindOfClass:[NSDictionary class]]) { //If items is a schema, then the child instance must be valid against this schema, regardless of its index, and regardless of the value of "additionalItems". - if (![self _validateJSON:jsonArray[index] withSchemaDict:items]) { + if (![self _validateJSON:jsonArray[index] withSchemaDict:items error:&detailedError]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Item '%@' is not valid according to schema dict: %@", jsonArray[index], items] forURL:nil detailedError:detailedError]; + } return NO; } } else if ([items isKindOfClass:[NSArray class]]) { if (index < [(NSArray *)items count]) { - if (![self _validateJSON:child withSchemaDict:items[index]]) { + if (![self _validateJSON:child withSchemaDict:items[index] error:&detailedError]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Item '%@' is not valid according to schema dict: %@", child, items[index]] forURL:nil detailedError:detailedError]; + } return NO; } } else { if ([additionalItems isKindOfClass:[NSNumber class]] && [additionalItems boolValue] == NO) { //if the value of "additionalItems" is boolean value false and the value of "items" is an array, the instance is valid if its size is less than, or equal to, the size of "items". + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"additionalItems is not in a valid format: %@", additionalItems] forURL:nil detailedError:detailedError]; + } return NO; } else { - if (![self _validateJSON:child withSchemaDict:additionalItems]) { + if (![self _validateJSON:child withSchemaDict:additionalItems error:&detailedError]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Item '%@' is not valid according to additional items: %@", child, additionalItems] forURL:nil detailedError:detailedError]; + } return NO; } } @@ -812,10 +986,20 @@ -(BOOL)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*)sche } } else if ([keyword isEqualToString:@"maxItems"]) { //An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword. - if ((NSInteger)[jsonArray count] > [schemaItem integerValue]) { return NO; } + if ((NSInteger)[jsonArray count] > [schemaItem integerValue]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Max item count of '%@' is exceeded by items: %@", schemaItem, jsonArray] forURL:nil detailedError:detailedError]; + } + return NO; + } //An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. } else if ([keyword isEqualToString:@"minItems"]) { - if ((NSInteger)[jsonArray count] < [schemaItem integerValue]) { return NO; } + if ((NSInteger)[jsonArray count] < [schemaItem integerValue]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Min item count of '%@' is not met by items: %@", schemaItem, jsonArray] forURL:nil detailedError:detailedError]; + } + return NO; + } } else if ([keyword isEqualToString:@"uniqueItems"]) { if ([schemaItem isKindOfClass:[NSNumber class]] && [schemaItem boolValue] == YES) { //If it has boolean value true, the instance validates successfully if all of its elements are unique. @@ -835,6 +1019,9 @@ -(BOOL)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*)sche if (([uniqueItems count] + fudgeFactor) < [jsonArray count]) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Not all items are unique as required by schema for items: %@", jsonArray] forURL:nil detailedError:detailedError]; + } return NO; } } @@ -910,4 +1097,20 @@ -(BOOL)checkSchemaRef:(NSDictionary*)schema } } +- (NSError *)validationErrorWithDescription:(NSString *)description forURL:(nullable NSURL *)url detailedError:(nullable NSError *)detailedError { + NSMutableDictionary *userInfo = [NSMutableDictionary new]; + + userInfo[NSLocalizedDescriptionKey] = description; + + if (url != nil) { + userInfo[NSURLErrorKey] = url; + } + + if (detailedError != nil) { + userInfo[NSUnderlyingErrorKey] = detailedError; + } + + return [NSError errorWithDomain:@"KiteJSONValidator" code:1 userInfo:userInfo]; +} + @end diff --git a/Tests/KiteJSONValidatorTests.m b/Tests/KiteJSONValidatorTests.m index 2ce4ff5..7766003 100644 --- a/Tests/KiteJSONValidatorTests.m +++ b/Tests/KiteJSONValidatorTests.m @@ -56,11 +56,11 @@ - (void)testDraft4Suite NSData * data = [NSData dataWithContentsOfFile:fullpath]; NSURL * url = [NSURL URLWithString:@"http://localhost:1234/"]; url = [NSURL URLWithString:refPath relativeToURL:url]; - BOOL success = [validator addRefSchemaData:data atURL:url]; + BOOL success = [validator addRefSchemaData:data atURL:url error:nil]; XCTAssertTrue(success == YES, @"Unable to add the reference schema at '%@'", url); } - BOOL result = [validator validateJSONInstance:json[@"data"] withSchema:test[@"schema"]]; + BOOL result = [validator validateJSONInstance:json[@"data"] withSchema:test[@"schema"] error:nil]; BOOL desired = [json[@"valid"] boolValue]; if (result != desired) { XCTFail(@"Category: %@ Test: %@ Expected result: %i", test[@"description"], json[@"description"], desired); diff --git a/Tests/Tests-Info.plist b/Tests/Tests-Info.plist index 0137d07..169b6f7 100644 --- a/Tests/Tests-Info.plist +++ b/Tests/Tests-Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier - samskiter.${PRODUCT_NAME:rfc1034identifier} + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType From 524d5e8ca2df0d5c77c494005e41caa3aad10ebb Mon Sep 17 00:00:00 2001 From: Werner Altewischer Date: Wed, 7 Mar 2018 17:28:09 +0100 Subject: [PATCH 2/5] Fixed schema resolving and fixed travis config --- .travis.yml | 6 +----- Sources/KiteJSONValidator.h | 1 + Sources/KiteJSONValidator.m | 41 +++++++++++++++++++++++++++++++++---- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 055bb17..5a94f63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,5 @@ language: objective-c before_script: - export LANG=en_US.UTF-8 -before_install: - - brew update - - brew uninstall xctool - - brew install xctool script: - - xctool -project KiteJSONValidator.xcodeproj -scheme KiteJSONValidatorTests -sdk iphonesimulator test ONLY_ACTIVE_ARCH=NO + - set -o pipefail && xcodebuild clean test -project KiteJSONValidator.xcodeproj -scheme KiteJSONValidatorTests -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty \ No newline at end of file diff --git a/Sources/KiteJSONValidator.h b/Sources/KiteJSONValidator.h index 06d8c88..65877c0 100644 --- a/Sources/KiteJSONValidator.h +++ b/Sources/KiteJSONValidator.h @@ -75,6 +75,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol KiteJSONSchemaRefDelegate +@optional -(nullable NSData*)schemaValidator:(KiteJSONValidator*)validator requiresSchemaDataForRefURL:(NSURL*)refURL; -(nullable NSDictionary*)schemaValidator:(KiteJSONValidator*)validator requiresSchemaForRefURL:(NSURL*)refURL; diff --git a/Sources/KiteJSONValidator.m b/Sources/KiteJSONValidator.m index 6a8859c..4a2abaa 100644 --- a/Sources/KiteJSONValidator.m +++ b/Sources/KiteJSONValidator.m @@ -222,9 +222,11 @@ -(BOOL)validateJSON:(id)json withSchemaAtReference:(NSString*)refString error:(N if ([lastResolution isEqual:refURI]) { schema = (NSDictionary*)self.schemaStack.lastObject; - } else if (self.schemaRefs != nil && self.schemaRefs[refURI] != nil) { - //we changed document - schema = self.schemaRefs[refURI]; + } else if (refURI != nil) { + schema = [self resolveSchemaRefURI:refURI withError:error]; + if (schema == nil) { + return NO; + } [self setResolutionUrl:refURI forSchema:schema]; newDocument = YES; } @@ -266,6 +268,37 @@ -(BOOL)validateJSON:(id)json withSchemaAtReference:(NSString*)refString error:(N return result; } +- (NSDictionary *)resolveSchemaRefURI:(NSURL *)refURI withError:(NSError **)error { + NSDictionary *schema = self.schemaRefs[refURI]; + + if (schema == nil) { + if ([self.delegate respondsToSelector:@selector(schemaValidator:requiresSchemaForRefURL:)]) { + schema = [self.delegate schemaValidator:self requiresSchemaForRefURL:refURI]; + } + + if (schema == nil && [self.delegate respondsToSelector:@selector(schemaValidator:requiresSchemaDataForRefURL:)]) { + NSData *data = [self.delegate schemaValidator:self requiresSchemaDataForRefURL:refURI]; + NSError *detailedError = nil; + schema = [NSJSONSerialization JSONObjectWithData:data options:0 error:&detailedError]; + if (schema == nil) { + if (error) { + *error = [self validationErrorWithDescription:@"Schema data could not be converted to json object" forURL:refURI detailedError:detailedError]; + } + return nil; + } else if (![schema isKindOfClass:[NSDictionary class]]) { + if (error) { + *error = [self validationErrorWithDescription:@"Schema data does not have a root level dictionary" forURL:refURI detailedError:nil]; + } + return nil; + } + } + } + if (schema == nil && error != nil) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Schema reference could not be resolved: %@", refURI] forURL:refURI detailedError:nil]; + } + return schema; +} + -(BOOL)setResolutionString:(NSString *)resolution forSchema:(NSDictionary *)schema { //res and schema as Pair only add if different to previous. pop smart. pre fill. leave ability to look up res anywhere. @@ -380,7 +413,7 @@ -(BOOL)validateJSONData:(NSData*)jsonData withKey:(NSString*)key withSchemaData: } if (![self validateJSON:json withSchemaDict:schema error:&detailedError]) { if (error) { - *error = [self validationErrorWithDescription:@"Supplied schema is not a valid schema" forURL:nil detailedError:detailedError]; + *error = [self validationErrorWithDescription:@"Supplied json is not valid according to schema" forURL:nil detailedError:detailedError]; } return NO; } From 3d49b208a7d5231c8f111e250b743d5538d2025d Mon Sep 17 00:00:00 2001 From: Werner Altewischer Date: Wed, 7 Mar 2018 17:48:35 +0100 Subject: [PATCH 3/5] Fixed check for nil NSData returned by delegate for schema ref --- Sources/KiteJSONValidator.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/KiteJSONValidator.m b/Sources/KiteJSONValidator.m index 4a2abaa..ecb99e8 100644 --- a/Sources/KiteJSONValidator.m +++ b/Sources/KiteJSONValidator.m @@ -278,6 +278,12 @@ - (NSDictionary *)resolveSchemaRefURI:(NSURL *)refURI withError:(NSError **)erro if (schema == nil && [self.delegate respondsToSelector:@selector(schemaValidator:requiresSchemaDataForRefURL:)]) { NSData *data = [self.delegate schemaValidator:self requiresSchemaDataForRefURL:refURI]; + if (data == nil) { + if (error) { + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"Delegate returned no schema data for reference: %@", refURI] forURL:refURI detailedError:nil]; + } + return nil; + } NSError *detailedError = nil; schema = [NSJSONSerialization JSONObjectWithData:data options:0 error:&detailedError]; if (schema == nil) { From 5a655aeb06d24a4f292293f9bac5a11457a322a8 Mon Sep 17 00:00:00 2001 From: Werner Altewischer Date: Wed, 7 Mar 2018 17:51:22 +0100 Subject: [PATCH 4/5] Added destination to travis config --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5a94f63..cd5473e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ +osx_image: xcode9.2 language: objective-c before_script: - export LANG=en_US.UTF-8 script: - - set -o pipefail && xcodebuild clean test -project KiteJSONValidator.xcodeproj -scheme KiteJSONValidatorTests -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO | xcpretty \ No newline at end of file + - set -o pipefail && xcodebuild clean test -project KiteJSONValidator.xcodeproj -scheme KiteJSONValidatorTests -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 6,OS=11.2' ONLY_ACTIVE_ARCH=NO | xcpretty From 03d159aedccfed38c872e9e5dd6eb76ff6996f5c Mon Sep 17 00:00:00 2001 From: Werner Altewischer Date: Thu, 8 Mar 2018 19:20:23 +0100 Subject: [PATCH 5/5] Fixed error propagation for 'allOf', 'anyOf', etc --- Sources/KiteJSONValidator.m | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Sources/KiteJSONValidator.m b/Sources/KiteJSONValidator.m index ecb99e8..63a3ba3 100644 --- a/Sources/KiteJSONValidator.m +++ b/Sources/KiteJSONValidator.m @@ -567,7 +567,9 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema error:(NSEr *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object is not one of the valid types according to the JSON schema spec: %@: %@", NSStringFromClass(json), json] forURL:nil detailedError:nil]; } return NO; // the schema is not one of the valid types. - } + } + + NSError *detailedError = nil; //TODO: extract the types first before the check (if there is no type specified, we'll never hit the checking code for (NSString * keyword in anyInstanceKeywords) { @@ -603,9 +605,9 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema error:(NSEr } } else if ([keyword isEqualToString:@"allOf"]) { for (NSDictionary * subSchema in schemaItem) { - if (![self _validateJSON:json withSchemaDict:subSchema error:error]) { + if (![self _validateJSON:json withSchemaDict:subSchema error:&detailedError]) { if (error) { - *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was not valid to allOf the specified schemas (json, schemaItem): (%@,%@)", json, schemaItem] forURL:nil detailedError:nil]; + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was not valid to allOf the specified schemas (json, schemaItem): (%@,%@)", json, schemaItem] forURL:nil detailedError:detailedError]; } return NO; } @@ -613,38 +615,38 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema error:(NSEr } else if ([keyword isEqualToString:@"anyOf"]) { BOOL anySuccess = NO; for (NSDictionary * subSchema in schemaItem) { - if ([self _validateJSON:json withSchemaDict:subSchema error:error]) { + if ([self _validateJSON:json withSchemaDict:subSchema error:&detailedError]) { anySuccess = YES; break; } } if (!anySuccess) { if (error) { - *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was not valid to anyOf the specified schemas (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:nil]; + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was not valid to anyOf the specified schemas (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:detailedError]; } return NO; } } else if ([keyword isEqualToString:@"oneOf"]) { int passes = 0; for (NSDictionary * subSchema in schemaItem) { - if ([self _validateJSON:json withSchemaDict:subSchema error:error]) { passes++; } + if ([self _validateJSON:json withSchemaDict:subSchema error:&detailedError]) { passes++; } if (passes > 1) { if (error) { - *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was valid to more than exactly oneOf the specified schemas (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:nil]; + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was valid to more than exactly oneOf the specified schemas (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:detailedError]; } return NO; } } if (passes != 1) { if (error) { - *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was valid to less than exactly oneOf the specified schemas (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:nil]; + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was valid to less than exactly oneOf the specified schemas (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:detailedError]; } return NO; } } else if ([keyword isEqualToString:@"not"]) { - if ([self _validateJSON:json withSchemaDict:schemaItem error:error]) { + if ([self _validateJSON:json withSchemaDict:schemaItem error:&detailedError]) { if (error) { - *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was valid to the specified schema while it should not be as specified by 'not' (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:nil]; + *error = [self validationErrorWithDescription:[NSString stringWithFormat:@"JSON object was valid to the specified schema while it should not be as specified by 'not' (json, schemaItem): (%@, %@)", json, schemaItem] forURL:nil detailedError:detailedError]; } return NO; } @@ -656,7 +658,6 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema error:(NSEr if (typeValidator != nil) { IMP imp = [self methodForSelector:typeValidator]; - NSError *detailedError = nil; BOOL (*func)(id, SEL, id, id, id*) = (BOOL(*)(id, SEL, id, id, id*))imp; if (!func(self, typeValidator, json, schema, &detailedError)) { if (error) {