From 9671c1bb0f2f70f9940b0d6ad1fd5057b54f829c Mon Sep 17 00:00:00 2001 From: Max Lunin Date: Sun, 1 Mar 2015 20:58:51 +0200 Subject: [PATCH 1/4] NSError as a result of validation --- Sources/KiteJSONValidator.h | 5 +- Sources/KiteJSONValidator.m | 270 +++++++++++++++++++-------------- Tests/KiteJSONValidatorTests.m | 4 +- 3 files changed, 159 insertions(+), 120 deletions(-) diff --git a/Sources/KiteJSONValidator.h b/Sources/KiteJSONValidator.h index 30bd0be..c6194b0 100644 --- a/Sources/KiteJSONValidator.h +++ b/Sources/KiteJSONValidator.h @@ -22,8 +22,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; + +-(NSError*)validateJSONData:(NSData*)jsonData withSchemaData:(NSData*)schemaData; +-(NSError*)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema; /** Used for adding an ENTIRE document to the list of reference schemas - the URL should therefore be fragmentless. diff --git a/Sources/KiteJSONValidator.m b/Sources/KiteJSONValidator.m index 8cc85f9..2ded43f 100644 --- a/Sources/KiteJSONValidator.m +++ b/Sources/KiteJSONValidator.m @@ -18,6 +18,18 @@ @interface KiteJSONValidator() @end +NSError* jsonError(NSString* format,...){ + + static NSString* KiteJSONValidatorDomain = @"KiteJSONValidator"; + + va_list args; + va_start(args, format); + NSString* description = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + + return [NSError errorWithDomain:KiteJSONValidatorDomain code:0 userInfo:@{NSLocalizedDescriptionKey:description}]; +} + @implementation KiteJSONValidator @synthesize validationStack=_validationStack; @@ -94,9 +106,10 @@ -(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BO NSDictionary *root = [self rootSchema]; if (![root isEqualToDictionary:schema]) { - BOOL isValidSchema = [self validateJSON:schema withSchemaDict:root]; - NSAssert(isValidSchema == YES, @"Invalid schema", nil); - if (!isValidSchema) return NO; + NSError* schemaError = [self validateJSON:schema withSchemaDict:root]; + NSAssert(!schemaError, @"Invalid schema", nil); + if (schemaError) + return NO; } else { @@ -135,7 +148,7 @@ -(NSDictionary *)rootSchema return rootSchema; } --(BOOL)pushToStackJSON:(id)json forSchema:(NSDictionary*)schema +-(NSError*)pushToStackJSON:(id)json forSchema:(NSDictionary*)schema { if (self.validationStack == nil) { self.validationStack = [NSMutableArray new]; @@ -144,10 +157,10 @@ -(BOOL)pushToStackJSON:(id)json forSchema:(NSDictionary*)schema } KiteValidationPair * pair = [KiteValidationPair pairWithLeft:json right:schema]; if ([self.validationStack containsObject:pair]) { - return NO; //Detects loops + return jsonError(@"loops detectsed"); //Detects loops } [self.validationStack addObject:pair]; - return YES; + return nil; } -(void)popStack @@ -174,12 +187,11 @@ -(NSURL*)urlWithoutFragment:(NSURL*)url return [NSURL URLWithString:refString]; } --(BOOL)validateJSON:(id)json withSchemaAtReference:(NSString*)refString +-(NSError*)validateJSON:(id)json withSchemaAtReference:(NSString*)refString { NSURL * refURI = [NSURL URLWithString:refString relativeToURL:self.resolutionStack.lastObject]; - if (!refURI) - { - return NO; + if (!refURI) { + return jsonError(@"No Ref URI"); } //get the fragment, if it is a JSON-Pointer @@ -205,7 +217,7 @@ -(BOOL)validateJSON:(id)json withSchemaAtReference:(NSString*)refString } if (!schema) { - return NO; + return jsonError(@"No schema for Ref URI"); } for (NSString * component in pointerComponents) { @@ -225,14 +237,14 @@ -(BOOL)validateJSON:(id)json withSchemaAtReference:(NSString*)refString } if (!schema) { - return NO; + return jsonError(@"No schema"); } } - BOOL result = [self _validateJSON:json withSchemaDict:schema]; + NSError* error = [self _validateJSON:json withSchemaDict:schema]; if (newDocument) { [self removeResolution]; } - return result; + return error; } -(BOOL)setResolutionString:(NSString *)resolution forSchema:(NSDictionary *)schema @@ -261,7 +273,7 @@ -(void)removeResolution [self.schemaStack removeLastObject]; } --(BOOL)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema; +-(NSError*)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema { NSError * error = nil; NSString * jsonKey = nil; @@ -272,56 +284,53 @@ -(BOOL)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema; json = @{jsonKey : json}; // schema = @{@"properties" : @{@"debugInvalidTopTypeKey" : schema}}; #else - return NO; + return jsonError(@"is not valid JSON Object"); #endif } NSData * jsonData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; - if (error != nil) { - return NO; + if (error ) { + return error; } NSData * schemaData = [NSJSONSerialization dataWithJSONObject:schema options:0 error:&error]; - if (error != nil) { - return NO; + if (error) { + return error; } return [self validateJSONData:jsonData withKey:jsonKey withSchemaData:schemaData]; } --(BOOL)validateJSONData:(NSData*)jsonData withSchemaData:(NSData*)schemaData +-(NSError*)validateJSONData:(NSData*)jsonData withSchemaData:(NSData*)schemaData { return [self validateJSONData:jsonData withKey:nil withSchemaData:schemaData]; } --(BOOL)validateJSONData:(NSData*)jsonData withKey:(NSString*)key withSchemaData:(NSData*)schemaData +-(NSError*)validateJSONData:(NSData*)jsonData withKey:(NSString*)key withSchemaData:(NSData*)schemaData { NSError * error = nil; id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; - if (error != nil) { - return NO; + if (error) { + return error; } if (key != nil) { json = json[key]; } id schema = [NSJSONSerialization JSONObjectWithData:schemaData options:0 error:&error]; - if (error != nil) { - return NO; + if (error) { + return error; } if (![schema isKindOfClass:[NSDictionary class]]) { - return NO; - } - if (![self validateJSON:json withSchemaDict:schema]) { - return NO; + return jsonError(@"Schema must be 'Object' type"); } - return YES; + return [self validateJSON:json withSchemaDict:schema]; } --(BOOL)validateJSON:(id)json withSchemaDict:(NSDictionary *)schema +-(NSError*)validateJSON:(id)json withSchemaDict:(NSDictionary *)schema { + if (!schema || ![schema isKindOfClass:[NSDictionary class]]) { //NSLog(@"No schema specified, or incorrect data type: %@", schema); - return NO; + return jsonError(@"Schema must be 'Object' type"); } - //need to make sure the validation of schema doesn't infinitely recurse (self references) // therefore should not expand any subschemas, and ensure schema are only checked on a 'top' level. //first validate the schema against the root schema then validate against the original @@ -333,40 +342,44 @@ -(BOOL)validateJSON:(id)json withSchemaDict:(NSDictionary *)schema [self setResolutionString:@"#" forSchema:schema]; - if (![self _validateJSON:schema withSchemaDict:self.rootSchema]) { - return NO; //error: invalid schema + NSError* error = [self _validateJSON:schema withSchemaDict:self.rootSchema]; + if (error) { + return error; //error: invalid schema } - if (![self _validateJSON:json withSchemaDict:schema]) { - return NO; + + error = [self _validateJSON:json withSchemaDict:schema]; + if (error) { + return error; } - + [self removeResolution]; - return YES; + return nil; } --(BOOL)_validateJSON:(id)json withSchemaDict:(NSDictionary *)schema +-(NSError*)_validateJSON:(id)json withSchemaDict:(NSDictionary *)schema { NSParameterAssert(schema != nil); //check stack for JSON and schema //push to stack the json and the schema. - if (![self pushToStackJSON:json forSchema:schema]) { - return NO; + NSError* error = [self pushToStackJSON:json forSchema:schema]; + if (error) { + return error; } BOOL newResolution = NO; NSString *resolutionValue = schema[@"id"]; if (resolutionValue) { newResolution = [self setResolutionString:resolutionValue forSchema:schema]; } - BOOL result = [self __validateJSON:json withSchemaDict:schema]; + error = [self __validateJSON:json withSchemaDict:schema]; //pop from the stacks if (newResolution) { [self removeResolution]; } [self popStack]; - return result; + return error; } --(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema +-(NSError*)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema { //TODO: synonyms (potentially in higher level too) @@ -397,24 +410,25 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema // } // } - if (schema[@"$ref"]) { - if (![schema[@"$ref"] isKindOfClass:[NSString class]]) { - return NO; + NSString* schemaRef = schema[@"$ref"]; + if (schemaRef) { + if (![schemaRef isKindOfClass:[NSString class]]) { + return jsonError(@"$ref '%@' is '%@' type, but must be a 'String' type", schemaRef, [schemaRef class]); } - return [self validateJSON:json withSchemaAtReference:schema[@"$ref"]]; + return [self validateJSON:json withSchemaAtReference:schemaRef]; } NSString *type = nil; - SEL typeValidator = nil; + NSError*(^typeValidator)() = nil; if ([json isKindOfClass:[NSArray class]]) { type = @"array"; - typeValidator = @selector(_validateJSONArray:withSchemaDict:); + typeValidator = ^{ return [self _validateJSONArray:json withSchemaDict:schema]; }; } 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 = ^{ return [self _validateJSONNumeric:json withSchemaDict:schema]; }; double num = [json doubleValue]; if ((num - floor(num)) == 0.0) { type = @"integer"; @@ -426,13 +440,13 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema type = @"null"; } else if ([json isKindOfClass:[NSDictionary class]]) { type = @"object"; - typeValidator = @selector(_validateJSONObject:withSchemaDict:); + typeValidator = ^{ return [self _validateJSONObject:json withSchemaDict:schema]; }; } else if ([json isKindOfClass:[NSString class]]) { type = @"string"; - typeValidator = @selector(_validateJSONString:withSchemaDict:); + typeValidator = ^{ return [self _validateJSONString:json withSchemaDict:schema]; }; } else { - return NO; // the schema is not one of the valid types. + return jsonError(@"The schema '%@' is not one of the valid types", [json class]); // the schema is not one of the valid types. } //TODO: extract the types first before the check (if there is no type specified, we'll never hit the checking code @@ -443,7 +457,7 @@ -(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]) { - return NO; + return jsonError(@"enum '%@' does not contain '%@'", schemaItem, json); } } else if ([keyword isEqualToString:@"type"]) { if ([schemaItem isKindOfClass:[NSString class]]) { @@ -451,37 +465,47 @@ -(BOOL)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema continue; } if (![schemaItem isEqualToString:type]) { - return NO; + return jsonError(@"schema '%@' != '%@'", schemaItem, type); } } else { //array if (![schemaItem containsObject:type]) { - return NO; + return jsonError(@"array '%@' does not contain '%@'", schemaItem, type); } } } else if ([keyword isEqualToString:@"allOf"]) { for (NSDictionary * subSchema in schemaItem) { - if (![self _validateJSON:json withSchemaDict:subSchema]) { return NO; } + NSError* error = [self _validateJSON:json withSchemaDict:subSchema]; + if (error) { + return error; + } } } else if ([keyword isEqualToString:@"anyOf"]) { - BOOL anySuccess = NO; for (NSDictionary * subSchema in schemaItem) { - if ([self _validateJSON:json withSchemaDict:subSchema]) { - anySuccess = YES; - break; + NSError* error = [self _validateJSON:json withSchemaDict:subSchema]; + if (error == nil) { + return nil; } } - if (!anySuccess) { - return NO; - } + return jsonError(@"'%@' must be ANY OF '%@'", json, schemaItem); } 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; } + NSError* error = [self _validateJSON:json withSchemaDict:subSchema]; + if (!error) { + passes++; + } + if (passes > 1) { + return jsonError(@"'%@' must be ONE OF '%@'", json, schemaItem); + } + } + if (passes != 1) { + return jsonError(@"'%@' must be ONE OF '%@'", json, schemaItem); } - if (passes != 1) { return NO; } } else if ([keyword isEqualToString:@"not"]) { - if ([self _validateJSON:json withSchemaDict:schemaItem]) { return NO; } + NSError* error = [self _validateJSON:json withSchemaDict:schemaItem]; + if (error == nil) { + return jsonError(@"'%@' must be NOT '%@'", json, schemaItem); + } } else if ([keyword isEqualToString:@"definitions"]) { } @@ -489,18 +513,14 @@ -(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)) { - return NO; - } + return typeValidator(); } - return YES; + return nil; } //for number and integer --(BOOL)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictionary*)schema +-(NSError*)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictionary*)schema { static NSArray * numericKeywords; static dispatch_once_t onceToken; @@ -508,8 +528,11 @@ -(BOOL)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictionary*) numericKeywords = @[@"multipleOf", @"maximum",/* @"exclusiveMaximum",*/ @"minimum",/* @"exclusiveMinimum"*/]; }); - if (!schema || ![schema isKindOfClass:[NSDictionary class]]) { - return NO; + if (!schema) { + return jsonError(@"no schema for '%@'", jsonNumber); + } + if (![schema isKindOfClass:[NSDictionary class]]) { + return jsonError(@"schema is not is object '%@'", schema); } for (NSString * keyword in numericKeywords) { @@ -520,39 +543,39 @@ -(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) { - return NO; + return jsonError(@"multipleOf"); } } 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". - return NO; + return jsonError(@"'%@' must be lower than '%@'", jsonNumber, schemaItem); } } 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" - return NO; + return jsonError(@"'%@' must be lower than, or equal to '%@'", jsonNumber, schemaItem); } } } else if ([keyword isEqualToString:@"minimum"]) { 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". - return NO; + return jsonError(@"'%@' must be greater than '%@'", jsonNumber, schemaItem); } } 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" - return NO; + return jsonError(@"'%@' must be greater than, or equal to '%@'", jsonNumber, schemaItem); } } } } } - return YES; + return nil; } --(BOOL)_validateJSONString:(NSString*)jsonString withSchemaDict:(NSDictionary*)schema +-(NSError*)_validateJSONString:(NSString*)jsonString withSchemaDict:(NSDictionary*)schema { static NSArray * stringKeywords; static dispatch_once_t onceToken; @@ -572,12 +595,16 @@ -(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])) { + return jsonError(@"string '%@' length must be less than, or equal to %d", jsonString, realLength); + } } 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])) { + return jsonError(@"string '%@' length must be greater than, or equal to %d", jsonString, realLength); + } } 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. @@ -589,15 +616,15 @@ -(BOOL)_validateJSONString:(NSString*)jsonString withSchemaDict:(NSDictionary*)s } 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. - return NO; + return jsonError(@"string does not match regular expression"); } } } } - return YES; + return nil; } --(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary*)schema +-(NSError*)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary*)schema { static NSArray * objectKeywords; static dispatch_once_t onceToken; @@ -611,16 +638,20 @@ -(BOOL)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDictionary* 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]) { + return jsonError(@"object properties count (%d) must be less than, or equal to %@", jsonDict.count, schemaItem); /*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]) { + return jsonError(@"object properties count (%d) must be greater than, or equal to %@", jsonDict.count, schemaItem); /*invalid JSON dict*/ + } } else if ([keyword isEqualToString:@"required"]) { NSArray * requiredArray = schemaItem; for (NSObject * requiredProp in requiredArray) { NSString * requiredPropStr = (NSString*)requiredProp; if (![jsonDict valueForKey:requiredPropStr]) { - return NO; //required not present. invalid JSON dict. + return jsonError(@"required property '%@' is not present", requiredPropStr); //required not present. invalid JSON dict. } } } else if (!doneProperties && ([keyword isEqualToString:@"properties"] || [keyword isEqualToString:@"patternProperties"] || [keyword isEqualToString:@"additionalProperties"])) { @@ -677,7 +708,7 @@ -(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) { - return NO; + return jsonError(@"additionalProperties = NO"); } } else { //find keys from allkeys that are not in testSchemas and add additionalProperties @@ -700,8 +731,9 @@ -(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]) { - return NO; + NSError* error = [self _validateJSON:jsonDict[property] withSchemaDict:subschema]; + if (error) { + return error; } } } @@ -718,25 +750,26 @@ -(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]) { - return NO; + NSError* error = [self _validateJSON:jsonDict withSchemaDict:schemaDependency]; + if (error) { + return error; } } else if ([dependency isKindOfClass:[NSArray class]]) { NSArray * propertyDependency = dependency; //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]) { - return NO; + return jsonError(@"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"); } } } } } } - return YES; + return nil; } --(BOOL)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*)schema +-(NSError*)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*)schema { static NSArray * arrayKeywords; static dispatch_once_t onceToken; @@ -763,32 +796,37 @@ -(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]) { - return NO; + NSError* error = [self _validateJSON:jsonArray[index] withSchemaDict:items]; + if (error) { + return error; } } else if ([items isKindOfClass:[NSArray class]]) { if (index < [(NSArray *)items count]) { - if (![self _validateJSON:child withSchemaDict:items[index]]) { - return NO; + NSError* error = [self _validateJSON:child withSchemaDict:items[index]]; + if (error) { + return error; } } 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". - return NO; + return jsonError(@"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'"); } else { - if (![self _validateJSON:child withSchemaDict:additionalItems]) { - return NO; + NSError* error = [self _validateJSON:child withSchemaDict:additionalItems]; + if (error) { + return error; } } } } } } 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; } - //An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. + if ((NSInteger)[jsonArray count] > [schemaItem integerValue]){ + return jsonError(@"An array instance is valid against 'maxItems' if its size is less 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]) { + return jsonError(@"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:@"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. @@ -808,13 +846,13 @@ -(BOOL)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*)sche if (([uniqueItems count] + fudgeFactor) < [jsonArray count]) { - return NO; + return jsonError(@"all elements must be unique"); } } } } } - return YES; + return nil; } - (BOOL)valuesHaveOneAndTrue:(NSArray *)values diff --git a/Tests/KiteJSONValidatorTests.m b/Tests/KiteJSONValidatorTests.m index 2ce4ff5..f271168 100644 --- a/Tests/KiteJSONValidatorTests.m +++ b/Tests/KiteJSONValidatorTests.m @@ -60,9 +60,9 @@ - (void)testDraft4Suite XCTAssertTrue(success == YES, @"Unable to add the reference schema at '%@'", url); } - BOOL result = [validator validateJSONInstance:json[@"data"] withSchema:test[@"schema"]]; + error = [validator validateJSONInstance:json[@"data"] withSchema:test[@"schema"]]; BOOL desired = [json[@"valid"] boolValue]; - if (result != desired) { + if ((error?YES:NO) == desired) { XCTFail(@"Category: %@ Test: %@ Expected result: %i", test[@"description"], json[@"description"], desired); } else From c36615a6aad2589c6b0aa1b1bdc24e232d7859ae Mon Sep 17 00:00:00 2001 From: Max Lunin Date: Sun, 1 Mar 2015 22:56:19 +0200 Subject: [PATCH 2/4] NSError as a result of schema adding --- Sources/KiteJSONValidator.h | 19 ++--- Sources/KiteJSONValidator.m | 133 +++++++++++++++------------------ Tests/KiteJSONValidatorTests.m | 4 +- 3 files changed, 72 insertions(+), 84 deletions(-) diff --git a/Sources/KiteJSONValidator.h b/Sources/KiteJSONValidator.h index c6194b0..63aeca1 100644 --- a/Sources/KiteJSONValidator.h +++ b/Sources/KiteJSONValidator.h @@ -20,7 +20,7 @@ @param jsonData The JSON to be validated @param schemaData The draft4 JSON schema to validate against - @return Whether the json is validated. + @return Error if json is invalid. Nil if validated */ -(NSError*)validateJSONData:(NSData*)jsonData withSchemaData:(NSData*)schemaData; @@ -32,9 +32,9 @@ @param schemaData The data for the document to be converted to JSON @param url The fragmentless URL for this document - @return Whether the reference schema was successfully added. + @return Error if the reference schema is invalid. Nil if was successfully added. */ --(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url; +-(NSError*)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url; /** Used for adding an ENTIRE document to the list of reference schemas - the URL should therefore be fragmentless. @@ -43,9 +43,10 @@ @param url The fragmentless URL for this document @param shouldValidateSchema Whether the new reference schema should be validated against the "root" schema. - @return Whether the reference schema was successfully added. + + @return Error if the reference schema is invalid. Nil if was successfully added. */ --(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BOOL)shouldValidateSchema; +-(NSError*)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BOOL)shouldValidateSchema; /** Used for adding an ENTIRE document to the list of reference schemas - the URL should therefore be fragmentless. @@ -53,9 +54,9 @@ @param schema The dictionary representation of the JSON schema (the JSON was therefore valid). @param url The fragmentless URL for this document - @return Whether the reference schema was successfully added. + @return Error if the reference schema is invalid. Nil if was successfully added. */ --(BOOL)addRefSchema:(NSDictionary*)schema atURL:(NSURL*)url; +-(NSError*)addRefSchema:(NSDictionary*)schema atURL:(NSURL*)url; /** Used for adding an ENTIRE document to the list of reference schemas - the URL should therefore be fragmentless. @@ -64,9 +65,9 @@ @param url The fragmentless URL for this document @param shouldValidateSchema Whether the new reference schema should be validated against the "root" schema. - @return Whether the reference schema was successfully added. + @return Error if the reference schema is invalid. Nil if was successfully added. */ --(BOOL)addRefSchema:(NSDictionary *)schema atURL:(NSURL *)url validateSchema:(BOOL)shouldValidateSchema; +-(NSError*)addRefSchema:(NSDictionary *)schema atURL:(NSURL *)url validateSchema:(BOOL)shouldValidateSchema; @end diff --git a/Sources/KiteJSONValidator.m b/Sources/KiteJSONValidator.m index 2ded43f..99ccfc2 100644 --- a/Sources/KiteJSONValidator.m +++ b/Sources/KiteJSONValidator.m @@ -18,7 +18,7 @@ @interface KiteJSONValidator() @end -NSError* jsonError(NSString* format,...){ +NSError* ValidationError(NSString* format, ...){ static NSString* KiteJSONValidatorDomain = @"KiteJSONValidator"; @@ -45,84 +45,72 @@ -(id)init if (self) { NSURL *rootURL = [NSURL URLWithString:@"http://json-schema.org/draft-04/schema#"]; NSDictionary *rootSchema = [self rootSchema]; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-variable" - BOOL success = [self addRefSchema:rootSchema atURL:rootURL validateSchema:NO]; -#pragma clang diagnostic pop - NSAssert(success == YES, @"Unable to add the root schema!", nil); + NSAssert([self addRefSchema:rootSchema atURL:rootURL validateSchema:NO]==nil, @"Unable to add the root schema!", nil); } return self; } --(BOOL)addRefSchema:(NSDictionary *)schema atURL:(NSURL *)url validateSchema:(BOOL)shouldValidateSchema +-(NSError*)addRefSchema:(NSDictionary *)schema atURL:(NSURL *)url validateSchema:(BOOL)shouldValidateSchema { 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) { - return NO; + if (error) { + return error; } return [self addRefSchemaData:schemaData atURL:url validateSchema:shouldValidateSchema]; } --(BOOL)addRefSchema:(NSDictionary*)schema atURL:(NSURL*)url +-(NSError*)addRefSchema:(NSDictionary*)schema atURL:(NSURL*)url { return [self addRefSchema:schema atURL:url validateSchema:YES]; } --(BOOL)addRefSchemaData:(NSData *)schemaData atURL:(NSURL *)url +-(NSError*)addRefSchemaData:(NSData *)schemaData atURL:(NSURL *)url { return [self addRefSchemaData:schemaData atURL:url validateSchema:YES]; } --(BOOL)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BOOL)shouldValidateSchema +-(NSError*)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BOOL)shouldValidateSchema { + if (!url) return ValidationError(@"URL must not be empty"); + if (!schemaData || ![schemaData isKindOfClass:[NSData class]]) { - return NO; + return ValidationError(@"Data must be NSData class. Not %@", schemaData.class); } NSError * error = nil; id schema = [NSJSONSerialization JSONObjectWithData:schemaData options:0 error:&error]; - if (error != nil) { - return NO; + if (error) { + return error; } else if (![schema isKindOfClass:[NSDictionary class]]) { - return NO; + return ValidationError(@"Schema must be an 'Object'. Not %@", [schema class]); } - NSAssert(url != NULL, @"URL must not be empty", nil); - NSAssert(schema != NULL, @"Schema must not be empty", nil); + if (!schema) return ValidationError(@"Schema must not be empty"); - if (!url || !schema) - { - //NSLog(@"Invalid schema for URL (%@): %@", url, schema); - return NO; - } url = [self urlWithoutFragment:url]; //TODO:consider failing if the url contained a fragment. - if (shouldValidateSchema) - { + if (shouldValidateSchema) { NSDictionary *root = [self rootSchema]; - if (![root isEqualToDictionary:schema]) - { + if (![root isEqualToDictionary:schema]) { NSError* schemaError = [self validateJSON:schema withSchemaDict:root]; - NSAssert(!schemaError, @"Invalid schema", nil); - if (schemaError) - return NO; + if (schemaError) { + return ValidationError(@"Invalid schema %@", schemaError.localizedDescription); + } } - else - { + else { //NSLog(@"Can't really validate the root schema against itself, right? ... Right?"); } } - if (!_schemaRefs) - { + if (!_schemaRefs) { _schemaRefs = [[NSMutableDictionary alloc] init]; } self.schemaRefs[url] = schema; - return YES; + return nil; } -(NSDictionary *)rootSchema @@ -157,7 +145,7 @@ -(NSError*)pushToStackJSON:(id)json forSchema:(NSDictionary*)schema } KiteValidationPair * pair = [KiteValidationPair pairWithLeft:json right:schema]; if ([self.validationStack containsObject:pair]) { - return jsonError(@"loops detectsed"); //Detects loops + return ValidationError(@"Loops detectsed"); //Detects loops } [self.validationStack addObject:pair]; return nil; @@ -191,7 +179,7 @@ -(NSError*)validateJSON:(id)json withSchemaAtReference:(NSString*)refString { NSURL * refURI = [NSURL URLWithString:refString relativeToURL:self.resolutionStack.lastObject]; if (!refURI) { - return jsonError(@"No Ref URI"); + return ValidationError(@"No Ref URI for '%@'", refString); } //get the fragment, if it is a JSON-Pointer @@ -217,7 +205,7 @@ -(NSError*)validateJSON:(id)json withSchemaAtReference:(NSString*)refString } if (!schema) { - return jsonError(@"No schema for Ref URI"); + return ValidationError(@"No schema for Ref URI: %@", refURI); } for (NSString * component in pointerComponents) { @@ -237,7 +225,7 @@ -(NSError*)validateJSON:(id)json withSchemaAtReference:(NSString*)refString } if (!schema) { - return jsonError(@"No schema"); + return ValidationError(@"No schema"); } } NSError* error = [self _validateJSON:json withSchemaDict:schema]; @@ -284,7 +272,7 @@ -(NSError*)validateJSONInstance:(id)json withSchema:(NSDictionary*)schema json = @{jsonKey : json}; // schema = @{@"properties" : @{@"debugInvalidTopTypeKey" : schema}}; #else - return jsonError(@"is not valid JSON Object"); + return ValidationError(@"is not valid JSON Object"); #endif } NSData * jsonData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; @@ -308,28 +296,27 @@ -(NSError*)validateJSONData:(NSData*)jsonData withKey:(NSString*)key withSchemaD NSError * error = nil; id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; if (error) { - return error; + return ValidationError(@"JSON data must be valid JSON: @%", error.localizedDescription); } if (key != nil) { json = json[key]; } id schema = [NSJSONSerialization JSONObjectWithData:schemaData options:0 error:&error]; if (error) { - return error; + return ValidationError(@"Scheme must be valid JSON: %@", error.localizedDescription); } if (![schema isKindOfClass:[NSDictionary class]]) { - return jsonError(@"Schema must be 'Object' type"); + return ValidationError(@"Schema must be 'Object' type. Not %@", [schema class]); } return [self validateJSON:json withSchemaDict:schema]; } -(NSError*)validateJSON:(id)json withSchemaDict:(NSDictionary *)schema { - if (!schema || ![schema isKindOfClass:[NSDictionary class]]) { //NSLog(@"No schema specified, or incorrect data type: %@", schema); - return jsonError(@"Schema must be 'Object' type"); + return ValidationError(@"Schema must be 'Object' type. Not %@", [schema class]); } //need to make sure the validation of schema doesn't infinitely recurse (self references) // therefore should not expand any subschemas, and ensure schema are only checked on a 'top' level. @@ -413,7 +400,7 @@ -(NSError*)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema NSString* schemaRef = schema[@"$ref"]; if (schemaRef) { if (![schemaRef isKindOfClass:[NSString class]]) { - return jsonError(@"$ref '%@' is '%@' type, but must be a 'String' type", schemaRef, [schemaRef class]); + return ValidationError(@"$ref '%@' is '%@' type, but must be a 'String' type", schemaRef, [schemaRef class]); } return [self validateJSON:json withSchemaAtReference:schemaRef]; } @@ -446,7 +433,7 @@ -(NSError*)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema type = @"string"; typeValidator = ^{ return [self _validateJSONString:json withSchemaDict:schema]; }; } else { - return jsonError(@"The schema '%@' is not one of the valid types", [json class]); // the schema is not one of the valid types. + return ValidationError(@"The schema '%@' is not one of the valid types", [json class]); // the schema is not one of the valid types. } //TODO: extract the types first before the check (if there is no type specified, we'll never hit the checking code @@ -457,7 +444,7 @@ -(NSError*)__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]) { - return jsonError(@"enum '%@' does not contain '%@'", schemaItem, json); + return ValidationError(@"enum '%@' does not contain '%@'", schemaItem, json); } } else if ([keyword isEqualToString:@"type"]) { if ([schemaItem isKindOfClass:[NSString class]]) { @@ -465,11 +452,11 @@ -(NSError*)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema continue; } if (![schemaItem isEqualToString:type]) { - return jsonError(@"schema '%@' != '%@'", schemaItem, type); + return ValidationError(@"schema '%@' != '%@'", schemaItem, type); } } else { //array if (![schemaItem containsObject:type]) { - return jsonError(@"array '%@' does not contain '%@'", schemaItem, type); + return ValidationError(@"array '%@' does not contain '%@'", schemaItem, type); } } } else if ([keyword isEqualToString:@"allOf"]) { @@ -486,7 +473,7 @@ -(NSError*)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema return nil; } } - return jsonError(@"'%@' must be ANY OF '%@'", json, schemaItem); + return ValidationError(@"'%@' must be ANY OF '%@'", json, schemaItem); } else if ([keyword isEqualToString:@"oneOf"]) { int passes = 0; for (NSDictionary * subSchema in schemaItem) { @@ -495,16 +482,16 @@ -(NSError*)__validateJSON:(id)json withSchemaDict:(NSDictionary *)schema passes++; } if (passes > 1) { - return jsonError(@"'%@' must be ONE OF '%@'", json, schemaItem); + return ValidationError(@"'%@' must be ONE OF '%@'", json, schemaItem); } } if (passes != 1) { - return jsonError(@"'%@' must be ONE OF '%@'", json, schemaItem); + return ValidationError(@"'%@' must be ONE OF '%@'", json, schemaItem); } } else if ([keyword isEqualToString:@"not"]) { NSError* error = [self _validateJSON:json withSchemaDict:schemaItem]; if (error == nil) { - return jsonError(@"'%@' must be NOT '%@'", json, schemaItem); + return ValidationError(@"'%@' must be NOT '%@'", json, schemaItem); } } else if ([keyword isEqualToString:@"definitions"]) { @@ -529,10 +516,10 @@ -(NSError*)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictiona }); if (!schema) { - return jsonError(@"no schema for '%@'", jsonNumber); + return ValidationError(@"no schema for '%@'", jsonNumber); } if (![schema isKindOfClass:[NSDictionary class]]) { - return jsonError(@"schema is not is object '%@'", schema); + return ValidationError(@"schema is not is object '%@'", schema); } for (NSString * keyword in numericKeywords) { @@ -543,30 +530,30 @@ -(NSError*)_validateJSONNumeric:(NSNumber*)jsonNumber withSchemaDict:(NSDictiona //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) { - return jsonError(@"multipleOf"); + return ValidationError(@"multipleOf"); } } 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". - return jsonError(@"'%@' must be lower than '%@'", jsonNumber, schemaItem); + return ValidationError(@"'%@' must be lower than '%@'", jsonNumber, schemaItem); } } 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" - return jsonError(@"'%@' must be lower than, or equal to '%@'", jsonNumber, schemaItem); + return ValidationError(@"'%@' must be lower than, or equal to '%@'", jsonNumber, schemaItem); } } } else if ([keyword isEqualToString:@"minimum"]) { 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". - return jsonError(@"'%@' must be greater than '%@'", jsonNumber, schemaItem); + return ValidationError(@"'%@' must be greater than '%@'", jsonNumber, schemaItem); } } 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" - return jsonError(@"'%@' must be greater than, or equal to '%@'", jsonNumber, schemaItem); + return ValidationError(@"'%@' must be greater than, or equal to '%@'", jsonNumber, schemaItem); } } } @@ -596,14 +583,14 @@ -(NSError*)_validateJSONString:(NSString*)jsonString withSchemaDict:(NSDictionar NSInteger realLength = [jsonString lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4; if (!(realLength <= [schemaItem integerValue])) { - return jsonError(@"string '%@' length must be less than, or equal to %d", jsonString, realLength); + return ValidationError(@"string '%@' length must be less than, or equal to %d", jsonString, realLength); } } 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 jsonError(@"string '%@' length must be greater than, or equal to %d", jsonString, realLength); + return ValidationError(@"string '%@' length must be greater than, or equal to %d", jsonString, realLength); } } 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. @@ -616,7 +603,7 @@ -(NSError*)_validateJSONString:(NSString*)jsonString withSchemaDict:(NSDictionar } 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. - return jsonError(@"string does not match regular expression"); + return ValidationError(@"string does not match regular expression"); } } } @@ -639,19 +626,19 @@ -(NSError*)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDiction 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 jsonError(@"object properties count (%d) must be less than, or equal to %@", jsonDict.count, schemaItem); /*invalid JSON dict*/ + return ValidationError(@"object properties count (%d) must be less than, or equal to %@", jsonDict.count, schemaItem); /*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 jsonError(@"object properties count (%d) must be greater than, or equal to %@", jsonDict.count, schemaItem); /*invalid JSON dict*/ + return ValidationError(@"object properties count (%d) must be greater than, or equal to %@", jsonDict.count, schemaItem); /*invalid JSON dict*/ } } else if ([keyword isEqualToString:@"required"]) { NSArray * requiredArray = schemaItem; for (NSObject * requiredProp in requiredArray) { NSString * requiredPropStr = (NSString*)requiredProp; if (![jsonDict valueForKey:requiredPropStr]) { - return jsonError(@"required property '%@' is not present", requiredPropStr); //required not present. invalid JSON dict. + return ValidationError(@"required property '%@' is not present", requiredPropStr); //required not present. invalid JSON dict. } } } else if (!doneProperties && ([keyword isEqualToString:@"properties"] || [keyword isEqualToString:@"patternProperties"] || [keyword isEqualToString:@"additionalProperties"])) { @@ -708,7 +695,7 @@ -(NSError*)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDiction //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) { - return jsonError(@"additionalProperties = NO"); + return ValidationError(@"additionalProperties = NO"); } } else { //find keys from allkeys that are not in testSchemas and add additionalProperties @@ -759,7 +746,7 @@ -(NSError*)_validateJSONObject:(NSDictionary*)jsonDict withSchemaDict:(NSDiction //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]) { - return jsonError(@"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"); + return ValidationError(@"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"); } } } @@ -809,7 +796,7 @@ -(NSError*)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*) } 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". - return jsonError(@"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'"); + return ValidationError(@"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'"); } else { NSError* error = [self _validateJSON:child withSchemaDict:additionalItems]; if (error) { @@ -821,11 +808,11 @@ -(NSError*)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*) } } else if ([keyword isEqualToString:@"maxItems"]) { if ((NSInteger)[jsonArray count] > [schemaItem integerValue]){ - return jsonError(@"An array instance is valid against 'maxItems' if its size is less than, or equal to, the value of this keyword."); + return ValidationError(@"An array instance is valid against 'maxItems' if its size is less than, or equal to, the value of this keyword."); } } else if ([keyword isEqualToString:@"minItems"]) { if ((NSInteger)[jsonArray count] < [schemaItem integerValue]) { - return jsonError(@"An array instance is valid against 'minItems' if its size is greater than, or equal to, the value of this keyword."); + return ValidationError(@"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:@"uniqueItems"]) { if ([schemaItem isKindOfClass:[NSNumber class]] && [schemaItem boolValue] == YES) { @@ -846,7 +833,7 @@ -(NSError*)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*) if (([uniqueItems count] + fudgeFactor) < [jsonArray count]) { - return jsonError(@"all elements must be unique"); + return ValidationError(@"all elements must be unique"); } } } diff --git a/Tests/KiteJSONValidatorTests.m b/Tests/KiteJSONValidatorTests.m index f271168..47a94d9 100644 --- a/Tests/KiteJSONValidatorTests.m +++ b/Tests/KiteJSONValidatorTests.m @@ -56,8 +56,8 @@ - (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]; - XCTAssertTrue(success == YES, @"Unable to add the reference schema at '%@'", url); + NSError* addRefError = [validator addRefSchemaData:data atURL:url]; + XCTAssertNil(addRefError, @"Unable to add the reference schema at '%@': %@", url, addRefError.localizedDescription); } error = [validator validateJSONInstance:json[@"data"] withSchema:test[@"schema"]]; From 9e65b7ba44910a58f69f1a7d6af9cf5490e02e37 Mon Sep 17 00:00:00 2001 From: Max Lunin Date: Sun, 1 Mar 2015 23:19:06 +0200 Subject: [PATCH 3/4] minor fixes --- Sources/KiteJSONValidator.m | 52 ++++++++++++++++++++++++---------- Tests/KiteJSONValidatorTests.m | 12 -------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/Sources/KiteJSONValidator.m b/Sources/KiteJSONValidator.m index 99ccfc2..04e638a 100644 --- a/Sources/KiteJSONValidator.m +++ b/Sources/KiteJSONValidator.m @@ -72,6 +72,13 @@ -(NSError*)addRefSchemaData:(NSData *)schemaData atURL:(NSURL *)url return [self addRefSchemaData:schemaData atURL:url validateSchema:YES]; } +-(NSMutableDictionary *)schemaRefs{ + if (!_schemaRefs){ + _schemaRefs = [NSMutableDictionary dictionary]; + } + return _schemaRefs; +} + -(NSError*)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema:(BOOL)shouldValidateSchema { if (!url) return ValidationError(@"URL must not be empty"); @@ -106,9 +113,6 @@ -(NSError*)addRefSchemaData:(NSData*)schemaData atURL:(NSURL*)url validateSchema } } - if (!_schemaRefs) { - _schemaRefs = [[NSMutableDictionary alloc] init]; - } self.schemaRefs[url] = schema; return nil; } @@ -126,9 +130,7 @@ -(NSDictionary *)rootSchema NSData *rootSchemaData = [NSData dataWithContentsOfFile:rootSchemaPath]; NSError *error = nil; - rootSchema = [NSJSONSerialization JSONObjectWithData:rootSchemaData - options:kNilOptions - error:&error]; + rootSchema = [NSJSONSerialization JSONObjectWithData:rootSchemaData options:0 error:&error]; NSAssert(rootSchema != NULL, @"Root schema wasn't found", nil); NSAssert([rootSchema isKindOfClass:[NSDictionary class]], @"Root schema wasn't a dictionary", nil); }); @@ -136,13 +138,29 @@ -(NSDictionary *)rootSchema return rootSchema; } +-(NSMutableArray *)validationStack { + if (!_validationStack) { + _validationStack = [NSMutableArray array]; + } + return _validationStack; +} + +-(NSMutableArray *)resolutionStack{ + if (!_resolutionStack) { + _resolutionStack = [NSMutableArray array]; + } + return _resolutionStack; +} + +-(NSMutableArray *)schemaStack{ + if (!_schemaStack) { + _schemaStack = [NSMutableArray array]; + } + return _schemaStack; +} + -(NSError*)pushToStackJSON:(id)json forSchema:(NSDictionary*)schema { - if (self.validationStack == nil) { - self.validationStack = [NSMutableArray new]; - self.resolutionStack = [NSMutableArray new]; - self.schemaStack = [NSMutableArray new]; - } KiteValidationPair * pair = [KiteValidationPair pairWithLeft:json right:schema]; if ([self.validationStack containsObject:pair]) { return ValidationError(@"Loops detectsed"); //Detects loops @@ -323,15 +341,15 @@ -(NSError*)validateJSON:(id)json withSchemaDict:(NSDictionary *)schema //first validate the schema against the root schema then validate against the original //first check valid json (use NSJSONSerialization) - self.validationStack = [NSMutableArray new]; - self.resolutionStack = [NSMutableArray new]; - self.schemaStack = [NSMutableArray new]; + self.validationStack = [NSMutableArray array]; + self.resolutionStack = [NSMutableArray array]; + self.schemaStack = [NSMutableArray array]; [self setResolutionString:@"#" forSchema:schema]; NSError* error = [self _validateJSON:schema withSchemaDict:self.rootSchema]; if (error) { - return error; //error: invalid schema + return ValidationError(@"Invalid schema: %@", error.localizedDescription); } error = [self _validateJSON:json withSchemaDict:schema]; @@ -908,4 +926,8 @@ -(BOOL)checkSchemaRef:(NSDictionary*)schema } } +-(NSString *)description{ + return [NSString stringWithFormat:@"<%@ %p\n%@>", NSStringFromClass(self.class), self, self.schemaRefs]; +} + @end diff --git a/Tests/KiteJSONValidatorTests.m b/Tests/KiteJSONValidatorTests.m index 47a94d9..3708677 100644 --- a/Tests/KiteJSONValidatorTests.m +++ b/Tests/KiteJSONValidatorTests.m @@ -15,18 +15,6 @@ @interface Tests : XCTestCase @implementation Tests -- (void)setUp -{ - [super setUp]; - // Put setup code here. This method is called before the invocation of each test method in the class. -} - -- (void)tearDown -{ - // Put teardown code here. This method is called after the invocation of each test method in the class. - [super tearDown]; -} - - (void)testDraft4Suite { NSBundle * mainBundle = [NSBundle bundleForClass:[self class]]; From 93d413d6b9541893d5c0f05e7c25eae41c112fcf Mon Sep 17 00:00:00 2001 From: Max Lunin Date: Sun, 1 Mar 2015 23:43:33 +0200 Subject: [PATCH 4/4] minor fixes --- Sources/KiteJSONValidator.m | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Sources/KiteJSONValidator.m b/Sources/KiteJSONValidator.m index 04e638a..4541095 100644 --- a/Sources/KiteJSONValidator.m +++ b/Sources/KiteJSONValidator.m @@ -331,8 +331,7 @@ -(NSError*)validateJSONData:(NSData*)jsonData withKey:(NSString*)key withSchemaD -(NSError*)validateJSON:(id)json withSchemaDict:(NSDictionary *)schema { - if (!schema || - ![schema isKindOfClass:[NSDictionary class]]) { + if (!schema || ![schema isKindOfClass:[NSDictionary class]]) { //NSLog(@"No schema specified, or incorrect data type: %@", schema); return ValidationError(@"Schema must be 'Object' type. Not %@", [schema class]); } @@ -791,10 +790,14 @@ -(NSError*)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*) doneItems = YES; id additionalItems = schema[@"additionalItems"]; id items = schema[@"items"]; - if (additionalItems == nil) { additionalItems = [NSDictionary new];} - if (items == nil) { items = [NSDictionary new];} + if (additionalItems == nil) { + additionalItems = [NSDictionary dictionary]; + } + if (items == nil) { + items = [NSDictionary dictionary]; + } if ([additionalItems isKindOfClass:[NSNumber class]] && strcmp([additionalItems objCType], @encode(char)) == 0 && [additionalItems boolValue] == YES) { - additionalItems = [NSDictionary new]; + additionalItems = [NSDictionary dictionary]; } for (NSUInteger index = 0; index < [jsonArray count]; index++) { @@ -851,7 +854,7 @@ -(NSError*)_validateJSONArray:(NSArray*)jsonArray withSchemaDict:(NSDictionary*) if (([uniqueItems count] + fudgeFactor) < [jsonArray count]) { - return ValidationError(@"all elements must be unique"); + return ValidationError(@"All elements must be unique"); } } }