Created
July 3, 2013 11:43
-
-
Save leeprobert/5917243 to your computer and use it in GitHub Desktop.
AGNJSONValidator - suite of tools for validating JSON against schemas. Also references the AGNLogging classes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONContainerLogger.h | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 14/02/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
#import "AGNContainerWalker.h" | |
@interface AGNJSONContainerLogger : NSObject <AGNContainerWalkerDelegate> | |
- (AGNJSONContainerLogger*)initWithValidatorFeedbackArray:(NSArray*)feedback; | |
@property (nonatomic, readonly) NSString* string; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONContainerLogger.m | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 14/02/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#if ! __has_feature(objc_arc) | |
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). | |
#endif | |
#import "AGNJSONContainerLogger.h" | |
#import "AGNJSONValidatorFeedback.h" | |
#import "AGNLogging.h" | |
@implementation AGNJSONContainerLogger | |
{ | |
@private | |
NSMutableString* _string; | |
NSUInteger _indentLevel; | |
NSMutableString* _indentString; | |
NSMutableString* _indentString2; | |
NSMutableArray* _feedback; | |
NSMutableSet* _pathsWithFeedback; | |
NSMutableArray* _maxKeyLengthStack; | |
} | |
static NSString* kLogStr = @"<s>"; | |
static NSString* kLogNum = @"<n>"; | |
static NSString* kLogBool = @"<b>"; | |
static NSString* kLogObj = @"<o>"; | |
static NSString* kLogArray = @"<a>"; | |
static NSString* kLogNull = @"<0>"; | |
static NSString* kLogInvalid= @"<!>"; | |
- (AGNJSONContainerLogger*)init | |
{ | |
return [self initWithValidatorFeedbackArray:nil]; | |
} | |
- (AGNJSONContainerLogger*)initWithValidatorFeedbackArray:(NSArray*)feedback | |
{ | |
self = [super init]; | |
if (self) | |
{ | |
_string = [[NSMutableString alloc] init]; | |
_indentString = [[NSMutableString alloc] initWithString:@"\n"]; | |
_indentString2 = [[NSMutableString alloc] initWithString:@" "]; | |
_feedback = [feedback mutableCopy]; | |
_pathsWithFeedback = [[NSMutableSet alloc] init]; | |
_maxKeyLengthStack = [[NSMutableArray alloc] init]; | |
for (AGNJSONValidatorFeedback* one in _feedback) | |
{ | |
// For speed of membership testing, we add the naive hash to the set instead of the path array itself. | |
// This is because [NSArray hash] only uses the count, so many items in the set would have the | |
// same hash and thereby cause it to have to fall back to isEqual to verify membership. | |
NSNumber* naiveHash = [[NSNumber alloc] initWithUnsignedInteger:[AGNJSONValidatorFeedback naiveHashForItemPath:one.itemPath]]; | |
[_pathsWithFeedback addObject:naiveHash]; | |
} | |
} | |
return self; | |
} | |
- (void)dealloc | |
{ | |
AGNLogLWarningIf([_feedback count], @"Not all feedback was logged: %@", _feedback); | |
} | |
- (void)increaseIndent | |
{ | |
++_indentLevel; | |
[_indentString appendString:@" "]; | |
[_indentString2 appendString:@" "]; | |
} | |
- (void)decreaseIndent | |
{ | |
if (_indentLevel) | |
{ | |
--_indentLevel; | |
[_indentString deleteCharactersInRange:NSMakeRange([_indentString length] - 4, 4)]; | |
[_indentString2 deleteCharactersInRange:NSMakeRange([_indentString2 length] - 4, 4)]; | |
} | |
} | |
- (BOOL)addFeedbackLogsForPath:(NSArray*)path | |
{ | |
NSNumber* naiveHash = [[NSNumber alloc] initWithUnsignedInteger:[AGNJSONValidatorFeedback naiveHashForItemPath:path]]; | |
BOOL result = [_pathsWithFeedback containsObject:naiveHash]; | |
if (result) | |
{ | |
// Pull all the feedback with this path | |
for (NSUInteger i = 0; i < [_feedback count];) | |
{ | |
AGNJSONValidatorFeedback* one = _feedback[i]; | |
if ([one.itemPath isEqualToArray:path]) | |
{ | |
NSMutableString* s = [[one description] mutableCopy]; | |
[s insertString:@"^!!! " atIndex:0]; | |
[s indentWithString:_indentString2]; | |
[_string appendFormat:@"\n%@", s]; | |
[_feedback removeObjectAtIndex:i]; | |
} | |
else | |
++i; | |
} | |
} | |
return result; | |
} | |
- (NSUInteger)maxKeyLengthForDictionary:(NSDictionary*)dictionary | |
{ | |
NSUInteger maxLength = 0; | |
for (NSString* key in dictionary) | |
maxLength = MAX(maxLength, [key length]); | |
return MIN(maxLength, 40); // Always limit it to 40 at most | |
} | |
- (NSString*)paddedStringForKey:(NSString*)key | |
{ | |
NSMutableString* result = [NSMutableString stringWithFormat:@"\"%@\"", key]; | |
NSUInteger maxLength = [[_maxKeyLengthStack lastObject] unsignedIntegerValue]; | |
for (NSUInteger i = [key length]; i < maxLength; ++i) | |
[result appendString:@" "]; | |
return result; | |
} | |
- (void)appendValueStartIndentForWalker:(AGNContainerWalker*)walker | |
{ | |
[_string appendString:_indentString]; | |
id lastPathComponent = [walker.pathStack lastObject]; | |
if ([lastPathComponent isKindOfClass:[NSNumber class]]) | |
[_string appendFormat:@"[%@] ", lastPathComponent]; | |
} | |
- (void)containerWalker:(AGNContainerWalker*)walker willStartDictionary:(NSDictionary*)dictionary withKey:(NSString*)key | |
{ | |
[self appendValueStartIndentForWalker:walker]; | |
if (key) | |
[_string appendFormat:@"%@:%@ {", [self paddedStringForKey:key], kLogObj]; | |
else | |
[_string appendFormat:@"%@ {", kLogObj]; | |
[self addFeedbackLogsForPath:walker.pathStack]; | |
[self increaseIndent]; | |
[_maxKeyLengthStack addObject:@([self maxKeyLengthForDictionary:dictionary])]; | |
} | |
- (void)containerWalker:(AGNContainerWalker*)walker didEndDictionary:(NSDictionary*)dictionary withKey:(NSString*)key | |
{ | |
[_maxKeyLengthStack removeLastObject]; | |
[self decreaseIndent]; | |
if ([_string characterAtIndex:[_string length] - 1] != '{') | |
[_string appendString:_indentString]; | |
[_string appendString:@"}"]; | |
} | |
- (void)containerWalker:(AGNContainerWalker*)walker willStartArray:(NSArray*)array withKey:(NSString*)key | |
{ | |
[self appendValueStartIndentForWalker:walker]; | |
if (key) | |
[_string appendFormat:@"%@:%@ (%d)[", [self paddedStringForKey:key], kLogArray, [array count]]; | |
else | |
[_string appendFormat:@"%@ (%d)[", kLogArray, [array count]]; | |
[self increaseIndent]; | |
} | |
- (void)containerWalker:(AGNContainerWalker*)walker didEndArray:(NSArray*)array withKey:(NSString*)key | |
{ | |
[self decreaseIndent]; | |
if ([_string characterAtIndex:[_string length] - 1] != '[') | |
[_string appendString:_indentString]; | |
[_string appendString:@"]"]; | |
[self addFeedbackLogsForPath:walker.pathStack]; | |
} | |
- (NSString*)descriptionOfNonContainer:(NSObject*)value withKey:(NSString*)key | |
{ | |
NSString* valueString = nil; | |
NSString* kindString = nil; | |
if ([value isKindOfClass:[NSString class]]) | |
{ | |
kindString = kLogStr; | |
valueString = [NSString stringWithFormat:@"\"%@\"", value]; | |
} | |
else if ([value isKindOfClass:[NSNumber class]]) | |
{ | |
if ((__bridge CFBooleanRef)value == kCFBooleanFalse || (__bridge CFBooleanRef)value == kCFBooleanTrue) | |
{ | |
kindString = kLogBool; | |
valueString = (__bridge CFBooleanRef)value == kCFBooleanFalse ? @"false" : @"true"; | |
} | |
else | |
{ | |
kindString = kLogNum; | |
// Lets see if its likely to be a date | |
NSTimeInterval timeInterval = [(NSNumber*)value doubleValue]; | |
const NSTimeInterval minDateCutoff = 1199167200.0; // 1st January 2008 | |
const NSTimeInterval maxDateCutoff = 1609394400.0; // 31st December 2020 | |
if ((minDateCutoff <= timeInterval) && (timeInterval <= maxDateCutoff)) | |
{ | |
NSDate* asDate = [[NSDate alloc] initWithTimeIntervalSince1970:timeInterval]; | |
valueString = [NSString stringWithFormat:@"%@ (%@)", value, [asDate HTTPDateHeaderString]]; | |
} | |
else | |
valueString = [value description]; | |
} | |
} | |
else if ([value isKindOfClass:[NSNull class]]) | |
{ | |
kindString = kLogNull; | |
valueString = @"null"; | |
} | |
else | |
{ | |
kindString = kLogInvalid; | |
valueString = [NSString stringWithFormat:@"%s %@", object_getClassName(value), value]; | |
} | |
NSString* result = nil; | |
if (key) | |
result = [NSString stringWithFormat:@"%@:%@ %@", [self paddedStringForKey:key], kindString, valueString]; | |
else | |
result = [NSString stringWithFormat:@"%@ %@", kindString, valueString]; | |
return result; | |
} | |
- (void)containerWalker:(AGNContainerWalker*)walker didFindNonContainer:(NSObject*)value withKey:(NSString*)key | |
{ | |
[self appendValueStartIndentForWalker:walker]; | |
[_string appendString:[self descriptionOfNonContainer:value withKey:key]]; | |
[self addFeedbackLogsForPath:walker.pathStack]; | |
} | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONHelpers.h | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 19/06/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
/*! | |
@brief A protocol for converting native objects to JSON format | |
@discussion These are merely convenience wrapers around calls to NSJSONSerialization. | |
If you need anything more specialized just use NSJSONSerialization directly. | |
@see NSJSONSerialization, AGNConvertFromJSON | |
*/ | |
@protocol AGNConvertToJSON | |
@required | |
/*! | |
@brief Convert self to JSON format as an NSData (in UTF-8). | |
@result An NSData containing JSON. nil if the object cannot be converted | |
*/ | |
- (NSData*)convertToJSONData; | |
/*! | |
@brief Convert self to JSON format as an NSString. | |
@result An NSString containing JSON. nil if the object cannot be converted | |
*/ | |
- (NSString*)convertToJSONString; | |
@end | |
/* Declare NSArray and NSDictionary as being able to convert to JSON. | |
Note - You can't directly convert NSString, NSNumber or NSNull directly | |
to JSON. You need to wrap such objects in a single-item NSArray and then | |
convert the NSArray | |
*/ | |
@interface NSArray(AGNConvertToJSON) <AGNConvertToJSON> @end | |
@interface NSDictionary(AGNConvertToJSON) <AGNConvertToJSON> @end | |
/*! | |
@brief A protocol for converting JSON formatted data to native objects. | |
@discussion These are merely convenience wrapers around calls to NSJSONSerialization. | |
If you need anything more specialized just use NSJSONSerialization directly. | |
@see NSJSONSerialization, AGNConvertToJSON | |
*/ | |
@protocol AGNConvertFromJSON | |
@required | |
/*! | |
@brief Convert self from JSON format into a native object. | |
@result A native equivalent of the JSON formatted data. nil if self cannot be converted | |
@see convertJSONToNativeValueWithoutNulls | |
*/ | |
- (id)convertJSONToNativeValue; | |
/*! | |
@brief Convert self from JSON format into a native object removing any null values. | |
@result A native equivalent of the JSON formatted data without any nested nulls. nil if self cannot be converted | |
@see convertJSONToNativeValue | |
*/ | |
- (id)convertJSONToNativeValueWithoutNulls; | |
@end | |
// Declare NSData and NSString as being able to convert from JSON to native value | |
@interface NSData(AGNConvertFromJSON) <AGNConvertFromJSON> @end | |
@interface NSString(AGNConvertFromJSON) <AGNConvertFromJSON> @end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONHelpers.h | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 19/06/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#if ! __has_feature(objc_arc) | |
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). | |
#endif | |
#import "AGNJSONHelpers.h" | |
#import "AGNJSONTypeValidator.h" | |
static NSData* convertToJSONData(id value) | |
{ | |
NSData* result = nil; | |
@try | |
{ // dataWithJSONObject is a bit annoying as it can either throw an exception or return an error. Why?? | |
NSError* error = nil; | |
result = [NSJSONSerialization dataWithJSONObject:value options:0 error:&error]; | |
AGNLogLErrorIf(!result, @"NSJSONSerialization dataWithJSONObject failed with error:%@, for object: %@", error, value); | |
} | |
@catch (NSException *exception) | |
{ | |
AGNLogLError(@"NSJSONSerialization dataWithJSONObject failed with exception:%@, for object: %@", exception, value); | |
} | |
return result; | |
} | |
static NSString* convertToJSONString(id value) | |
{ | |
NSString* result = nil; | |
@autoreleasepool | |
{ | |
NSData* asData = convertToJSONData(value); | |
result = [[NSString alloc] initWithData:asData encoding:NSUTF8StringEncoding]; | |
} | |
return result; | |
} | |
@implementation NSArray(AGNConvertToJSON) | |
- (NSData*)convertToJSONData | |
{ | |
return convertToJSONData(self); | |
} | |
- (NSString*)convertToJSONString | |
{ | |
return convertToJSONString(self); | |
} | |
@end | |
@implementation NSDictionary(AGNConvertToJSON) | |
- (NSData*)convertToJSONData | |
{ | |
return convertToJSONData(self); | |
} | |
- (NSString*)convertToJSONString | |
{ | |
return convertToJSONString(self); | |
} | |
@end | |
@implementation NSData(AGNConvertFromJSON) | |
- (id)convertJSONToNativeValue | |
{ | |
NSError* error = nil; | |
id result = [NSJSONSerialization JSONObjectWithData:self options:0 error:&error]; | |
AGNLogLErrorIf(!result, @"NSJSONSerialization JSONObjectWithData failed:%@", error); | |
return result; | |
} | |
- (id)convertJSONToNativeValueWithoutNulls | |
{ | |
id valueWithPossibleNulls = [self convertJSONToNativeValue]; | |
if (!valueWithPossibleNulls) | |
return nil; | |
// To strip the Nulls, we send it through the validator with an empty schema | |
AGNJSONValidator* validator = [[AGNJSONValidator alloc] initWithSchemaURL:[AGNJSONValidator URLForEmptySchema]]; | |
id result = [validator validatedJSONValueFromJSONValue:valueWithPossibleNulls options:JSONValidator_strict]; | |
return result; | |
} | |
@end | |
@implementation NSString(AGNConvertFromJSON) | |
- (id)convertJSONToNativeValue | |
{ | |
id result = nil; | |
@autoreleasepool | |
{ | |
NSData* asData = [self dataUsingEncoding:NSUTF8StringEncoding]; | |
result = [asData convertJSONToNativeValue]; | |
} | |
return result; | |
} | |
- (id)convertJSONToNativeValueWithoutNulls | |
{ | |
id result = nil; | |
@autoreleasepool | |
{ | |
NSData* asData = [self dataUsingEncoding:NSUTF8StringEncoding]; | |
result = [asData convertJSONToNativeValueWithoutNulls]; | |
} | |
return result; | |
} | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONTypeValidator.h | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 14/02/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
@interface AGNJSONTypeValidator : NSObject | |
+ (AGNJSONTypeValidator*)validatorForJSONType:(NSString*)type; | |
- (Class)expectedClass; | |
- (id)resolveValue:(id)value; | |
- (NSMutableArray*)errorMessagesForValue:(id)value schema:(NSDictionary*)schema; | |
- (NSString*)descriptionForValue:(id)value; | |
@end | |
NSString* JSONTypeForValue(id value); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONTypeValidator.m | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 14/02/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#if ! __has_feature(objc_arc) | |
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). | |
#endif | |
#import "AGNJSONTypeValidator.h" | |
#import "AGNJSONValidatorConstants.h" | |
#import "AGNJSONValidator.h" | |
NSString* JSONTypeForValue(id value) | |
{ | |
NSString* result = nil; | |
if ((__bridge CFBooleanRef)value == kCFBooleanFalse || (__bridge CFBooleanRef)value == kCFBooleanTrue) | |
result = boolK; | |
else if ([value isKindOfClass:[NSString class]]) | |
result = stringK; | |
else if ([value isKindOfClass:[NSNumber class]]) | |
result = numberK; | |
else if ([value isKindOfClass:[NSDictionary class]]) | |
result = objectK; | |
else if ([value isKindOfClass:[NSArray class]]) | |
result = arrayK; | |
else if (value == [NSNull null]) | |
result = nullK; | |
return result; | |
} | |
@interface AGNJSONstringValidator : AGNJSONTypeValidator | |
@end | |
@implementation AGNJSONstringValidator | |
- (Class)expectedClass | |
{ | |
return [NSString class]; | |
} | |
- (id)resolveValue:(id)value | |
{ | |
id result = value; | |
NSString* JSONType = JSONTypeForValue(value); | |
if (JSONType == stringK) | |
; // Nothing to do, already the correct type | |
else if (JSONType == numberK) | |
result = [value stringValue]; | |
else if (JSONType == boolK) | |
result = [value boolValue] ? @"true" : @"false"; | |
return result; | |
} | |
- (NSUInteger)minLengthForClass | |
{ | |
return 0; | |
} | |
- (NSUInteger)maxLengthForClass | |
{ | |
return NSUIntegerMax; | |
} | |
- (NSUInteger)minLengthForSchema:(NSDictionary*)schema | |
{ | |
NSUInteger minLength = [schema[minLengthK] unsignedIntegerValue]; | |
return MAX([self minLengthForClass], minLength); | |
} | |
- (NSUInteger)maxLengthForSchema:(NSDictionary*)schema | |
{ | |
NSNumber* maxLength = schema[maxLengthK]; | |
NSUInteger result = maxLength ? [maxLength unsignedIntegerValue] : NSUIntegerMax; | |
result = MIN(result, [self maxLengthForClass]); | |
return result; | |
} | |
- (NSMutableArray*)errorMessagesForValue:(NSString*)value schema:(NSDictionary*)schema | |
{ | |
NSMutableArray* errors = [super errorMessagesForValue:value schema:schema]; | |
NSUInteger length = [value length]; | |
NSUInteger minLength = [self minLengthForSchema:schema]; | |
if (length < minLength) | |
{ | |
[errors addObject:[NSString stringWithFormat:@"String \"%@\" is shorter than the minLength of %d", value, minLength]]; | |
} | |
NSUInteger maxLength = [self maxLengthForSchema:schema]; | |
if (maxLength < length) | |
{ | |
[errors addObject:[NSString stringWithFormat:@"String \"%@\" is longer than the maxLength of %d", value, maxLength]]; | |
} | |
return errors; | |
} | |
@end | |
@interface AGNJSONNonEmptyStringValidator : AGNJSONstringValidator | |
@end | |
@implementation AGNJSONNonEmptyStringValidator | |
- (NSUInteger)minLengthForClass | |
{ | |
return 1; | |
} | |
@end | |
@interface AGNJSONnumberValidator : AGNJSONTypeValidator | |
@end | |
@implementation AGNJSONnumberValidator | |
- (id)resolveValue:(id)value | |
{ | |
id result = value; | |
NSString* JSONType = JSONTypeForValue(value); | |
if (JSONType == numberK) | |
; // Nothing to do, already the correct type | |
else if (JSONType == boolK) | |
result = [value boolValue] ? @1 : @0; | |
else if (JSONType == stringK) | |
{ | |
// See if we can round-trip the string | |
// No attempt is made to convert floating point here | |
NSNumber* asNumber = [NSNumber numberWithLongLong:[value longLongValue]]; | |
if ([[asNumber stringValue] isEqualToString:value]) | |
result = asNumber; | |
} | |
return result; | |
} | |
- (Class)expectedClass | |
{ | |
return [NSNumber class]; | |
} | |
- (BOOL)integersOnly | |
{ | |
return NO; | |
} | |
- (NSNumber*)minValueForClass | |
{ | |
return [NSDecimalNumber minimumDecimalNumber]; | |
} | |
- (NSNumber*)maxValueForClass | |
{ | |
return [NSDecimalNumber maximumDecimalNumber]; | |
} | |
- (NSNumber*)resolvedValueForSchema:(NSDictionary*)schema key:(NSString*)key | |
{ | |
return schema[key]; | |
} | |
- (NSNumber*)minValueForSchema:(NSDictionary*)schema | |
{ | |
NSNumber* minForClass = [self minValueForClass]; | |
NSNumber* minForSchema = [self resolvedValueForSchema:schema key:minimumK]; | |
NSNumber* result; | |
if (minForSchema && [minForSchema compare:minForClass] == NSOrderedAscending) | |
result = minForSchema; | |
else | |
result = minForClass; | |
return result; | |
} | |
- (NSNumber*)maxValueForSchema:(NSDictionary*)schema | |
{ | |
NSNumber* maxForClass = [self maxValueForClass]; | |
NSNumber* maxForSchema = [self resolvedValueForSchema:schema key:maximumK]; | |
NSNumber* result; | |
if (maxForSchema && [maxForSchema compare:maxForClass] == NSOrderedDescending) | |
result = maxForSchema; | |
else | |
result = maxForClass; | |
return result; | |
} | |
- (NSMutableArray*)errorMessagesForValue:(NSNumber*)value schema:(NSDictionary*)schema | |
{ | |
NSMutableArray* errors = [super errorMessagesForValue:value schema:schema]; | |
NSNumber* minValue = [self minValueForSchema:schema]; | |
if ([value compare:minValue] == NSOrderedAscending) | |
[errors addObject:[NSString stringWithFormat:@"Number (%@) is less than the minimum value (%@)", | |
[self descriptionForValue:value], [self descriptionForValue:minValue]]]; | |
NSNumber* maxValue = [self maxValueForSchema:schema]; | |
if ([value compare:maxValue] == NSOrderedDescending) | |
[errors addObject:[NSString stringWithFormat:@"Number (%@) is more than the maximum value (%@)", | |
[self descriptionForValue:value], [self descriptionForValue:maxValue]]]; | |
if ([self integersOnly]) | |
{ | |
// See if its not a whole number - This isn't a very pleasant way. Please find something better :/ | |
NSString* asString = [value stringValue]; | |
if ([asString rangeOfString:@"."].location != NSNotFound) | |
[errors addObject:[NSString stringWithFormat:@"Number (%@) should be an integer", asString]]; | |
} | |
return errors; | |
} | |
@end | |
@interface AGNJSONIntegerValidator : AGNJSONnumberValidator | |
@end | |
@implementation AGNJSONIntegerValidator | |
- (BOOL)integersOnly | |
{ | |
return YES; | |
} | |
@end | |
@interface AGNJSONPositiveIntegerValidator : AGNJSONIntegerValidator | |
@end | |
@implementation AGNJSONPositiveIntegerValidator | |
- (NSNumber*)minValueForClass | |
{ | |
return [NSDecimalNumber zero]; | |
} | |
@end | |
@interface AGNJSONboolValidator : AGNJSONPositiveIntegerValidator | |
@end | |
@implementation AGNJSONboolValidator | |
- (id)resolveValue:(id)value | |
{ | |
id result = value; | |
NSString* JSONType = JSONTypeForValue(value); | |
if (JSONType == boolK) | |
; // Nothing to do, already the correct type | |
else if (JSONType == numberK) | |
{ | |
if ([value isEqualToNumber:@1]) | |
result = (id)kCFBooleanTrue; | |
else if ([value isEqualToNumber:@0]) | |
result = (id)kCFBooleanFalse; | |
} | |
else if (JSONType == stringK) | |
{ | |
// We could be more leniant here, but we are already being lenient | |
// only true, false, Yes, No, 1 and 0 are recognized | |
if ([value isEqualToString:@"true"] || [value isEqualToString:@"Yes"] || [value isEqualToString:@"1"]) | |
result = (id)kCFBooleanTrue; | |
else if ([value isEqualToString:@"false"] || [value isEqualToString:@"No"] || [value isEqualToString:@"0"]) | |
result = (id)kCFBooleanFalse; | |
} | |
return result; | |
} | |
- (NSNumber*)minValueForClass | |
{ | |
return @0; | |
} | |
- (NSNumber*)maxValueForClass | |
{ | |
return @1; | |
} | |
- (NSMutableArray*)errorMessagesForValue:(NSNumber*)value schema:(NSDictionary*)schema | |
{ | |
NSMutableArray* errors = [super errorMessagesForValue:value schema:schema]; | |
if (JSONTypeForValue(value) != boolK) | |
[errors addObject:[NSString stringWithFormat:@"value (%@) is not a bool", value]]; | |
return errors; | |
} | |
@end | |
@interface AGNJSONTimestampValidator : AGNJSONIntegerValidator | |
@end | |
@implementation AGNJSONTimestampValidator | |
- (id)resolveValue:(id)value | |
{ | |
id result = value; | |
NSString* JSONType = JSONTypeForValue(value); | |
if (JSONType == numberK) | |
; // Nothing to do, already the correct type | |
else if (JSONType == boolK) | |
; // Nothing to do, we let this fail in caller | |
else if (JSONType == stringK) | |
{ | |
NSDate* asDate = [NSDate dateWithHTTPDateHeaderString:value]; | |
if (asDate) | |
result = [NSNumber numberWithLongLong:[asDate timeIntervalSince1970]]; | |
else | |
result = [super resolveValue:value]; | |
} | |
return result; | |
} | |
- (NSNumber*)resolvedValueForSchema:(NSDictionary*)schema key:(NSString*)key | |
{ | |
NSNumber* result = nil; | |
id val = schema[key]; | |
if (!val || [val isKindOfClass:[NSNumber class]]) | |
result = val; | |
else if ([val isKindOfClass:[NSString class]]) | |
{ | |
NSDate* asDate = [NSDate dateWithHTTPDateHeaderString:val]; | |
if (asDate) | |
result = @([asDate timeIntervalSince1970]); | |
} | |
return result; | |
} | |
- (NSString*)descriptionForValue:(id)value | |
{ | |
NSString* result = [NSString stringWithFormat:@"%@ - %@", | |
[super descriptionForValue:value], | |
[NSDate HTTPDateHeaderStringWithTimeIntervalSince1970:[value doubleValue]]]; | |
return result; | |
} | |
@end | |
@implementation AGNJSONTypeValidator | |
- (Class)expectedClass | |
{ | |
return [NSObject class]; | |
} | |
- (id)resolveValue:(id)value | |
{ | |
return value; | |
} | |
+ (AGNJSONTypeValidator*)validatorForJSONType:(NSString*)type | |
{ | |
static NSDictionary* sValidators = nil; | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
sValidators = @{ | |
numberK : [[AGNJSONnumberValidator alloc] init], | |
IntegerK : [[AGNJSONIntegerValidator alloc] init], | |
PositiveIntegerK : [[AGNJSONPositiveIntegerValidator alloc] init], | |
stringK : [[AGNJSONstringValidator alloc] init], | |
NonEmptyStringK : [[AGNJSONNonEmptyStringValidator alloc] init], | |
TimestampK : [[AGNJSONTimestampValidator alloc] init], | |
boolK : [[AGNJSONboolValidator alloc] init] | |
}; | |
}); | |
AGNJSONTypeValidator* validator = sValidators[type]; | |
return validator; | |
} | |
- (NSMutableArray*)errorMessagesForValue:(id)value schema:(NSDictionary*)schema | |
{ | |
return [NSMutableArray array]; | |
} | |
- (NSString*)descriptionForValue:(id)value | |
{ | |
return [value description]; | |
} | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONValidator.h | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 25/01/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
#import "AGNJSONValidatorFeedback.h" | |
typedef NS_OPTIONS(NSUInteger, AGNJSONValidatorOptions) | |
{ | |
/* These are bitmasks */ | |
/*! | |
When set, additional fields within dictionaries that are not specified in the | |
schema will be allowed (but generate a warning). | |
When not set, any additional fields in a dictionary will generate an error. | |
*/ | |
JSONValidator_allowExtraProperties = 1, | |
/*! | |
When set, a limited set of conversions from found values to expected types will be performed | |
to allow the JSON to pass validation. (Any conversions will generate a warning). | |
E.g Finding the string "23" when expecting a number will result in the value being converted | |
into the number 23. | |
When not set, no conversions will be performed and found values must be of the expected | |
type otherwise an error will be generated. | |
*/ | |
JSONValidator_allowConversions = 2, | |
/* These are predefined mask sets */ | |
//! Strict validation. None of the JSONValidator_allowXXX options will be enabled. | |
JSONValidator_strict = 0, | |
//! A reasonable default set of options for most uses. | |
JSONValidator_default = JSONValidator_allowExtraProperties | |
#if DEBUG | |
| JSONValidator_allowConversions | |
#endif | |
}; | |
/*! | |
@interface AGNJSONValidator | |
@brief AGNJSONValidator provides a means to validate JSON data against specific schemas. | |
@discussion For most uses, the convenience method +[AGNJSONValidator validatedJSONFrom:usingSchemaURL:options:feedback:] | |
should suffice. Validators are meant for one-shot validation. Don't try re-using one, I don't know if it would work. | |
@see The Schema format https://wiki.ciklum.net/display/AGN/Defining+JSON+Specifications+and+Schemas | |
*/ | |
@interface AGNJSONValidator : NSObject | |
/*! | |
@brief A convenience method for getting a schema URL from the main bundle. | |
@discussion The schemas are assumed to exist in the 'Schemas' subdirectoy of the main bundle's Resources folder and have | |
the 'json' extension. | |
@param schemaName The name of the schema file without an extension. | |
@result The schema's URL if the file exists, otherwise nil. | |
*/ | |
+ (NSURL*)URLForSchemaNamed:(NSString*)schemaName; | |
/*! | |
@brief The Empty Schema is one which validates any correctly formed JSON. | |
@result The returned URL does not point to a real file. Do not try to use | |
it with any NSURL-based APIs. | |
*/ | |
+ (NSURL*)URLForEmptySchema; | |
/*! | |
@brief A convenience method for validating anything against a schema. | |
@param jsonValue The json value to validate. | |
@param schemaURL The URL of the schema to validate against. | |
@param options The validation options. JSONValidator_default should suffice for most uses. | |
@param feedback Pass in a pointer to an NSArray* if you wish to receive the feedback from the | |
validator. Each item in the array will be an AGNJSONValidatorFeedback object. | |
@result If jsonValue validates against the schema then the relevant | |
foundation container for the JSON (NSArray or NSDictionary). Otherwise nil. The returned container may | |
be a different object to the jsonValue parameter. | |
@see AGNJSONValidatorFeedback and +[AGNJSONValidator annotatedStringForJSONValue:feedback:] | |
*/ | |
+ (id)validatedJSONFrom:(NSObject*)jsonValue | |
usingSchemaURL:(NSURL*)schemaURL | |
options:(AGNJSONValidatorOptions)options | |
feedback:(NSArray**)feedback; | |
/*! | |
@brief Creates a string suitable for logging the feedback from the validatedJSONFromXXX methods. | |
@discussion - You can use this method regardless of whether the validation passed or failed. | |
@param jsonValue The same object that you passed into the validation method | |
@param feedback The validator feedback. | |
@result An NSString with annotations for all items in the jsonValue | |
*/ | |
+ (NSString*)annotatedStringForJSONValue:(NSObject*)jsonValue | |
feedback:(NSArray*)feedback; | |
/*! | |
@brief Read the contents of a URL into a schema dictionary. | |
@discussion There is probably no need to use this externally, but may be useful in debugging | |
@param URL The URL whose data you wish to read. The data must be in JSON format. | |
@result An NSDictionary version of the schema. nil if the URL is invalid or cannot be read as JSON. | |
*/ | |
+ (NSDictionary*)schemaDictionaryForURL:(NSURL*)URL; | |
/*! | |
@brief Create a new validator | |
@param schemaURL The URL of the schema against which to validate. nil is not a valid schema URL | |
*/ | |
- (id)initWithSchemaURL:(NSURL*)schemaURL; | |
/*! | |
@brief Validates a Foundation container object (NSArray or NSDictionary). | |
@param jsonValue The json value to validate. | |
@param options The validation options. JSONValidator_default should suffice for most uses. | |
@result The validated container object or nil if it does not validate. The returned container may | |
be a different object to the jsonValue parameter. | |
*/ | |
- (id)validatedJSONValueFromJSONValue:(NSObject*)jsonValue | |
options:(AGNJSONValidatorOptions)options; | |
//! The schema URL to validate against | |
@property (nonatomic, readonly) NSURL* schemaURL; | |
//! The feedback from the most recent validation attempt for the validator. | |
@property (nonatomic, readonly) NSArray* allFeedback; // AGNJSONValidatorFeedback | |
@end | |
// These belong elsewhere eventually | |
@interface NSDate (M5Timestamp) | |
+ (NSDate*)dateWithHTTPDateHeaderString:(NSString*)dateString; | |
- (NSString*)HTTPDateHeaderString; | |
+ (NSTimeInterval)timeIntervalSince1970WithHTTPDateHeaderString:(NSString*)dateString; | |
+ (NSString*)HTTPDateHeaderStringWithTimeIntervalSince1970:(NSTimeInterval)timeInterval; | |
@end | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONValidator.m | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 25/01/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#if ! __has_feature(objc_arc) | |
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). | |
#endif | |
#import "AGNJSONValidator.h" | |
#import "AGNJSONTypeValidator.h" | |
#import "AGNJSONValidatorConstants.h" | |
#import "AGNContainerWalker.h" | |
#import "AGNJSONContainerLogger.h" | |
@interface AGNJSONAnyOfInfo : NSObject | |
{ | |
@public | |
NSArray* _schemas; | |
NSMutableArray* _schemaFeedback; | |
NSMutableArray* _schemaDescriptions; | |
NSUInteger _currentSchemaIndex; | |
NSUInteger _initialFeedbackCount; | |
} | |
@end | |
@implementation AGNJSONAnyOfInfo | |
- (id)initWithSchemas:(NSArray*)schemas currentFeedback:(NSArray*)feedback | |
{ | |
self = [super init]; | |
if (self) | |
{ | |
_schemas = [schemas copy]; | |
if (_schemas) | |
{ | |
_schemaFeedback = [[NSMutableArray alloc] init]; | |
_schemaDescriptions = [[NSMutableArray alloc] init]; | |
} | |
_initialFeedbackCount = [feedback count]; | |
} | |
return self; | |
} | |
@end | |
@interface AGNJSONAccumulator : NSObject <AGNContainerWalkerDelegate> | |
{ | |
NSURL* _schemaURL; | |
NSObject* _object; | |
NSArray* _schemaStack; // Really Mutable, but changes have to go via push and pop methods | |
NSMutableArray* _containerStack; | |
NSMutableArray* _feedback; | |
NSMutableDictionary* _referencedSchemas; | |
NSMutableArray* _anyOfInfoStack; | |
NSMutableSet* _cachedKeyStrings; | |
id _accumulated; | |
AGNJSONValidatorOptions _options; | |
AGNContainerWalker* _walker; | |
} | |
- (id)initWithObject:(NSObject*)object | |
schemaDictionary:(NSDictionary*)schemaDict | |
schemaURL:(NSURL*)schemaURL | |
options:(AGNJSONValidatorOptions)options; | |
- (id)accumulate; | |
- (NSArray*)feedback; | |
@end | |
@implementation AGNJSONAccumulator | |
- (id)initWithObject:(NSObject*)object | |
schemaDictionary:(NSDictionary*)schemaDict | |
schemaURL:(NSURL*)schemaURL | |
options:(AGNJSONValidatorOptions)options | |
{ | |
self = [super init]; | |
if (self) | |
{ | |
_object = object; | |
_schemaURL = schemaURL; | |
_schemaStack = [[NSMutableArray alloc] init]; | |
_containerStack = [[NSMutableArray alloc] init]; | |
_feedback = [[NSMutableArray alloc] init]; | |
_referencedSchemas = [[NSMutableDictionary alloc] init]; | |
_anyOfInfoStack = [[NSMutableArray alloc] init]; | |
_cachedKeyStrings = [[NSMutableSet alloc] init]; | |
_options = options; | |
if (schemaDict) | |
[self pushToSchemaStack:schemaDict]; | |
} | |
return self; | |
} | |
- (NSDictionary*)pushToSchemaStack:(NSDictionary*)schema | |
{ | |
if (!schema) | |
[NSException raise:NSInternalInconsistencyException format:@"NULL schema pushed to schema stack:%@", _schemaStack]; | |
NSString* refId; | |
NSMutableSet* refSet = nil; | |
// See if there is a reference schema and add that instead | |
while ((refId = schema[refK])) | |
{ | |
NSDictionary* refSchema = [self referencedSchemaWithId:refId]; | |
if (!refSchema) | |
{ | |
[self addError:[NSString stringWithFormat:@"Referenced schema not found: %@", refId]]; | |
schema = @{}; | |
} | |
else | |
{ | |
if (refSet && [refSet containsObject:refId]) | |
{ | |
// We've gone recursive - abandon ship | |
[self addError:[NSString stringWithFormat:@"Referenced schema went recursive: %@, %@", refId, refSet]]; | |
schema = @{}; | |
} | |
else | |
{ | |
if (!refSet) | |
refSet = [[NSMutableSet alloc] init]; | |
[refSet addObject:refId]; | |
schema = refSchema; | |
} | |
} | |
} | |
[(NSMutableArray*)_schemaStack addObject:schema]; | |
return [_schemaStack lastObject]; | |
} | |
- (void)popFromSchemaStack | |
{ | |
[(NSMutableArray*)_schemaStack removeLastObject]; | |
} | |
- (id)accumulate | |
{ | |
_walker = [[AGNContainerWalker alloc] init]; | |
[_walker setDelegate:self]; | |
[_walker walkContainer:_object]; | |
_walker = nil; | |
return _accumulated; | |
} | |
- (NSArray*)adjustedMessages:(NSArray*)messages | |
{ | |
AGNJSONAnyOfInfo* anyOfInfo = [_anyOfInfoStack lastObject]; | |
if (anyOfInfo && anyOfInfo->_schemas && (anyOfInfo->_currentSchemaIndex < [anyOfInfo->_schemas count])) | |
{ | |
NSString* schemaDescription = anyOfInfo->_schemaDescriptions[anyOfInfo->_currentSchemaIndex]; | |
NSMutableArray* adjustedMessages = [NSMutableArray arrayWithCapacity:[messages count]]; | |
for (__strong NSString* message in messages) | |
{ | |
message = [message stringByAppendingFormat:@" validating against '%@'", schemaDescription]; | |
[adjustedMessages addObject:message]; | |
} | |
return adjustedMessages; | |
} | |
else | |
return messages; | |
} | |
- (void)addErrors:(NSArray*)messages | |
{ | |
[_feedback addObject:[AGNJSONValidatorFeedback errorWithPath:_walker.pathStack messages:[self adjustedMessages:messages]]]; | |
} | |
- (void)addError:(NSString*)message | |
{ | |
[self addErrors:@[message]]; | |
} | |
- (void)addWarning:(NSString*)message | |
{ | |
[_feedback addObject:[AGNJSONValidatorFeedback warningWithPath:_walker.pathStack messages:[self adjustedMessages:@[message]]]]; | |
} | |
- (void)addNotice:(NSString*)message | |
{ | |
[_feedback addObject:[AGNJSONValidatorFeedback noticeWithPath:_walker.pathStack messages:[self adjustedMessages:@[message]]]]; | |
} | |
- (NSArray*)feedback | |
{ | |
return _feedback; | |
} | |
- (NSDictionary*)referencedSchemaWithId:(NSString*)schemaId | |
{ | |
if (!schemaId) | |
return nil; | |
NSDictionary* result = _referencedSchemas[schemaId]; | |
if (!result) | |
{ | |
NSString* schemaFilename = [schemaId stringByAppendingString:@".json"]; | |
NSURL* newURL = [[_schemaURL URLByDeletingLastPathComponent] URLByAppendingPathComponent:schemaFilename]; | |
result = [AGNJSONValidator schemaDictionaryForURL:newURL]; | |
if (result) // Stash it away in case it gets used multiple times | |
_referencedSchemas[schemaId] = result; | |
} | |
return result; | |
} | |
- (NSString*)cachedKeyForKey:(NSString*)key | |
{ | |
/* | |
We don't want every occurance of the same key to be its own unique object. | |
So we cache every key and return the cached version if we already hit an equivalent key. | |
Note - we don't do the same for all string values, only keys. NSJSONSerialization already | |
seems to re-use strings, but not keys for some reason. | |
*/ | |
NSString* cachedKey = [_cachedKeyStrings member:key]; | |
if (!cachedKey) | |
{ | |
cachedKey = [key copy]; | |
[_cachedKeyStrings addObject:cachedKey]; | |
} | |
return cachedKey; | |
} | |
#pragma mark AGNContainerWalkerDelegate | |
- (void)containerWalker:(AGNContainerWalker*)walker willStartDictionary:(NSDictionary*)dictionary withKey:(NSString*)key | |
{ | |
NSMutableDictionary* newDict = [NSMutableDictionary dictionary]; | |
id currentContainer = [_containerStack lastObject]; | |
if (!currentContainer) | |
{ | |
if (!key) | |
{ // This is the top level container | |
_accumulated = newDict; | |
} | |
else | |
{ | |
[NSException raise:NSInternalInconsistencyException format:@"Started Dict with key:%@ when no container", key]; | |
} | |
} | |
else | |
{ | |
if (key) | |
((NSMutableDictionary*)currentContainer)[[self cachedKeyForKey:key]] = newDict; | |
else | |
{ | |
if ([currentContainer isKindOfClass:[NSArray class]]) | |
[currentContainer addObject:newDict]; | |
else | |
[NSException raise:NSInternalInconsistencyException format:@"Trying to add object without key:%@ to dictionary", key]; | |
} | |
} | |
[_containerStack addObject:newDict]; | |
NSDictionary* currentSchema = [_schemaStack lastObject]; | |
if (key) | |
{ // Look for the subschema | |
currentSchema = [self pushToSchemaStack:currentSchema[propertiesK][key] ?: @{}]; | |
} | |
NSString* type = currentSchema[typeK]; | |
if (type && ![type isEqualToString:objectK]) | |
{ | |
[self addError:[NSString stringWithFormat:@"Expected:%@, got:%@", type, objectK]]; | |
} | |
NSArray* anyOfSchemas = currentSchema[anyOfK]; | |
if (anyOfSchemas) | |
{ | |
if (![anyOfSchemas isKindOfClass:[NSArray class]]) | |
[NSException raise:NSInternalInconsistencyException format:@"Schema specifies 'anyOf' that is not an array"]; | |
if (![anyOfSchemas count]) | |
[NSException raise:NSInternalInconsistencyException format:@"Schema specifies 'anyOf' that is an empty array"]; | |
currentSchema = [self pushToSchemaStack:anyOfSchemas[0]]; | |
} | |
AGNJSONAnyOfInfo* anyOfInfo = [[AGNJSONAnyOfInfo alloc] initWithSchemas:anyOfSchemas currentFeedback:_feedback]; | |
[_anyOfInfoStack addObject:anyOfInfo]; | |
if (anyOfSchemas) | |
{ | |
// Try title first, then description, then schema index | |
NSString* schemaDescription = currentSchema[titleK] ?: (currentSchema[descriptionK] ?: @"[0]"); | |
[anyOfInfo->_schemaDescriptions addObject:schemaDescription]; | |
} | |
} | |
- (BOOL)containerWalker:(AGNContainerWalker*)walker shouldRewindDictionary:(NSDictionary*)dictionary withKey:(NSString*)key | |
{ | |
NSDictionary* currentSchema = [_schemaStack lastObject]; | |
NSMutableDictionary* currentDictionary = [_containerStack lastObject]; | |
// Add feedback for any missing required properties | |
NSArray* requiredKeys = currentSchema[requiredK]; | |
if ([requiredKeys count]) | |
{ | |
NSMutableArray* missingKeys = [NSMutableArray new]; | |
for (NSString* key in requiredKeys) | |
{ | |
if (!currentDictionary[key]) | |
[missingKeys addObject:key]; | |
} | |
if ([missingKeys count]) | |
[self addError:[NSString stringWithFormat:@"Object is missing required keys: %@", missingKeys]]; | |
} | |
// Add feedback for any additional properties | |
NSArray* schemaProperties = [currentSchema[propertiesK] allKeys]; | |
if ([schemaProperties count]) | |
{ | |
NSMutableSet* foundProperties = [[NSMutableSet alloc] initWithArray:[dictionary allKeys]]; | |
NSSet* knownToSchema = [[NSSet alloc] initWithArray:schemaProperties]; | |
[foundProperties minusSet:knownToSchema]; | |
if ([foundProperties count]) | |
{ | |
if (_options & JSONValidator_allowExtraProperties) | |
{ | |
[self addWarning:[NSString stringWithFormat:@"Object contains additional keys: %@", [foundProperties allObjects]]]; | |
} | |
else | |
{ | |
[self addError:[NSString stringWithFormat:@"Object contains additional keys: %@", [foundProperties allObjects]]]; | |
} | |
} | |
} | |
BOOL rewind = NO; | |
AGNJSONAnyOfInfo* anyOfInfo = [_anyOfInfoStack lastObject]; | |
if (anyOfInfo->_schemas) | |
{ | |
BOOL isError = NO; | |
NSRange newItemsRange = NSMakeRange(anyOfInfo->_initialFeedbackCount, [_feedback count] - anyOfInfo->_initialFeedbackCount); | |
if (newItemsRange.length) // There was feedback for this schema | |
{ | |
NSArray* newFeedback = [_feedback subarrayWithRange:newItemsRange]; | |
// See if there are any errors in the feedback | |
for (AGNJSONValidatorFeedback* oneFeedback in newFeedback) | |
{ | |
if (oneFeedback.isError) | |
{ | |
isError = YES; | |
break; | |
} | |
} | |
if (isError) | |
{ | |
// Stash new feedback away in case we can't validate against any schemas | |
[anyOfInfo->_schemaFeedback addObject:newFeedback]; | |
// Rewind the feedback | |
[_feedback removeObjectsInRange:newItemsRange]; | |
if (++anyOfInfo->_currentSchemaIndex < [anyOfInfo->_schemas count]) | |
{ | |
// There are more schemas to validate against. Roll back ready for rewind | |
rewind = YES; | |
// Clean the current dictionary of any items that validated. | |
[currentDictionary removeAllObjects]; | |
// Switch current schema to next one to try | |
[self popFromSchemaStack]; | |
currentSchema = [self pushToSchemaStack:anyOfInfo->_schemas[anyOfInfo->_currentSchemaIndex]]; | |
// Try title first, then description, then schema index | |
NSString* schemaDescription = currentSchema[titleK] ?: (currentSchema[descriptionK] ?: [NSString stringWithFormat:@"[%d]", anyOfInfo->_currentSchemaIndex]); | |
[anyOfInfo->_schemaDescriptions addObject:schemaDescription]; | |
} | |
else | |
{ | |
// No more schemas to try. Add all the feedback we got so far for all attempts at validating | |
[self addError:[NSString stringWithFormat:@"Object could not validate against any of its 'anyOf' schemas: %@", anyOfInfo->_schemaDescriptions]]; | |
for (NSArray* oneSchemaFeedback in anyOfInfo->_schemaFeedback) | |
{ | |
[_feedback addObjectsFromArray:oneSchemaFeedback]; | |
} | |
} | |
} | |
} | |
if (!isError) | |
{ | |
// We validated ok against this schema. | |
if ([currentSchema[validatorIsCatchAllSchemaK] boolValue]) | |
{ | |
// We hit a catch all schema - In this case we want emit all the feedback gathered for the failed schemas | |
// but mark errors as being treated as warnings to prevent validator from thinking there are real errors. | |
NSMutableArray* descriptions = [anyOfInfo->_schemaDescriptions mutableCopy]; | |
[descriptions removeLastObject]; // Don't report the validating schema as being one of the ones that failed. | |
[self addNotice:[NSString stringWithFormat:@"Object could not validate against any of its main 'anyOf' schemas: %@", descriptions]]; | |
descriptions = nil; | |
for (NSArray* oneSchemaFeedback in anyOfInfo->_schemaFeedback) | |
{ | |
for (AGNJSONValidatorFeedback* oneFeedback in oneSchemaFeedback) | |
{ | |
oneFeedback.treatErrorAsWarning = YES; | |
[_feedback addObject:oneFeedback]; | |
} | |
} | |
} | |
} | |
} | |
return rewind; | |
} | |
- (void)containerWalker:(AGNContainerWalker*)walker didEndDictionary:(NSDictionary*)dictionary withKey:(NSString*)key | |
{ | |
AGNJSONAnyOfInfo* anyOfInfo = [_anyOfInfoStack lastObject]; | |
if (anyOfInfo->_schemas) | |
{ | |
[self popFromSchemaStack]; | |
} | |
[_anyOfInfoStack removeLastObject]; | |
if (key) | |
[self popFromSchemaStack]; | |
[_containerStack removeLastObject]; | |
} | |
- (void)containerWalker:(AGNContainerWalker*)walker willStartArray:(NSArray*)array withKey:(NSString*)key | |
{ | |
NSMutableArray* newArray = [NSMutableArray array]; | |
id currentContainer = [_containerStack lastObject]; | |
if (!currentContainer) | |
{ | |
if (!key) | |
{ // This is the top level container | |
_accumulated = newArray; | |
} | |
else | |
{ | |
[NSException raise:NSInternalInconsistencyException format:@"Started array with key:%@ when no container", key]; | |
} | |
} | |
else | |
{ | |
if (key) | |
((NSMutableDictionary*)currentContainer)[[self cachedKeyForKey:key]] = newArray; | |
else | |
{ | |
if (![currentContainer isKindOfClass:[NSArray class]]) | |
[NSException raise:NSInternalInconsistencyException format:@"Trying to add object without key:%@ to dictionary", key]; | |
} | |
} | |
[_containerStack addObject:newArray]; | |
NSDictionary* currentSchema = [_schemaStack lastObject]; | |
if (key) | |
{ | |
// Look for the subschema | |
currentSchema = [self pushToSchemaStack:currentSchema[propertiesK][key] ?: @{}]; | |
} | |
NSString* type = currentSchema[typeK]; | |
if (type && ![type isEqualToString:arrayK]) | |
{ | |
[self addError:[NSString stringWithFormat:@"Expected:%@, got:%@", type, arrayK]]; | |
} | |
[self pushToSchemaStack:currentSchema[itemsK] ?: @{}]; | |
} | |
- (void)containerWalker:(AGNContainerWalker*)walker didEndArray:(NSArray*)array withKey:(NSString*)key | |
{ | |
NSDictionary* currentSchema = [_schemaStack lastObject]; | |
NSMutableArray* currentArray = [_containerStack lastObject]; | |
NSUInteger minLength = [currentSchema[minLengthK] integerValue]; | |
if ([currentArray count] < minLength) | |
{ | |
[self addError:[NSString stringWithFormat:@"Array has minLength of %d, but only has %d items", minLength, [currentArray count]]]; | |
} | |
[self popFromSchemaStack]; | |
if (key) | |
[self popFromSchemaStack]; | |
[_containerStack removeLastObject]; | |
} | |
- (void)containerWalker:(AGNContainerWalker*)walker didFindNonContainer:(NSObject*)value withKey:(NSString*)key | |
{ | |
if (value == [NSNull null]) | |
{ | |
[self addNotice:@"Ignored a null value"]; | |
return; | |
} | |
NSDictionary* currentSchema = [_schemaStack lastObject]; | |
NSDictionary* itemSchema = key ? currentSchema[propertiesK][key] : currentSchema; | |
const id originalValue = value; | |
AGNJSONTypeValidator* validator = nil; | |
NSString* expectedType = nil; | |
BOOL isValid = NO; | |
NSArray* enumerated = itemSchema[enumK]; | |
if (enumerated) | |
{ | |
NSUInteger index = 0; | |
BOOL multipleTypes = NO; | |
for (id item in enumerated) | |
{ | |
NSString* itemType = JSONTypeForValue(item); | |
if (!index) | |
expectedType = itemType; | |
else if (expectedType != itemType) | |
multipleTypes = YES; | |
++index; | |
} | |
if (multipleTypes) | |
[self addError:[NSString stringWithFormat:@"Enum contains multiple types:%@", enumerated]]; | |
else if (!expectedType) | |
[self addError:[NSString stringWithFormat:@"Enum does not specify any valid items:%@", enumerated]]; | |
else | |
{ | |
if (_options & JSONValidator_allowConversions) | |
{ | |
validator = [AGNJSONTypeValidator validatorForJSONType:expectedType]; | |
if (validator) | |
value = [validator resolveValue:value]; | |
} | |
NSString* valueType = JSONTypeForValue(value); | |
if (valueType != expectedType) | |
{ | |
[self addError:[NSString stringWithFormat:@"Value (%@) is not the right type. Expected:%@, got:%@", | |
value, expectedType, valueType ?: [value class]]]; | |
} | |
else | |
{ | |
if (![enumerated containsObject:value]) | |
{ | |
[self addError:[NSString stringWithFormat:@"Value (%@) is not in enum %@", value, enumerated]]; | |
} | |
else | |
isValid = YES; | |
} | |
} | |
} | |
else | |
{ | |
expectedType = itemSchema[typeK]; | |
if (expectedType) | |
{ | |
AGNJSONTypeValidator* validator = [AGNJSONTypeValidator validatorForJSONType:expectedType]; | |
if (!validator) | |
{ | |
if ([expectedType isEqualToString:objectK] || [expectedType isEqualToString:arrayK]) | |
{ | |
[self addError:[NSString stringWithFormat:@"Expected:%@, got:%@", | |
expectedType, JSONTypeForValue(value) ?: [value class]]]; | |
} | |
else | |
[self addError:[NSString stringWithFormat:@"Unknown type found (%@) for value (%@)", expectedType, value]]; | |
} | |
else | |
{ | |
if (_options & JSONValidator_allowConversions) | |
value = [validator resolveValue:value]; | |
if (![value isKindOfClass:[validator expectedClass]]) | |
{ | |
[self addError:[NSString stringWithFormat:@"Expected:%@, got:%@", | |
expectedType, JSONTypeForValue(value) ?: [value class]]]; | |
} | |
else | |
{ | |
NSArray* errorMessages = [validator errorMessagesForValue:value schema:itemSchema]; | |
if ([errorMessages count]) | |
[self addErrors:errorMessages]; | |
else | |
isValid = YES; | |
} | |
} | |
} | |
} | |
if (isValid && (value != originalValue)) | |
{ | |
// Its valid, but has been converted - Let's warn | |
[self addWarning:[NSString stringWithFormat:@"Value (%@) was converted. Expected:%@, got:%@", | |
value, expectedType, JSONTypeForValue(originalValue) ?: [originalValue class]]]; | |
} | |
if (!key) | |
[[_containerStack lastObject] addObject:value]; | |
else | |
[_containerStack lastObject][[self cachedKeyForKey:key]] = value; | |
} | |
@end | |
@implementation AGNJSONValidator { | |
NSDictionary* _schemaDictionary; | |
} | |
+ (NSURL*)URLForSchemaNamed:(NSString*)schemaName | |
{ | |
NSURL* schemaURL = [[NSBundle mainBundle] URLForResource:schemaName | |
withExtension:@"json" | |
subdirectory:@"Schemas"]; | |
if (!schemaURL) | |
AGNLogError(@"Cannot find schema named: %@", schemaName); | |
return schemaURL; | |
} | |
+ (NSURL*)URLForEmptySchema | |
{ | |
static NSURL* sEmptySchemaURL; | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
sEmptySchemaURL = [[NSURL alloc] initFileURLWithPath:@"/{}"]; | |
}); | |
return sEmptySchemaURL; | |
} | |
+ (NSDictionary*)schemaDictionaryForURL:(NSURL*)URL | |
{ | |
NSDictionary* result = nil; | |
NSError* error = nil; | |
if ([URL isEqual:[self URLForEmptySchema]]) | |
result = @{}; | |
else | |
{ | |
NSData* schemaData = [[NSData alloc] initWithContentsOfURL:URL | |
options:NSDataReadingMappedIfSafe | |
error:&error]; | |
if (schemaData) | |
result = [NSJSONSerialization JSONObjectWithData:schemaData | |
options:0 | |
error:&error]; | |
} | |
AGNLogErrorIf(!result, @"Schema %@ could not be read. Error:%@", URL, error); | |
return result; | |
} | |
- (id)initWithSchemaURL:(NSURL*)schemaURL | |
{ | |
self = [super init]; | |
if (self) | |
{ | |
_schemaURL = schemaURL; | |
_schemaDictionary = [[self class] schemaDictionaryForURL:_schemaURL]; | |
if (!_schemaDictionary) | |
{ | |
self = nil; | |
} | |
} | |
return self; | |
} | |
- (id)validatedJSONValueFromJSONValue:(NSObject*)jsonValue options:(AGNJSONValidatorOptions)options | |
{ | |
if (!jsonValue) | |
return nil; | |
if (!_schemaDictionary) | |
{ | |
AGNLogError(@"Schema missing"); | |
return nil; | |
} | |
NSObject* result = nil; | |
@autoreleasepool | |
{ | |
AGNJSONAccumulator* accumulator = nil; | |
@try | |
{ | |
accumulator = [[AGNJSONAccumulator alloc] initWithObject:jsonValue | |
schemaDictionary:_schemaDictionary | |
schemaURL:_schemaURL | |
options:options]; | |
result = [accumulator accumulate]; | |
} | |
@catch (NSException *exception) | |
{ | |
AGNLogError(@"Exception during JSON validation: %@", exception); | |
// This is always fatal to validation and ends up with a nil result | |
} | |
@finally | |
{ | |
_allFeedback = [accumulator feedback]; | |
if ([_allFeedback count]) | |
{ | |
AGNJSONValidatorFeedbackSeverity maxSeverity = feedbackSeverityNotice; | |
for (AGNJSONValidatorFeedback* oneFeedback in _allFeedback) | |
{ | |
maxSeverity = MAX(maxSeverity, oneFeedback.severity); | |
if (maxSeverity >= feedbackSeverityError) | |
break; | |
} | |
if (maxSeverity >= feedbackSeverityError) | |
{ | |
AGNLogError(@"Schema named %@ found errors while validating JSON:%@", | |
[_schemaURL lastPathComponent], | |
[AGNJSONValidator annotatedStringForJSONValue:jsonValue feedback:_allFeedback]); | |
} | |
else if (maxSeverity == feedbackSeverityWarning) | |
{ | |
AGNLogWarning(@"Schema named %@ found warnings while validating JSON:%@", | |
[_schemaURL lastPathComponent], | |
[AGNJSONValidator annotatedStringForJSONValue:jsonValue feedback:_allFeedback]); | |
} | |
else | |
{ | |
AGNLogNotice(@"Schema named %@ found notices while validating JSON:%@", | |
[_schemaURL lastPathComponent], | |
[AGNJSONValidator annotatedStringForJSONValue:jsonValue feedback:_allFeedback]); | |
} | |
if (maxSeverity >= feedbackSeverityError) | |
{ // Any errors invalidate the result. | |
result = nil; | |
} | |
} | |
} | |
} | |
return result; | |
} | |
+ (id)validatedJSONFrom:(NSObject*)jsonValue | |
usingSchemaURL:(NSURL*)schemaURL | |
options:(AGNJSONValidatorOptions)options | |
feedback:(NSArray* __autoreleasing*)feedback | |
{ | |
AGNJSONValidator* validator = [[self alloc] initWithSchemaURL:schemaURL]; | |
id result = [validator validatedJSONValueFromJSONValue:jsonValue options:options]; | |
if (feedback) | |
*feedback = validator.allFeedback; | |
return result; | |
} | |
+ (NSString*)annotatedStringForJSONValue:(NSObject*)jsonValue feedback:(NSArray*)feedback | |
{ | |
AGNContainerWalker* logWalker = [[AGNContainerWalker alloc] init]; | |
AGNJSONContainerLogger* logger = [[AGNJSONContainerLogger alloc] initWithValidatorFeedbackArray:feedback]; | |
[logWalker setDelegate:logger]; | |
[logWalker walkContainer:jsonValue]; | |
return logger.string; | |
} | |
@end | |
@implementation NSDate (M5Timestamp) | |
// Based on http://blog.mro.name/2009/08/nsdateformatter-http-header/ | |
+ (NSDate*)dateWithHTTPDateHeaderString:(NSString*)dateString | |
{ | |
if(!dateString) | |
return nil; | |
static NSDateFormatter* rfc1123 = nil; | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
rfc1123 = [[NSDateFormatter alloc] init]; | |
rfc1123.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; | |
rfc1123.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; | |
rfc1123.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss z"; | |
}); | |
NSDate* result = [rfc1123 dateFromString:dateString]; | |
return result; | |
} | |
- (NSString*)HTTPDateHeaderString | |
{ | |
static NSDateFormatter *df = nil; | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
df = [[NSDateFormatter alloc] init]; | |
df.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; | |
df.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"]; | |
df.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'"; | |
}); | |
return [df stringFromDate:self]; | |
} | |
+ (NSTimeInterval)timeIntervalSince1970WithHTTPDateHeaderString:(NSString*)dateString | |
{ | |
return [[self dateWithHTTPDateHeaderString:dateString] timeIntervalSince1970]; | |
} | |
+ (NSString*)HTTPDateHeaderStringWithTimeIntervalSince1970:(NSTimeInterval)timeInterval | |
{ | |
return [[NSDate dateWithTimeIntervalSince1970:timeInterval] HTTPDateHeaderString]; | |
} | |
@end | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONValidatorConstants.h | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 14/02/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
extern NSString* const titleK; | |
extern NSString* const descriptionK; | |
extern NSString* const itemsK; | |
extern NSString* const typeK; | |
/**/extern NSString* const nullK; | |
/**/extern NSString* const numberK; | |
/**/extern NSString* const IntegerK; | |
/****/extern NSString* const PositiveIntegerK; | |
/**/extern NSString* const objectK; | |
/**/extern NSString* const arrayK; | |
/**/extern NSString* const boolK; | |
/**/extern NSString* const stringK; | |
/****/extern NSString* const NonEmptyStringK; | |
/****/extern NSString* const TimestampK; | |
extern NSString* const propertiesK; | |
extern NSString* const enumK; | |
extern NSString* const requiredK; | |
extern NSString* const definitionsK; | |
extern NSString* const minLengthK; | |
extern NSString* const maxLengthK; | |
extern NSString* const minimumK; | |
extern NSString* const maximumK; | |
extern NSString* const refK; | |
extern NSString* const anyOfK; | |
extern NSString* const validatorIsCatchAllSchemaK; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONValidatorConstants.m | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 14/02/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#import "AGNJSONValidatorConstants.h" | |
// Schema keywords | |
NSString* const titleK = @"title"; | |
NSString* const descriptionK = @"description"; | |
NSString* const itemsK = @"items"; | |
NSString* const typeK = @"type"; | |
/**/NSString* const nullK = @"null"; | |
/**/NSString* const numberK = @"number"; | |
/**/NSString* const IntegerK = @"Integer"; | |
/****/NSString* const PositiveIntegerK = @"PositiveInteger"; | |
/****/NSString* const TimestampK = @"Timestamp"; | |
/**/NSString* const objectK = @"object"; | |
/**/NSString* const arrayK = @"array"; | |
/**/NSString* const boolK = @"bool"; | |
/**/NSString* const stringK = @"string"; | |
/****/NSString* const NonEmptyStringK = @"NonEmptyString"; | |
NSString* const propertiesK = @"properties"; | |
NSString* const enumK = @"enum"; | |
NSString* const requiredK = @"required"; | |
NSString* const definitionsK = @"definitions"; | |
NSString* const minLengthK = @"minLength"; | |
NSString* const maxLengthK = @"maxLength"; | |
NSString* const minimumK = @"minimum"; | |
NSString* const maximumK = @"maximum"; | |
NSString* const refK = @"$ref"; | |
NSString* const anyOfK = @"anyOf"; | |
NSString* const validatorIsCatchAllSchemaK = @"validator.isCatchAllSchema"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONValidatorFeedback.h | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 14/02/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#import <Foundation/Foundation.h> | |
typedef enum { | |
feedbackSeverityNotice = 0, | |
feedbackSeverityWarning = 1, | |
feedbackSeverityError = 2 | |
} AGNJSONValidatorFeedbackSeverity; | |
@interface AGNJSONValidatorFeedback : NSObject | |
@property (nonatomic, readonly) BOOL isError; | |
@property (nonatomic, assign) BOOL treatErrorAsWarning; // Defaults to NO | |
@property (nonatomic, readonly) AGNJSONValidatorFeedbackSeverity severity; | |
@property (nonatomic, readonly) NSArray* itemPath; | |
@property (nonatomic, readonly) NSArray* messagesArray; | |
+ (AGNJSONValidatorFeedback*)errorWithPath:(NSArray*)path messages:(NSArray*)messages; | |
+ (AGNJSONValidatorFeedback*)warningWithPath:(NSArray*)path messages:(NSArray*)messages; | |
+ (AGNJSONValidatorFeedback*)noticeWithPath:(NSArray*)path messages:(NSArray*)messages; | |
+ (NSString*)descriptionForItemPath:(NSArray*)itemPath; | |
+ (NSUInteger)naiveHashForItemPath:(NSArray*)itemPath; | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// AGNJSONValidatorFeedback.m | |
// Agnitio iPlanner | |
// | |
// Created by Matt Gough on 14/02/2013. | |
// Copyright (c) 2013 Agnitio. All rights reserved. | |
// | |
#if ! __has_feature(objc_arc) | |
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). | |
#endif | |
#import "AGNJSONValidatorFeedback.h" | |
@implementation AGNJSONValidatorFeedback | |
- (id)initWithPath:(NSArray*)path messages:(NSArray*)messages | |
{ | |
self = [super init]; | |
if (self) | |
{ | |
_itemPath = [path copy]; | |
_messagesArray = [messages copy]; | |
} | |
return self; | |
} | |
+ (NSString*)descriptionForItemPath:(NSArray*)itemPath | |
{ | |
NSMutableString* result = [NSMutableString string]; | |
BOOL isFirstComponent = YES; | |
for (id component in itemPath) | |
{ | |
if ([component isKindOfClass:[NSNumber class]]) | |
[result appendFormat:@"[%@]", component]; | |
else | |
{ | |
if (!isFirstComponent) | |
[result appendString:@"."]; | |
[result appendFormat:@"%@", component]; | |
} | |
isFirstComponent = NO; | |
} | |
return result; | |
} | |
+ (NSUInteger)naiveHashForItemPath:(NSArray*)itemPath | |
{ | |
NSUInteger result = 0; | |
for (id component in itemPath) | |
{ | |
NSUInteger componentHash = [component hash]; | |
result ^= componentHash; | |
} | |
return result; | |
} | |
- (BOOL)isError | |
{ | |
return (_severity == feedbackSeverityError) && !_treatErrorAsWarning; | |
} | |
- (NSString*)description | |
{ | |
NSMutableString* result = [NSMutableString string]; | |
switch (_severity) { | |
case feedbackSeverityNotice: | |
[result appendString:@"Notice :"]; | |
break; | |
case feedbackSeverityWarning: | |
[result appendString:@"Warning:"]; | |
break; | |
case feedbackSeverityError: | |
[result appendString:_treatErrorAsWarning ? @"error :" : @"Error :"]; | |
break; | |
default: | |
[result appendString:@"Unknown:"]; | |
break; | |
} | |
[result appendString:@"'"]; | |
[result appendString:[AGNJSONValidatorFeedback descriptionForItemPath:_itemPath]]; | |
[result appendString:@"' - "]; | |
NSMutableString* padString = [[NSMutableString alloc] init]; | |
for (NSUInteger indentLength = [result length]; indentLength; --indentLength) | |
[padString appendString:@" "]; | |
NSUInteger index = 0; | |
for (NSString* message in _messagesArray) | |
{ | |
if (index == 0) | |
[result appendString:message]; | |
else | |
[result appendFormat:@"\n%@%@", padString, message]; | |
} | |
return result; | |
} | |
+ (AGNJSONValidatorFeedback*)errorWithPath:(NSArray*)path messages:(NSArray*)messages | |
{ | |
AGNJSONValidatorFeedback* result = [[self alloc] initWithPath:path messages:messages]; | |
result->_severity = feedbackSeverityError; | |
return result; | |
} | |
+ (AGNJSONValidatorFeedback*)warningWithPath:(NSArray*)path messages:(NSArray*)messages | |
{ | |
AGNJSONValidatorFeedback* result = [[self alloc] initWithPath:path messages:messages]; | |
result->_severity = feedbackSeverityWarning; | |
return result; | |
} | |
+ (AGNJSONValidatorFeedback*)noticeWithPath:(NSArray*)path messages:(NSArray*)messages | |
{ | |
AGNJSONValidatorFeedback* result = [[self alloc] initWithPath:path messages:messages]; | |
result->_severity = feedbackSeverityNotice; | |
return result; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment