From 0c5ee71376d3a6625a1d9525f4cb1ae4f73c3614 Mon Sep 17 00:00:00 2001 From: Joan Martin Date: Wed, 11 Mar 2015 12:43:24 +0100 Subject: [PATCH] Adding enum support (value mapping) --- Motis.podspec.json | 4 +- NSObject+Motis.h | 15 ++- NSObject+Motis.m | 116 +++++++++++++++++- SampleProject/Motis Tests/MJValidationTest.m | 30 +++++ .../Validation Test/MJMotisObject.m | 27 ++++ .../Validation Test/MJTestObject.h | 19 +++ SampleProject/Motis/MJAppDelegate.m | 33 +++-- SampleProject/Motis/MJUser.h | 18 +++ SampleProject/Motis/MJUser.m | 24 ++++ 9 files changed, 262 insertions(+), 24 deletions(-) diff --git a/Motis.podspec.json b/Motis.podspec.json index 7fda32d..6b75821 100644 --- a/Motis.podspec.json +++ b/Motis.podspec.json @@ -1,6 +1,6 @@ { "name": "Motis", - "version": "1.0.2", + "version": "1.1.0", "summary": "Easy JSON to NSObject mapping", "description": "Map and set your JSON objects to objective-C objects using Cocoa's KeyValueCoding. This category sets a minimalist set of methods to map the JSON keys into class properties and set your JSON objects into NSObjects subclasses.\n\t\t \nFor a basic overview of how Motis works checkout the Mobile Jazz blog post: http://blog.mobilejazz.cat/ios-using-kvc-to-parse-json\n\nSimple & Powerful.", "homepage": "https://github.com/mobilejazz/Motis", @@ -14,7 +14,7 @@ "social_media_url": "http://twitter.com/mobilejazzcat", "source": { "git": "https://github.com/mobilejazz/Motis.git", - "tag": "1.0.2" + "tag": "1.1.0" }, "source_files": [ "Motis.h", diff --git a/NSObject+Motis.h b/NSObject+Motis.h index f8e29d3..93c9665 100644 --- a/NSObject+Motis.h +++ b/NSObject+Motis.h @@ -25,6 +25,11 @@ **/ #define mts_key(name) NSStringFromSelector(@selector(name)) +/** + * Use this value to define a default value in the value mapping. + **/ +#define MTSDefaultValue [NSNull null] + /* *************************************************************************************************************************************** * * Motis Methods * *************************************************************************************************************************************** */ @@ -194,6 +199,14 @@ * @name Automatic Validation ** ---------------------------------------------- **/ +/** + * Return a mapping for enum types. + * @param key The key of the property. + * @return The dictionary with the enumeration mapping for the given key. + * @discussion Using this method it is possible to convert the value received via the mapping. + **/ ++ (NSDictionary*)mts_valueMappingForKey:(NSString*)key; + /** * Return a mapping between the array property name to the contained object class type. * For example: @{@"myArrayPropertyName": User.class, ... }; @@ -283,6 +296,6 @@ * @param key The key of the attribute. * @discussion This can be useful when using Motis with Core Data, because Core Data will flag `NSManagedObject` instances as updated, triggering database work, even if no properties have meaningfully changed. **/ -- (BOOL)mts_checkValueOfKeyForEqualityBeforeAssignment:(NSString *)key; +- (BOOL)mts_checkValueEqualityBeforeAssignmentForKey:(NSString *)key; @end diff --git a/NSObject+Motis.m b/NSObject+Motis.m index 92cba52..0cbcb38 100644 --- a/NSObject+Motis.m +++ b/NSObject+Motis.m @@ -132,6 +132,12 @@ @interface NSObject (Motis_Private) **/ + (NSDictionary*)mts_cachedMapping; +/** + * Collects all the value mappings from each subclass layer. + * @return The value mapping for the given key. + **/ ++ (NSDictionary *)mts_cachedValueMappingForKey:(NSString*)key; + /** * Collects all the array class mappings from each subclass layer. * @return The Motis Array Class Mapping. @@ -228,12 +234,25 @@ - (void)mts_setValue:(id)value forKey:(NSString *)key // Automatic validation only if the value has not been manually validated if (originalValue == value && validated) - validated = [self mts_validateAutomaticallyValue:&value forKey:mappedKey]; + { + id mappedValue = [self mts_mapValue:value forKey:mappedKey]; + + if (mappedValue != value) + value = mappedValue; + else + validated = [self mts_validateAutomaticallyValue:&value forKey:mappedKey]; + } if (validated) { - if (![self mts_checkValueOfKeyForEqualityBeforeAssignment:mappedKey] - || ![[self mts_valueForKey:key] isEqual:value]) + if ([self mts_checkValueEqualityBeforeAssignmentForKey:mappedKey]) + { + if (![[self mts_valueForKey:key] isEqual:value]) + { + [self setValue:value forKey:mappedKey]; + } + } + else { [self setValue:value forKey:mappedKey]; } @@ -242,6 +261,47 @@ - (void)mts_setValue:(id)value forKey:(NSString *)key [self mts_invalidValue:value forKey:mappedKey error:error]; } +- (id)mts_mapValue:(id)value forKey:(NSString*)key +{ + NSDictionary *valueMapping = [self.class mts_cachedValueMappingForKey:key]; + + if (valueMapping.count > 0) + { + id mappedValue = valueMapping[value]; + + // Direct mapping + if (mappedValue) + return mappedValue; + + // Conversion to string + if ([value isKindOfClass:NSNumber.class]) + { + mappedValue = valueMapping[[value stringValue]]; + if (mappedValue) + return mappedValue; + } + + // Conversion to number + if ([value isKindOfClass:NSString.class]) + { + mappedValue = valueMapping[@([value floatValue])]; + if (mappedValue) + return mappedValue; + + mappedValue = valueMapping[@([value integerValue])]; + if (mappedValue) + return mappedValue; + } + + // NotFound value + mappedValue = valueMapping[MTSDefaultValue]; + if (mappedValue) + return mappedValue; + } + + return value; +} + - (void)mts_setValuesForKeysWithDictionary:(NSDictionary *)keyedValues { for (NSString *key in keyedValues) @@ -374,6 +434,12 @@ + (BOOL)mts_shouldSetUndefinedKeys return mapping.count == 0; } ++ (NSDictionary *)mts_valueMappingForKey:(NSString *)key +{ + // Subclasses might override. + return nil; +} + + (NSDictionary*)mts_arrayClassMapping { // Subclasses might override. @@ -427,7 +493,7 @@ - (void)mts_invalidValue:(id)value forArrayKey:(NSString *)key error:(NSError*)e MJLog(@"Item for ArrayKey <%@> is not valid in class %@. Error: %@", key, [self.class description], error); } -- (BOOL)mts_checkValueOfKeyForEqualityBeforeAssignment:(NSString *)key +- (BOOL)mts_checkValueEqualityBeforeAssignmentForKey:(NSString *)key { // Subclasses might override. return NO; @@ -490,6 +556,48 @@ + (NSDictionary*)mts_cachedMapping } } ++ (NSDictionary*)mts_cachedValueMappingForKey:(NSString*)key +{ + static NSMutableDictionary *valueMappings = nil; + static dispatch_once_t onceToken1; + dispatch_once(&onceToken1, ^{ + valueMappings = [NSMutableDictionary dictionary]; + }); + + @synchronized(valueMappings) + { + NSString *className = stringFromClass(self); + NSMutableDictionary *classValueMappings = valueMappings[className]; + + if (!classValueMappings) + { + classValueMappings = [NSMutableDictionary dictionary]; + valueMappings[className] = classValueMappings; + } + + NSMutableDictionary *valueMapping = classValueMappings[key]; + + if (!valueMapping) + { + Class superClass = [self superclass]; + + NSMutableDictionary *dictionary = nil; + + if ([superClass isSubclassOfClass:NSObject.class]) + dictionary = [[superClass mts_cachedValueMappingForKey:key] mutableCopy]; + else + dictionary = [NSMutableDictionary dictionary]; + + [dictionary addEntriesFromDictionary:[self mts_valueMappingForKey:key]]; + + valueMapping = [dictionary copy]; + classValueMappings[key] = valueMapping; + } + + return valueMapping; + } +} + + (NSDictionary*)mts_cachedArrayClassMapping { static NSMutableDictionary *arrayClassMappings = nil; diff --git a/SampleProject/Motis Tests/MJValidationTest.m b/SampleProject/Motis Tests/MJValidationTest.m index c0b303c..1e96feb 100644 --- a/SampleProject/Motis Tests/MJValidationTest.m +++ b/SampleProject/Motis Tests/MJValidationTest.m @@ -448,6 +448,36 @@ - (void)testStringToIdProtocol XCTAssertEqualObjects(object.idProtocolField, string, @"Failed to map string value"); } +#pragma mark - ENUMS + +#pragma mark Unsigned Enum + +- (void)testUnsignedEnum +{ + NSArray *array = @[@"zero",@"one",@"two",@"three",[NSNull null],@"four"]; + NSArray *results = @[@(MJUnsignedEnumZero),@(MJUnsignedEnumOne),@(MJUnsignedEnumTwo),@(MJUnsignedEnumThree),@(MJUnsignedEnumOne),@(MJUnsignedEnumOne)]; + + [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + MJMotisObject *object = [MJMotisObject new]; + [object mts_setValue:obj forKey:@"unsigned_enum"]; + + XCTAssertEqual(object.unsignedEnum, [results[idx] integerValue], @"Failed to map unsigned enum value"); + }]; +} + +- (void)testSignedEnum +{ + NSArray *array = @[@"zero",@"one",@"two",@"three",[NSNull null],@"four"]; + NSArray *results = @[@(MJSignedEnumZero),@(MJSignedEnumOne),@(MJSignedEnumTwo),@(MJSignedEnumThree),@(MJSignedEnumTwo),@(MJSignedEnumTwo)]; + + [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + MJMotisObject *object = [MJMotisObject new]; + [object mts_setValue:obj forKey:@"signed_enum"]; + + XCTAssertEqual(object.signedEnum, [results[idx] integerValue], @"Failed to map unsigned enum value"); + }]; +} + #pragma mark - FROM DICTIONARY // ------------------------------------------------------------------------------------------------------------------------ // diff --git a/SampleProject/Motis Tests/Validation Test/MJMotisObject.m b/SampleProject/Motis Tests/Validation Test/MJMotisObject.m index fbe7d33..7cd5a03 100644 --- a/SampleProject/Motis Tests/Validation Test/MJMotisObject.m +++ b/SampleProject/Motis Tests/Validation Test/MJMotisObject.m @@ -42,6 +42,9 @@ + (NSDictionary*)mts_mapping @"number_array": mts_key(numbersArray), @"url_array": mts_key(urlsArray), @"date_array": mts_key(datesArray), + + @"unsigned_enum": mts_key(unsignedEnum), + @"signed_enum": mts_key(signedEnum), }; } @@ -55,6 +58,30 @@ + (NSDictionary*)mts_arrayClassMapping }; } + + (NSDictionary *)mts_valueMappingForKey:(NSString *)key +{ + if ([key isEqualToString:mts_key(unsignedEnum)]) + { + return @{@"zero": @(MJUnsignedEnumZero), + @"one": @(MJUnsignedEnumOne), + @"two": @(MJUnsignedEnumTwo), + @"three": @(MJUnsignedEnumThree), + MTSDefaultValue: @(MJUnsignedEnumOne), + }; + } + else if ([key isEqualToString:mts_key(signedEnum)]) + { + return @{@"zero": @(MJSignedEnumZero), + @"one": @(MJSignedEnumOne), + @"two": @(MJSignedEnumTwo), + @"three": @(MJSignedEnumThree), + MTSDefaultValue: @(MJSignedEnumTwo), + }; + } + + return nil; +} + #pragma mark Property Nillification - (void)setNilValueForKey:(NSString *)key diff --git a/SampleProject/Motis Tests/Validation Test/MJTestObject.h b/SampleProject/Motis Tests/Validation Test/MJTestObject.h index b11f526..6ce1203 100644 --- a/SampleProject/Motis Tests/Validation Test/MJTestObject.h +++ b/SampleProject/Motis Tests/Validation Test/MJTestObject.h @@ -10,6 +10,22 @@ @class MJMotisObject; +typedef NS_ENUM(NSUInteger, MJUnsignedEnum) +{ + MJUnsignedEnumZero, + MJUnsignedEnumOne, + MJUnsignedEnumTwo, + MJUnsignedEnumThree +}; + +typedef NS_ENUM(NSInteger, MJSignedEnum) +{ + MJSignedEnumZero, + MJSignedEnumOne, + MJSignedEnumTwo, + MJSignedEnumThree +}; + @interface MJTestObject : NSObject @property (nonatomic, assign) NSInteger integerField; @@ -40,4 +56,7 @@ @property (nonatomic, strong) NSArray *urlsArray; @property (nonatomic, strong) NSArray *datesArray; +@property (nonatomic, assign) MJUnsignedEnum unsignedEnum; +@property (nonatomic, assign) MJSignedEnum signedEnum; + @end diff --git a/SampleProject/Motis/MJAppDelegate.m b/SampleProject/Motis/MJAppDelegate.m index 2614035..f953cf2 100644 --- a/SampleProject/Motis/MJAppDelegate.m +++ b/SampleProject/Motis/MJAppDelegate.m @@ -25,18 +25,7 @@ @implementation MJAppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self testMotis]; - }); - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self testMotis]; - }); - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self testMotis]; - }); - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [self testMotis]; - }); + [self testMotis]; UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; window.rootViewController = [[UIViewController alloc] init]; @@ -67,19 +56,27 @@ - (void)testMotis @"likes_count": [NSNull null], @"uploader":@{@"user_name":@"Joan", @"user_id": @7, - @"followers": @209 + @"followers": @209, + @"user_gender": @"male", + @"awesomeness_level": @3, }, @"users_cast":@[@{@"user_name":@"Stefan", @"user_id": @19, - @"followers": @1209 + @"followers": @1209, + @"user_gender": @"male", + @"awesomeness_level": @1, }, @{@"user_name":@"Hermes", @"user_id": @23, - @"followers": @1455 + @"followers": @1455, + @"user_gender": @"male", + @"awesomeness_level": @2, }, - @{@"user_name":@"Jordi", + @{@"user_name":@"Martha", @"user_id": @14, - @"followers": @452 + @"followers": @452, + @"user_gender": @"female", + @"awesomeness_level": @3, }, ], @"privateVideoKey": @(1234), // <-- simulating an unexpected attribute in the JSON @@ -108,6 +105,8 @@ - (void)testMotis NSLog(@"AFTER parsing: %@", video.mts_extendedObjectDescription); NSLog(@"video.privateVideoKey: %@",[video.privateVideoKey description]); + + NSLog(@"Video.uploader: %@", video.uploader.mts_extendedObjectDescription); } @end diff --git a/SampleProject/Motis/MJUser.h b/SampleProject/Motis/MJUser.h index 50a9912..84a9d90 100644 --- a/SampleProject/Motis/MJUser.h +++ b/SampleProject/Motis/MJUser.h @@ -18,10 +18,28 @@ #import "Motis.h" +typedef NS_ENUM(NSUInteger, MJUserGender) +{ + MJUserGenderUndefined = 0, + MJUserGenderMale, + MJUserGenderFemale, +}; + +typedef NS_ENUM(NSUInteger, MJAwesomenessLevel) +{ + MJAwesomenessLevelZero = 0, + MJAwesomenessLevelOne, + MJAwesomenessLevelTwo, + MJAwesomenessLevelThree, +}; + @interface MJUser : NSObject @property (nonatomic, assign) NSNumber *userId; @property (nonatomic, strong) NSString *username; @property (nonatomic, assign) NSInteger followers; +@property (nonatomic, assign) MJUserGender gender; +@property (nonatomic, assign) MJAwesomenessLevel awesomenessLevel; + @end diff --git a/SampleProject/Motis/MJUser.m b/SampleProject/Motis/MJUser.m index f591446..0882feb 100644 --- a/SampleProject/Motis/MJUser.m +++ b/SampleProject/Motis/MJUser.m @@ -25,6 +25,8 @@ + (NSDictionary*)mts_mapping return @{@"user_name": mts_key(username), @"user_id": mts_key(userId), @"followers": mts_key(followers), + @"user_gender": mts_key(gender), + @"awesomeness_level": mts_key(awesomenessLevel), }; } @@ -33,4 +35,26 @@ + (BOOL)mts_shouldSetUndefinedKeys return NO; } ++ (NSDictionary*)mts_valueMappingForKey:(NSString*)key +{ + if ([key isEqualToString:mts_key(gender)]) + { + return @{@"male": @(MJUserGenderMale), + @"female": @(MJUserGenderFemale), + MTSDefaultValue: @(MJUserGenderUndefined), + }; + } + else if ([key isEqualToString:mts_key(awesomenessLevel)]) + { + return @{@1: @(MJAwesomenessLevelZero), + @2: @(MJAwesomenessLevelOne), + @3: @(MJAwesomenessLevelTwo), + @4: @(MJAwesomenessLevelThree), + MTSDefaultValue: @(MJAwesomenessLevelOne), + }; + } + + return nil; +} + @end